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)
- 内容:用于程序运行时的动态内存分配(如使用
malloc
、calloc
、realloc
等函数申请的内存)。 - 特性:可读写。
- 增长方向:从低地址向高地址增长。
- 管理:由C标准库中的分配器(如
malloc
的实现)管理,它通过brk
/sbrk
或mmap
系统调用向操作系统申请更大的虚拟地址空间。
5. 内存映射区(Memory Mapping Segment)
- 内容:
- 动态链接库(如
.so
文件)映射到此区域(数据并没有真正从磁盘加载到物理内存,它只是建立了一个虚拟地址到文件页的映射关系)。 - 使用
mmap
系统调用进行文件 I/O 映射的区域。 - 分配大块动态内存(
malloc
超过一定阈值时会使用mmap
)。
- 动态链接库(如
- 特性:灵活,可读写。
- 增长方向:通常从高地址向低地址增长。
6. 栈(Stack)
- 内容:用于存放局部变量、函数参数、返回值以及函数调用所需的上下文信息(如返回地址)。
- 特性:可读写。
- 增长方向:从高地址向低地址增长。
- 管理:由编译器和操作系统自动管理,遵循 后进先出(LIFO) 的原则。
内存排布和地址增长方向
进程的虚拟地址空间通常呈现出以下排布规律(以典型的 Linux 系统为例):
- 内核空间(Kernel Space):位于虚拟地址的最高端。这部分内存为操作系统内核保留,用户程序通常无法直接访问。
- 栈(Stack):紧挨着内核空间下方,从高地址向低地址增长。
- 内存映射区:位于栈和堆之间,通常也从高地址向低地址增长。
- 堆(Heap):位于代码段和数据段上方,从低地址向高地址增长。
- 堆和栈的“相遇”:在设计上,堆和栈的增长方向是相反的,它们之间通常会预留一大块未映射的区域。如果堆或栈增长过快,导致它们各自的边界相遇或越过对方,就会导致内存溢出或段错误。
- 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 0000 或 0x5000 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(直接读写内存地址)来控制硬件。 |