用_sdata这样的变量来标记各个段在Flash或RAM中的位置。
ResetHandler的地址被精准地放在0x00000004,方便MCU找到。
这些操作看似天衣无缝,但实际上,它们要么依赖约定俗成的规则,要么是无数次复制粘贴的祖传代码。不同MCU的内存映射可能千差万别,启动脚本的变量命名可能五花八门,甚至程序段的划分也可能各不相同。既然没有统一标准,这些信息就得在项目中明确指定。对于用类Unix-ld工具链接的项目来说,这个指定工作就落在了链接脚本身上。
今天,我们继续以一个最简单的程序为例,拆解一下链接脚本的门道。
链接过程
链接是编译的最后一步,它把一堆编译好的目标文件合并成一个完整的程序,同时填上正确的地址,让所有内容各就各位。
在链接之前,编译器已经把你的源代码逐一编译成了机器码。在这个过程中,编译器会为地址留下占位符,因为它既不知道代码在整个程序中的最终位置,也对当前文件外的符号一无所知。
链接器的工作就是把这些目标文件,连同C标准库等外部依赖,合并成一个完整的程序。为了搞清楚每个部分该放哪儿,链接器需要一份地图——这就是链接脚本的用武之地。最后,链接器会把所有占位符替换成实际地址。
接下来就以我们的代码为例,看看main函数到底发生了什么。编译器先把minimal.c编译成目标文件:
arm-none-eabi-gcc -c -o build/objs/a/b/c/minimal.o minimal.c <CFLAGS>
用nm工具查看minimal.o的符号表:
arm-none-eabi-nm build/objs/a/b/c/minimal.o
...
00000000 T main
...
可以看到,main函数的地址还是个0,说明还没分配具体位置。接下来,链接器登场:
arm-none-eabi-gcc <LDFLAGS> build/objs/a/b/c/minimal.o <其他目标文件> -o build/minimal.elf
再次查看符号表:
arm-none-eabi-nm build/minimal.elf
...
00000294 T main
...
这回main函数有了个具体的地址,链接器完成了它的使命。除了地址分配,链接器还能顺手生成调试信息、清理无用代码,或者做全程序优化(LTO)。不过今天我们先聚焦链接脚本的本质,别的功能下回再说。 |