第三章 链接脚本与内存布局

好,咱们今天聊聊链接脚本。说实话,很多做嵌入式开发的朋友,写了几年C代码,可能都没正眼看过.lds文件。但我得说,如果你真想做Bootloader开发,这一关必须过。

链接脚本,说白了就是告诉链接器:你的代码该放哪儿,数据该放哪儿,堆栈又该放哪儿。没有它,编译器生成的二进制文件就是一盘散沙。

3.1 链接脚本(.lds)的作用

链接脚本的全称叫Linker Script,后缀通常是.lds。它的核心任务就三个:

  • 定义内存布局:告诉链接器芯片的ROM从哪个地址开始,RAM又在哪里
  • 控制段放置:决定.text、.data、.bss这些段分别放到哪个区域
  • 提供符号信息:给程序提供一些关键地址,比如栈顶地址、代码起始地址

我刚开始做Bootloader时,犯过一个低级错误——没在链接脚本里定义栈空间。结果程序一跑就飞,查了两天才发现是栈指针指向了未初始化区域。嗯,从那以后,我每次写链接脚本都会反复确认栈的地址。

核心观点:链接脚本是嵌入式工程的"地图",没有它,你的代码就是无头苍蝇。

3.2 内存区域划分

一个典型的嵌入式系统,内存区域大概分成这几块:

区域 用途 特点
ROM(Flash) 存放代码、只读数据 掉电不丢失,但写入慢
RAM 存放变量、堆栈 掉电丢失,读写快
Stack 函数调用、局部变量 向下增长,大小固定
Heap 动态内存分配 向上增长,容易碎片化

你想想看,Bootloader启动时,CPU上电第一件事就是去ROM的0x00000000(或者0x08000000,取决于芯片)取第一条指令。所以链接脚本必须把复位向量放在这个位置。

3.3 一个典型的链接脚本示例

咱们直接看代码,我拿STM32的链接脚本举个例子:

/* STM32F103 链接脚本示例 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS
{
    /* 中断向量表,必须放在Flash起始位置 */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } > FLASH

    /* 代码段 */
    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        . = ALIGN(4);
    } > FLASH

    /* 只读数据段 */
    .rodata :
    {
        . = ALIGN(4);
        *(.rodata)
        *(.rodata*)
        . = ALIGN(4);
    } > FLASH

    /* 初始化数据段,运行时需要从Flash拷贝到RAM */
    .data :
    {
        . = ALIGN(4);
        _sdata = .;
        *(.data)
        *(.data*)
        _edata = .;
    } > RAM AT> FLASH

    /* BSS段,运行时清零 */
    .bss :
    {
        . = ALIGN(4);
        _sbss = .;
        *(.bss)
        *(.bss*)
        *(COMMON)
        _ebss = .;
    } > RAM

    /* 堆空间 */
    .heap :
    {
        . = ALIGN(8);
        _sheap = .;
        . = . + _HeapSize;
        _eheap = .;
    } > RAM

    /* 栈空间 */
    .stack :
    {
        . = ALIGN(8);
        _sstack = .;
        . = . + _StackSize;
        _estack = .;
    } > RAM
}

个人经验:我习惯在链接脚本里给每个段都加上起始和结束符号,比如_sdata、_edata。这样在C代码里可以直接用extern声明来获取这些地址,做数据拷贝或者清零时特别方便。

3.4 符号表与重定位

符号表,你可以理解成一个"地址-名字"的对照表。链接器在链接过程中,会把每个函数名、全局变量名都绑定到一个具体的地址上。

重定位就更关键了。你想想看,编译器在编译每个.c文件时,并不知道其他文件里的函数或者变量到底在哪个地址。所以它只能先留个空位,等链接器来填。这个过程就叫重定位。

我记得有一次调试一个Bootloader,发现函数跳转总是跑飞。后来用readelf工具一看,发现符号表里某个函数的地址根本不对。原来是链接脚本里RAM的起始地址写错了,导致重定位时所有地址都偏移了。嗯,这种问题最坑人,因为编译器不会报错,只有跑起来才知道。

避坑指南:我曾经因为忘记在链接脚本里给.data段指定加载地址(LMA),导致程序启动时数据拷贝失败。记住:.data段在Flash里有一份拷贝,运行时需要搬到RAM里。链接脚本里用"AT> FLASH"就是干这个的。

3.5 内存布局的实战要点

做Bootloader开发时,内存布局有几个地方要特别注意:

  1. 中断向量表的位置:必须放在Flash的起始地址,且4字节对齐。有些芯片支持重映射,但默认都是从0地址开始。
  2. 栈空间大小:别设太小。我一般给Bootloader分配至少1KB的栈空间。如果用到中断嵌套,还得再加。
  3. 堆和栈的冲突:堆向上增长,栈向下增长。如果两者之间没有足够的隔离空间,就会互相覆盖。这种bug极难排查。
  4. Flash和RAM的地址映射:不同芯片的地址映射完全不同。比如STM32的Flash从0x08000000开始,而某些国产芯片可能从0x00000000开始。一定要查数据手册。

说白了,链接脚本就是你和硬件之间的"契约"。你告诉链接器内存长什么样,链接器帮你把代码安排得明明白白。如果这个契约写错了,后面所有的工作都是白费。

总结一下:链接脚本不是可有可无的配置,它是Bootloader的基石。花半小时把.lds文件吃透,能省下后面几天的调试时间。我个人建议,每做一个新项目,第一件事就是根据芯片手册重新写链接脚本,而不是从旧项目里复制粘贴。

好,这一章就到这里。下一章咱们聊聊启动流程,看看CPU上电后到底是怎么一步步跑到main函数的。