Xiao's Blog

Sep 30, 2025

Linux和裸机程序内存布局

所谓的“进程的内存”通常指的是进程的虚拟地址空间(Virtual Address Space),而不是直接的物理内存。

每个进程都有一个独立、私有的虚拟地址空间,操作系统(如 Linux)会将这个空间划分为几个主要区域,并进行特定的排列。


Linux

进程虚拟地址空间的主要分区

一个进程的虚拟地址空间通常从低地址向高地址延伸,主要被划分为以下几个区域:

1. 代码段(Text Segment / Code Segment)

  • 内容:存放进程执行的机器指令(程序代码)。
  • 特性:通常是只读的,以防止程序意外地修改自身的指令。它可以被多个进程共享(例如多个进程运行同一个程序)。
  • 排布:位于虚拟地址空间的低端

2. 数据段(Data Segment)

  • 内容:存放已初始化全局变量静态变量
  • 特性可读写。这些变量的值在程序启动时就已经确定。

3. BSS 段(Block Started by Symbol Segment)

  • 内容:存放未初始化全局变量静态变量
  • 特性可读写。在程序加载时,操作系统会将其初始化为 0 或空指针。
  • 名称由来:BSS 段在程序文件中不占实际空间,只是记录了所需内存的大小,这有助于减小可执行文件体积。

4. 堆(Heap)

  • 内容:用于程序运行时的动态内存分配(如使用 malloccallocrealloc 等函数申请的内存)。
  • 特性可读写
  • 增长方向:从低地址向高地址增长
  • 管理:由C标准库中的分配器(如 malloc 的实现)管理,它通过 brk / sbrkmmap 系统调用向操作系统申请更大的虚拟地址空间。

5. 内存映射区(Memory Mapping Segment)

  • 内容
    • 动态链接库(如 .so 文件)映射到此区域(数据并没有真正从磁盘加载到物理内存,它只是建立了一个虚拟地址文件页的映射关系)。
    • 使用 mmap 系统调用进行文件 I/O 映射的区域。
    • 分配大块动态内存(malloc 超过一定阈值时会使用 mmap)。
  • 特性:灵活,可读写。
  • 增长方向:通常从高地址向低地址增长

6. 栈(Stack)

  • 内容:用于存放局部变量、函数参数返回值以及函数调用所需的上下文信息(如返回地址)。
  • 特性可读写
  • 增长方向:从高地址向低地址增长
  • 管理:由编译器和操作系统自动管理,遵循 后进先出(LIFO) 的原则。

内存排布和地址增长方向

进程的虚拟地址空间通常呈现出以下排布规律(以典型的 Linux 系统为例):

  1. 内核空间(Kernel Space):位于虚拟地址的最高端。这部分内存为操作系统内核保留,用户程序通常无法直接访问。
  2. 栈(Stack):紧挨着内核空间下方,从高地址向低地址增长。
  3. 内存映射区:位于栈和堆之间,通常也从高地址向低地址增长。
  4. 堆(Heap):位于代码段和数据段上方,从低地址向高地址增长。
    • 堆和栈的“相遇”:在设计上,堆和栈的增长方向是相反的,它们之间通常会预留一大块未映射的区域。如果堆或栈增长过快,导致它们各自的边界相遇或越过对方,就会导致内存溢出或段错误。
  5. BSS 段 / 数据段 / 代码段:这三个静态区域位于虚拟地址的低端

简化示意图(从低地址到高地址)

虚拟地址(高) 区域名称 增长方向 内存用途
内核空间 - 操作系统内核使用
栈 (Stack) ↓ (向低地址增长) 局部变量、函数调用
未分配区域 - -
内存映射区 (mmap) ↓ (向低地址增长) 动态库、大块内存
未分配区域 - -
堆 (Heap) ↑ (向高地址增长) 动态分配内存 (malloc)
BSS 段 - 未初始化全局/静态变量
数据段 - 已初始化全局/静态变量
代码段 (Text) - 程序的机器指令
虚拟地址(低)

按需分页(Demand Paging)

