第一章 链接脚本与内存布局
大家好,我是你们的老朋友。做了十几年嵌入式底层开发,从单片机到游戏机系统,我踩过的坑比走过的路还多。今天咱们聊聊链接脚本和内存布局——这玩意儿看着枯燥,但你要是搞不定它,Bootloader 根本跑不起来。
说白了,链接脚本就是告诉编译器:「你的代码放哪儿,数据放哪儿,栈和堆又该在哪儿安家。」没有它,你写再多代码也是白搭。
1.1 链接脚本语法入门
先看一个最简单的链接脚本骨架。我个人习惯从 ENTRY 开始写,因为它定义了程序的第一条指令。
ENTRY(_start)
MEMORY
{
ROM (rx) : ORIGIN = 0x00000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : {
*(.text*)
} > ROM
.data : {
*(.data*)
} > RAM AT> ROM
.bss : {
*(.bss*)
} > RAM
}
嗯,这里要注意三个关键字:
- ENTRY:指定入口点。我见过有人忘了写,结果程序从地址 0 开始跑,直接飞了。
- MEMORY:定义物理内存区域。ROM 是只读的,RAM 可读写。长度别写错,写大了链接器不报错,但运行时必挂。
- SECTIONS:把代码和数据分配到各个段里。
重要提醒:AT> ROM 表示 .data 段在 ROM 里存储,运行时拷贝到 RAM。这个机制叫「加载时地址 vs 运行时地址」,很多新手在这里翻车。
1.2 代码段、数据段、BSS段布局
咱们拆开来看每个段是干嘛的。
| 段名 | 内容 | 存放位置 | 特点 |
|---|---|---|---|
| .text | 可执行指令、常量字符串 | ROM/Flash | 只读,掉电不丢失 |
| .data | 已初始化的全局变量 | 运行时在 RAM,镜像在 ROM | 需要从 ROM 拷贝到 RAM |
| .bss | 未初始化的全局变量 | RAM | 启动时清零,不占 ROM 空间 |
| .rodata | 只读数据(const 变量) | ROM | 和 .text 常合并 |
我在项目中遇到过一个问题:.bss 段忘记清零,结果全局变量初始值全是随机数。那次调试花了我整整两天。后来我养成了习惯——在 Bootloader 里手动清零 .bss。
我的小技巧:在链接脚本里定义两个符号 __bss_start 和 __bss_end,然后在 C 代码里用 memset 清零。这样既清晰又可靠。
1.3 栈与堆的规划
栈和堆是运行时最活跃的区域,但链接脚本里通常只给它们划个范围。具体怎么用,得靠启动代码来初始化。
先看栈。我个人习惯把栈放在 RAM 的最高地址,因为栈是向下生长的。你想想看,如果栈放在中间,一不小心就踩到堆或者数据段了。
_stack_top = ORIGIN(RAM) + LENGTH(RAM);
_stack_size = 4K; /* 根据需求调整 */
堆呢?一般放在 .bss 后面。游戏机系统里,堆主要用于动态分配纹理、音频缓冲区等。但 Bootloader 阶段我建议少用堆,能静态分配就静态分配。
避坑指南:我曾经在一个项目里把栈设得太小,结果函数调用深一点就栈溢出,系统随机死机。排查了三天才发现是栈不够用。记住:中断嵌套、递归调用都会吃栈,留足余量!
1.4 实战:一个游戏机 Bootloader 的内存布局
假设咱们在做一台掌机,CPU 是 Cortex-M7,Flash 2MB,RAM 512KB。我会这样规划:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2M
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 512K
}
SECTIONS
{
/* 中断向量表必须放在开头 */
.isr_vector : {
*(.isr_vector)
} > FLASH
/* 代码段 */
.text : {
*(.text*)
*(.rodata*)
} > FLASH
/* 初始化数据 */
.data : {
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT> FLASH
/* BSS 段 */
.bss : {
_sbss = .;
*(.bss*)
_ebss = .;
} > RAM
/* 堆 */
.heap : {
_heap_start = .;
. = . + 64K;
_heap_end = .;
} > RAM
/* 栈 */
.stack : {
_stack_start = .;
. = . + 16K;
_stack_end = .;
} > RAM
}
为什么栈只给 16K?因为 Bootloader 阶段功能简单,不需要太多栈。等加载完游戏主程序,栈会重新设置。你想想看,省下来的 RAM 可以给游戏用,多划算。
核心要点:链接脚本不是写一次就完事的。每加一个新功能,都要回头看看内存够不够。我习惯在编译后检查 .map 文件,确认每个段的实际大小。
1.5 本章小结
嗯,这一章内容不少。咱们捋一捋:
- 链接脚本用 ENTRY、MEMORY、SECTIONS 三个关键字定义程序布局
- .text 放代码,.data 放初始化数据,.bss 放未初始化数据
- 栈向下生长,放在 RAM 顶部;堆向上生长,放在 .bss 后面
- Bootloader 里尽量少用堆,栈大小要留余量
下一章咱们会深入启动代码,看看怎么用汇编把 C 环境搭起来。到时候你会看到,链接脚本里定义的符号是怎么在代码里发挥作用的。
记住:内存布局是 Bootloader 的地基。地基没打好,上面盖什么都白搭。