单片机内存划分详解及全面介绍
1 单片机内存划分总览
内存区域 |
存储内容 |
特性 |
FLASH (Text) |
代码(.text)、常量(.rodata) |
只读,掉电不丢失 |
RAM |
.data(已初始化全局变量) |
读写,掉电丢失 |
|
.bss(未初始化全局变量) |
初始化为0,占用RAM空间 |
|
堆(Heap)(动态内存分配) |
手动分配释放(如malloc) |
|
栈(Stack)(局部变量、函数调用) |
自动分配释放,大小需预先配置 |
1. .text 段(代码段)
定义与特性
代码示例
// 代码本身编译后位于.text段
int main() {
return 0;
}
// const常量位于.rodata段(.text的子段)
const uint32_t FIXED_VALUE = 0x12345678; // 存储在FLASH中
关键点
2. .data 段(已初始化数据段)
定义与特性
代码示例
uint32_t global_var = 0xABCD; // 全局变量,位于.data段
void func() {
static int static_var = 100; // 静态局部变量,也位于.data段
}
初始化过程
编译器将初始值写入FLASH的 .data 镜像区。
单片机启动时,启动代码(如 startup_*.s)将FLASH中的初始值复制到RAM的 .data 段。
链接脚本片段
Ld
.data : {
_data_start = .; /* RAM中.data段的起始地址 */
*(.data*) /* 所有.data段内容 */
_data_end = .; /* RAM中.data段的结束地址 */
} > RAM AT > FLASH /* 物理存储在FLASH,运行时在RAM */
3. .bss 段(未初始化数据段)
定义与特性
代码示例
uint32_t bss_var; // 未初始化,位于.bss段
static char buffer[1024]; // 未初始化的静态数组,位于.bss段
int zero_var = 0; // 显式初始化为0,位于.bss段
初始化过程
启动代码将 .bss 段对应的RAM区域清零:
assembly
/* 典型启动代码片段(ARM汇编) */
ldr r0, =_bss_start
ldr r1, =_bss_end
mov r2, #0
clear_bss:
cmp r0, r1
strlt r2, [r0], #4
blt clear_bss
4. 堆(Heap)
定义与特性
代码示例
void demo_heap() {
int* ptr = (int*)malloc(100 * sizeof(int)); // 从堆分配
if (ptr != NULL) {
// 使用内存
free(ptr); // 必须手动释放
}
}
配置堆大小
在链接脚本中定义堆区域:
ld
_heap_start = .; /* 堆起始地址 */
. = . + 0x1000; /* 堆大小为4KB */
_heap_end = .; /* 堆结束地址 */
5. 栈(Stack)
定义与特性
代码示例
void demo_stack() {
int local_var = 42; // 局部变量,位于栈
char buffer[256]; // 大型局部数组,可能引发栈溢出
// …
}
配置栈大小
在链接脚本中定义栈区域(通常位于RAM顶端):
Ld
_stack_top = ORIGIN(RAM) + LENGTH(RAM); /* 栈顶地址(向下生长) */
_stack_size = 0x800; /* 栈大小为2KB */
. = . – _stack_size; /* 分配栈空间 */
_stack_bottom = .; /* 栈底地址 */
6. 扩展段:.ccmram(紧耦合内存)
定义与特性
代码示例
1
__attribute__((section(".ccmram"))) uint32_t fast_buffer[128]; // 分配到CCM RAM
7. 内存布局验证(Map文件分析)
编译生成的 .map 文件显示各段地址和大小:
Plaintext
.text 0x08000000 0x4000 // 代码和常量(FLASH)
.data 0x20000000 0x200 // 已初始化变量(RAM)
.bss 0x20000200 0x400 // 未初始化变量(RAM)
.heap 0x20000600 0x1000 // 堆区域
.stack 0x20001600 0x800 // 栈区域
8. 实战注意事项
避免栈溢出:
优化RAM使用:
// 将大型只读数据放入FLASH
const uint8_t large_lookup_table[1024] __attribute__((section(".rodata"))) = { … };
动态内存替代方案:
9. 链接脚本高级配置
通过自定义段实现精细控制:
ld
/* 将特定函数放入快速执行区域 */
.fast_code : {
*(.fast_code) /* 自定义段 */
} > FLASH_FAST
/* 将关键变量放入备份RAM(掉电保持) */
.backup_ram : {
_backup_start = .;
*(.backup_data) /* 自定义段 */
_backup_end = .;
} > BACKUP_RAM
通过深入理解内存段的划分,开发者可以优化资源使用,避免常见内存错误,并提升系统可靠性。建议结合具体芯片手册和调试工具(如STM32CubeIDE的Memory Analysis)实时验证内存分配。
2 .bss 段详细介绍
(Block Started by Symbol)
在单片机或嵌入式系统中,内存的 .bss 段(Block Started by Symbol)是一个关键内存区域,用于存储未初始化或显式初始化为零的全局变量和静态变量。它的核心目的是优化存储空间,并确保程序的正确初始化。以下是详细解释:
1. .bss 段的核心特性
特性 |
说明 |
存储内容 |
未初始化的全局变量、静态变量,或显式初始化为0的变量 |
存储介质 |
RAM(掉电丢失数据) |
初始化时机 |
程序启动时由启动代码(Bootloader)清零 |
空间优化 |
不占用FLASH空间(仅记录大小信息,无需存储初始值) |
2. .bss 段的作用
示例代码
// 未初始化的全局变量 → 位于.bss段
uint32_t global_uninitialized;
// 显式初始化为0 → 也存放在.bss段
uint32_t global_zero_initialized = 0;
void func() {
static int static_uninitialized; // 未初始化的静态变量 → .bss段
}
3. 为什么需要.bss段?
4. .bss 段的工作流程
步骤1:编译阶段
步骤2:链接阶段
步骤3:启动阶段
5. .bss 段与.data段的区别
对比项 |
.bss 段 |
.data 段 |
初始化方式 |
启动时清零 |
启动时从FLASH复制初始值到RAM |
FLASH占用 |
不存储初始值(仅记录大小) |
存储初始值 |
变量类型 |
未初始化或初始化为0的变量 |
已初始化的全局/静态变量 |
RAM内容初始化 |
全部清零 |
由FLASH中的初始值覆盖 |
6. 实际调试技巧
(1) 查看.bss段大小
.bss 0x20000000 0x400 // 地址和长度(单位:字节)
(2) 验证.bss段是否被清零
(3) 避免.bss段溢出
7. 注意事项
显式初始化为零的变量:
int x = 0; // 位于.bss段(编译器优化)
编译器会将其放入 .bss 段而非 .data 段,以减少FLASH占用。
未初始化的变量不等于随机值:
在嵌入式系统中,启动代码会清零 .bss 段,因此变量的初始值为 0,而非未定义值。
静态局部变量:
void func() {
static int y; // 未初始化的静态局部变量 → 位于.bss段
}
优化RAM使用:
将初始值为零的大数组放在 .bss 段而非 .data 段,避免占用FLASH空间:
uint8_t large_buffer[1024]; // 正确 → 位于.bss段
// 而非:
uint8_t large_buffer[1024] = {0}; // 错误 → 位于.data段(浪费FLASH)
3 全局变量的内存分段规则
变量类型 |
存储段 |
存储介质 |
生命周期 |
说明 |
已初始化的全局变量 |
.data段 |
RAM |
程序运行期间 |
初始值存储在FLASH中,启动时复制到RAM |
未初始化的全局变量 |
.bss段 |
RAM |
程序运行期间 |
启动时由启动代码清零(初始值为0) |
初始化为0的全局变量 |
.bss段 |
RAM |
程序运行期间 |
编译器优化后等同于未初始化变量 |
const修饰的全局变量 |
.rodata段 |
FLASH(ROM) |
程序生命周期内 |
只读,不可修改(存放于FLASH中) |
静态全局变量 |
.data或.bss |
RAM |
程序运行期间 |
静态全局变量遵循与普通全局变量相同的规则 |
3.1 示例代码与段分配
// 1. 已初始化的全局变量 -> .data段(RAM)
int global_initialized = 42;
// 2. 未初始化的全局变量 -> .bss段(RAM)
int global_uninitialized;
// 3. 显式初始化为0的全局变量 -> .bss段(RAM)
int global_zero_initialized = 0;
// 4. const修饰的全局变量 -> .rodata段(FLASH)
const int global_const = 100;
// 5. 静态全局变量 -> .data或.bss段(RAM)
static int static_global = 5; // .data段(已初始化)
static int static_global_uninit; // .bss段(未初始化)
详细说明
1. 已初始化的全局变量(.data段)
2. 未初始化的全局变量(.bss段)
3. const修饰的全局变量(.rodata段)
4. 静态全局变量
验证方法
1. 查看编译生成的Map文件
2. 调试器观察变量地址
特殊场景与注意事项
1. 初始化为0的变量优化
2. 静态局部变量
3. 多文件中的全局变量
总结
全局变量的存储段完全由初始化状态和修饰符决定:
合理规划全局变量的初始化方式,可显著优化FLASH和RAM的使用效率。
4 内存区域编译器划分
编译器将变量分配到不同内存段,具体取决于变量的类型、作用域和存储类别:
内存段 |
存储内容 |
生命周期 |
示例 |
.text |
可执行代码(机器指令) |
程序运行期间 |
void main() { … } |
.data |
已初始化的全局变量、静态变量 |
程序运行期间 |
int global = 10; |
.bss |
未初始化或初始化为0的全局/静态变量 |
程序运行期间 |
int global_uninit; |
栈 (Stack) |
局部变量、函数参数 |
函数调用期间 |
int local_var = 5; |
堆 (Heap) |
动态分配的内存(malloc/new) |
显式释放前持续存在 |
int* ptr = malloc(100); |
.rodata |
只读常量(const修饰的全局变量) |
程序运行期间 |
const int PI = 3.14; |
2. 变量分配规则
(1) 全局变量
(2) 静态变量
(3) 局部变量
(4) 常量
3. 内存分配流程
步骤1:符号表生成
步骤2:内存段分配
步骤3:地址计算与对齐
步骤4:生成初始化数据
步骤5:栈与堆管理
4. 优化策略
(1) 寄存器分配
(2) 冗余变量消除
(3) 常量传播
5. 动态内存分配
动态内存(堆)由程序员手动管理,编译器生成调用内存管理函数的代码:
int* arr = (int*)malloc(10 * sizeof(int)); // 编译器生成调用malloc的指令
free(arr);
6. 实战验证方法
(1) 查看汇编代码
(2) 分析Map文件
(3) 调试器查看内存
总结
编译器通过类型、作用域和存储类别确定变量内存段,结合对齐和优化策略高效分配内存。理解这些机制有助于编写内存安全的代
5 .data 段大小的决定因素
示例计算
int a = 1; // 4字节 → .data段(RAM)
static double b = 2.0; // 8字节 → .data段(RAM)
const char c = 'A'; // 1字节 → .rodata段(Flash,不计入.data)
2. 如何控制 .data 段大小?
(1) 减少已初始化的全局/静态变量
(2) 链接脚本(Linker Script)配置
在链接脚本中显式定义 .data 段的地址和空间大小,防止溢出:
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K /* Flash配置 */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K /* RAM配置 */
}
SECTIONS {
.data : {
_data_start = .;
*(.data*) /* 所有.data段内容 */
_data_end = .;
} > RAM AT > FLASH /* .data段占用的RAM空间根据变量实际大小动态分配 */
}
(3) 结构体对齐优化
3. 验证 .data 段大小
(1) 生成Map文件
在编译选项中添加 -Wl,-Map=output.map,生成的 output.map 文件会显示各段大小:
.data 0x20000000 0x18 /* 0x18 = 24字节 */
global_var 0x20000000 0x4
static_var 0x20000004 0x8
…
(2) 使用 size 命令
查看可执行文件各段大小:
arm-none-eabi-size firmware.elf
输出示例:
text data bss dec hex filename
12345 678 901 13924 3664 firmware.elf
(3) 调试器查看符号地址
使用调试工具(如GDB)检查变量的地址范围:
(gdb) p &global_var # 输出 0x20000000(RAM地址)
(gdb) p &static_var # 输出 0x20000004
4. 解决 .data 段溢出的问题
如果 .data 段超出RAM容量:
5. 实战示例
场景:某STM32工程 .data 段过大,需优化。
步骤1:识别占用大的变量
int buffer[1024] = {0}; // 初始化为0 → 实际在.bss段(无需优化)
const char* strings[] = {"Hello", "World"}; // 指针数组在.data段,字符串在.rodata段
步骤2:优化代码 将常量指针改为 const,直接指向 .rodata:
const char* const strings[] = {"Hello", "World"}; // 指针本身移至.rodata
步骤3:重新编译并检查Map文件
.data 0x20000000 0x8 /* 优化后大小减少 */
6 .text 段存储与执行
在嵌入式系统中,.text 段存储编译后的 可执行代码(机器指令),其物理位置位于 ROM非易失性存储器(通常是 Flash) 中。以下是详细说明:
1. .text 段的内容
示例代码
// main函数 → 编译后代码存入.text段
int main() {
while(1) {
// …
}
}
// 中断服务程序 → 代码存入.text段
void SysTick_Handler(void) {
// …
}
2. .text 段的存储介质
(1) Flash(非易失性存储器)
(2) 特殊情况:RAM 中执行代码
3. 为什么代码必须存储在 Flash 中?
非易失性需求:
代码需要长期保存,断电后不丢失。
空间容量:
Flash 容量通常远大于 RAM(如 STM32F4 的 Flash 可达 2MB,RAM 仅 192KB)。
安全性:
防止程序运行时意外修改自身代码。
4. .text 段的加载与执行流程
烧录阶段:
编译器将 .text 段代码写入 Flash(通过烧录工具如 ST-Link、J-Link)。
启动阶段:
MCU 上电后,从 Flash 的复位向量(Reset Vector)地址(如 0x08000004)读取指令,开始执行代码。
运行时:
CPU 直接从 Flash 读取指令执行,或通过缓存加速。
5. 验证 .text 段地址的方法
(1) 查看编译生成的 Map 文件
在编译选项中添加 -Wl,-Map=output.map,生成的文件中会显示 .text 段地址:
.text 0x08000000 0x4000 /* 起始地址和大小 */
(2) 调试器查看函数地址
使用 GDB 或 IDE 调试工具(如 Keil、STM32CubeIDE)查看函数地址:
(gdb) p main # 输出类似 0x08000123(位于Flash地址范围内)
(3) 反汇编代码
通过 objdump 生成反汇编文件:
arm-none-eabi-objdump -d firmware.elf > disassembly.txt
查看反汇编结果:
08000123 <main>:
8000123: b480 push {r7}
8000125: af00 add r7, sp, #0
8000127: e7fe b.n 8000127 <main+0x4>
6. 优化 .text 段大小的技巧
编译器优化选项:
避免冗余代码:
手动指定代码段:
高频代码放入 .ram_code 段,减少 Flash 访问次数。
总结
arm-none-eabi-gcc -Os -o firmware.elf main.c
移除未使用的函数。
.ram_code : {
*(.ram_code) /* 将.ram_code段分配到RAM */
} > RAM AT > FLASH /* 初始值在Flash,启动时复制到RAM */
__attribute__((section(".ram_code"))) void fast_function() {
// 高频执行代码(从RAM运行)
}
#pragma pack(1) // 1字节对齐
struct SensorData {
uint8_t id; // 1字节
uint32_t value; // 4字节
}; // 总大小=5字节(默认对齐时为8字节)
#pragma pack() // 恢复默认对齐
int a; // 从 .data 改为 .bss(初始值0,不占用Flash)
const int table[100] = { … }; // 移入Flash的.rodata段
(gdb) p &global_var # 输出类似 0x20000000(RAM地址)
(gdb) p &const_var # 输出类似 0x08001000(Flash地址)
.text 0x08000000 0x200
.data 0x20000000 0x10
.bss 0x20000010 0x8
gcc -S main.c -o main.s
.data
global_var: .word 10 # .data段
.bss
static_var: .space 4 # .bss段
.text
main:
pushl %ebp
movl %esp, %ebp
subl $16, %esp # 栈分配16字节(含局部变量)
const int N = 100;
int arr[N]; // 替换为 int arr[100];
int unused_var = 10; // 被优化删除
register int i; // 建议编译器使用寄存器(实际由编译器决定)
x86示例:分配8字节栈空间
sub esp, 8
int initialized = 0x1234; // 初始值0x1234存储在Flash,启动时复制到RAM
struct Example {
char a; // 1字节
// 填充3字节
int b; // 4字节(地址为4的倍数)
}; // 总大小 = 8字节
int global = 10; // .data段
static float static_var; // .bss段
void func() {
int local; // 栈
}
// file1.c
int global_var = 42; // 定义在.data段
// file2.c
extern int global_var; // 声明使用其他文件中定义的全局变量
void func() {
static int static_local = 10; // 已初始化 -> .data段
static int static_local_uninit; // 未初始化 -> .bss段
}
int x = 0; // 可能被优化到.bss段
.data 0x20000000 0x8 // 已初始化全局变量
global_initialized 0x20000000 0x4
static_global 0x20000004 0x4
.bss 0x20000008 0x8 // 未初始化全局变量
global_uninitialized 0x20000008 0x4
static_global_uninit 0x2000000c 0x4
.rodata 0x08001000 0x4 // 只读常量
global_const 0x08001000 0x4
const int READ_ONLY_DATA = 0x1234;
// READ_ONLY_DATA = 0x5678; // 编译报错:尝试修改只读数据
.data : {
_data_start = .; /* RAM中的.data起始地址 */
*(.data*) /* 所有已初始化的全局变量 */
_data_end = .;
} > RAM AT > FLASH /* 物理存储在FLASH,运行时在RAM */
int* ptr = NULL; // 位于.bss段
uint32_t my_var __attribute__((section(".bss")));
Memory region Used Size Free Size
RAM 0x400 0x1000 // 已用1KB,剩余4KB
extern uint32_t global_uninitialized;
// 调试时检查 global_uninitialized == 0
示例汇编代码(ARM Cortex-M):
ldr r0, =_bss_start /* 加载.bss段起始地址 */
ldr r1, =_bss_end /* 加载.bss段结束地址 */
mov r2, #0 /* 清零值 */
clear_bss_loop:
cmp r0, r1 /* 检查是否到达结束地址 */
strlt r2, [r0], #4 /* 清零内存并递增指针 */
blt clear_bss_loop /* 循环直到完成 */
示例链接脚本片段:
ld
.bss : {
_bss_start = .; /* 记录.bss段起始地址 */
*(.bss*) /* 所有.bss段内容 */
_bss_end = .; /* 记录.bss段结束地址 */
} > RAM
如果未初始化的变量放在 .data 段,需要在FLASH中存储零值,浪费空间。
.bss 段只需记录长度信息,无需存储实际数据。
启动时只需将 .bss 段对应的RAM区域清零,而不是逐个复制初始值(如 .data 段)。
未初始化的变量不需要在FLASH中存储初始值,仅在运行时分配RAM空间并初始化为零。
所有未初始化或零初始化的变量统一管理,避免分散处理。
c
#define POOL_SIZE 10
static uint8_t memory_pool[POOL_SIZE][256]; // 预分配内存池
static bool pool_used[POOL_SIZE] = {0};
void* my_alloc() {
for (int i = 0; i < POOL_SIZE; i++) {
if (!pool_used[i]) {
pool_used[i] = true;
return memory_pool[i];
}
}
return NULL; // 分配失败
}
作者:成为老板的第x-1天