3. 链接脚本(Linker Script)入门:链接脚本的作用、MEMORY命令定义内存区域、SECTIONS命令分配段
说实话,我刚入行那会儿,觉得链接脚本就是个黑盒子。编译能过就行,谁管它里面写了啥?直到有一次,我调试一个Bootloader,程序死活跑不起来,用JTAG单步一看,中断向量表根本没放在起始地址。折腾了两天,最后发现是链接脚本里少写了一行。嗯,从那以后,我再也不敢小看这个文件了。
链接脚本,说白了就是告诉链接器:你的代码该放哪儿,数据该放哪儿。对于Bootloader来说,这尤其重要——你想想看,Bootloader通常跑在SRAM里,或者从Flash启动后要重定位,没有链接脚本,编译器根本不知道你的内存布局长什么样。
3.1 链接脚本到底在干什么?
我们平时写C代码,用int a = 0;定义一个全局变量,用void func(void) {}定义一个函数。编译器把这些符号翻译成目标文件(.o),但目标文件里只有相对地址。链接器拿到这些目标文件后,需要根据链接脚本的指示,给每个段(Section)分配一个绝对地址。
链接脚本的核心任务就两个:
- 定义内存区域:芯片有哪些物理内存?Flash多大?SRAM多大?地址范围是多少?
- 分配段:代码段(.text)放Flash还是RAM?数据段(.data)初始值放哪儿?运行时要搬到哪里?
我个人的习惯是,每做一个新平台,第一件事就是打开芯片手册,把Flash和RAM的地址范围抄下来,然后写链接脚本。这一步做扎实了,后面能省很多调试时间。
核心观点:链接脚本是嵌入式工程师和硬件之间的契约。你告诉链接器内存长什么样,链接器帮你把程序安排得明明白白。
3.2 MEMORY命令:定义你的内存地图
MEMORY命令用来描述目标芯片的物理内存。语法很简单,但参数很关键。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
每个内存区域都有三个属性:
| 参数 | 含义 | 常见值 |
|---|---|---|
ORIGIN |
起始地址 | 芯片手册里的基地址 |
LENGTH |
大小 | 以K或M为单位 |
属性 |
访问权限 | r读、w写、x执行 |
这里有个坑,我曾经踩过:属性里的r、w、x不是随便写的。如果你把Flash写成rwx,链接器可能会把可写数据放进去,但Flash在运行时是不能写的(除非你做了特殊处理)。所以Flash我一般只给rx,SRAM给rwx。
注意:ORIGIN和LENGTH必须和芯片手册严格一致。多写一个字节,链接器都不会报错,但程序跑起来就可能访问到不存在的地址,导致HardFault。
3.3 SECTIONS命令:把段安排得明明白白
SECTIONS命令是链接脚本的核心。它定义了每个段放在哪个内存区域,以及段的排列顺序。
一个典型的Bootloader链接脚本大概长这样:
SECTIONS
{
. = ORIGIN(FLASH);
.text :
{
KEEP(*(.isr_vector)) /* 中断向量表,必须放在最前面 */
*(.text*) /* 代码段 */
*(.rodata*) /* 只读数据 */
. = ALIGN(4);
} > FLASH
.data : AT(ADDR(.text) + SIZEOF(.text))
{
_sdata = .; /* 数据段起始地址(RAM中) */
*(.data*) /* 初始化数据 */
. = ALIGN(4);
_edata = .; /* 数据段结束地址 */
} > SRAM AT> FLASH
.bss :
{
_sbss = .; /* BSS段起始地址 */
*(.bss*) /* 未初始化数据 */
. = ALIGN(4);
_ebss = .; /* BSS段结束地址 */
} > SRAM
}
这段脚本里,有几个关键点我得跟你聊聊:
KEEP:告诉链接器,这个段即使没人引用也要保留。中断向量表就是典型例子——它没有函数调用它,但CPU上电后会直接跳过去。AT:指定加载地址(LMA)和运行地址(VMA)。比如.data段,运行时要放在SRAM里(VMA),但初始值存在Flash里(LMA)。Bootloader启动后,需要把数据从Flash拷贝到SRAM。ALIGN:对齐。ARM Cortex-M要求中断向量表4字节对齐,有些外设DMA要求8字节甚至16字节对齐。不加对齐,程序可能莫名其妙地崩溃。
我的经验:_sdata、_edata这些符号,是给启动代码用的。你可以在C代码里用extern uint32_t _sdata;声明它们,然后取地址来拷贝数据。这是Bootloader里最常用的手法。
3.4 一张图看懂链接脚本的工作流程
下面这张SVG图,展示了从目标文件到最终可执行文件的映射过程。我画这张图的时候,特意把MEMORY和SECTIONS的关系标清楚了。
从这张图你能看到,链接脚本就像个调度员。它把目标文件里的各个段,按照你的要求,安排到Flash和SRAM的对应位置。其中.data段最特殊——它有两个地址:一个在Flash里存着初始值,一个在SRAM里运行。Bootloader启动后,必须把数据从Flash拷贝到SRAM,否则全局变量就是错的。
3.5 常见陷阱与避坑指南
链接脚本写起来不难,但坑不少。我把自己踩过的坑列出来,你遇到了可以少走弯路:
- 段顺序搞反:中断向量表必须放在Flash的最开头(地址0x08000000)。如果你把其他段放在它前面,CPU上电后找不到向量表,直接跑飞。
- 忘记
KEEP:链接器默认会优化掉“未被引用”的段。中断向量表没人调用它,不加KEEP就会被丢掉。我有个同事就因为这个,调试了整整一天。 - 对齐没对齐:ARM的
__align(8)不是摆设。有些外设的DMA要求缓冲区8字节对齐,链接脚本里不加ALIGN(8),DMA传输就会出错。 - LMA和VMA搞混:
.data段的AT> FLASH很容易漏掉。漏掉的话,链接器会把数据段放在SRAM的地址上,但烧录时不会把初始值写进Flash。启动后SRAM里的数据是随机的。
我曾经在一个项目里,把.bss段也加了AT属性,结果启动代码把一堆零从Flash拷贝到SRAM,白白浪费了Flash空间和启动时间。其实.bss段不需要初始值,启动时直接清零就行。
3.6 小结
链接脚本是Bootloader开发的基石。你只要记住三件事:
- 用
MEMORY告诉链接器芯片有哪些内存。 - 用
SECTIONS告诉链接器每个段放哪儿。 - 用
AT处理.data段的加载地址和运行地址。
做到这三点,你的Bootloader内存布局就不会出大问题。剩下的,就是在实际项目中慢慢积累经验了。