真正的效率提升在于**按需分页(Demand Paging)**机制:

  • 初始状态:动态库被映射后,其对应的虚拟地址并没有实际的物理内存页与之对应。
  • 首次访问:当程序第一次试图执行动态库中的某个函数或访问某个数据时,CPU 会根据虚拟地址去查找页表。此时发现该页还没有加载到物理内存,会触发一个缺页中断(Page Fault)
  • 加载页面:操作系统内核捕获到缺页中断后,会从磁盘上的动态库文件找到对应的**页面(Page)**内容,将其加载到一个**物理内存帧**中,并更新页表,建立虚拟地址与物理地址的映射。
  • 继续执行:程序随后在中断处恢复执行,就能够顺利访问该内存了。

裸机(STM32)

这是一个从通用计算机(如 Linux 进程)转移到嵌入式系统(如 STM32 微控制器)的精彩问题!

STM32 微控制器没有操作系统提供的复杂的虚拟内存机制,也没有进程的概念。它的内存布局直接对应于其物理内存片上外设的地址空间。

STM32 的内存布局是固定的,由其内部架构(基于 ARM Cortex-M 内核)决定,主要分为以下几个关键区域:


STM32 微控制器的内存布局

STM32 采用**哈佛结构(Harvard Architecture)*或*改进的冯·诺依曼结构,其中代码和数据可以分开访问。其内存空间被划分为几个主要区域,每个区域都有一个固定的地址范围。

1. 闪存(Flash Memory) - CODE 区

特性 描述
地址范围 通常从 0x0800 0000 开始
内容 存放程序代码(指令)和常量数据(如字符串字面量、const 变量)。
特性 非易失性(掉电不丢失)。这是程序烧录的主要目标区域。程序启动时,CPU 从这里取指令执行。

2. SRAM(Static RAM) - DATA/HEAP/STACK 区

SRAM 是 STM32 的主要工作内存易失性(掉电丢失)。它被细分为我们熟悉的数据、堆和栈等区域。

特性 描述
地址范围 通常从 0x2000 0000 开始
内容
数据段(.data) 存放已初始化全局/静态变量。程序启动时,这些数据会从 Flash 复制到 SRAM。
BSS 段(.bss) 存放未初始化全局/静态变量。程序启动时,这部分空间会被清零
堆(Heap) 用于程序运行时的动态内存分配(如 malloc)。从低地址向高地址增长
栈(Stack) 用于存放局部变量、函数参数返回地址从高地址向低地址增长
注意 在 STM32 上,堆和栈的空间是静态预留的,在链接脚本(Linker Script)中定义它们的大小和位置。堆和栈的增长如果互相侵占,会导致程序崩溃。

3. 外设(Peripherals) - 外设寄存器区

特性 描述
地址范围 通常从 0x4000 00000x5000 0000 开始
内容 存放所有片上外设的控制寄存器(如 GPIO、定时器、ADC 等)。
特性 程序员通过读写特定地址的内存来控制硬件。例如,向某个地址写入 1 就可以点亮一个 LED 灯。这就是所谓的内存映射 I/O (MMIO)

4. 启动区域(Boot Memory)

特性 描述
地址范围 0x0000 0000 开始
内容 包含了重映射的地址。STM32 在启动时,会将这个低地址区域映射到 Flash、系统存储器(System Memory,用于 Bootloader)或 SRAM 中的某一个。
特性 决定了微控制器启动后执行的第一条指令来自哪里。

与 PC 内存模型的关键区别

特性 PC / Linux 进程 STM32 / 嵌入式系统
内存类型 虚拟内存(抽象层) 物理内存(直接访问)
内存管理 复杂,由 OS 动态管理和分配,使用分页页表 简单,静态分配,内存由链接器脚本预先分配。
动态库/mmap 大量使用 mmap 加载动态库,实现按需分页 不存在动态库概念,所有代码和数据在编译时就确定了位置。
堆/栈 堆和栈之间的空间巨大且包含 mmap 区。 堆和栈的空间是有限且相邻的,由链接脚本严格划分。
外设 通过系统调用或驱动访问硬件。 通过内存映射 I/O(直接读写内存地址)来控制硬件。
OLDER > < NEWER