{"code":1,"msg":"成功","time":"1791589173","data":{"title":"MCU 链接脚本“升级”","content":"# 链接脚本深入：处理孤儿 Section 与对齐问题\r\n\r\n## 一、从\"能跑\"到\"严谨\"\r\n\r\n在上一期中，我们完成了一个最简单的链接脚本，只安排了程序最核心的几个 section：\r\n\r\n- `.text`\r\n- `.rodata`\r\n- `.data`\r\n- `.bss`\r\n- `.isr_vector`（芯片要求的中断向量表）\r\n\r\n实测表明，使用该脚本，程序确实能够正常跑起来。\r\n\r\n但一个真实的项目工程，除了上述必要的 section 之外，还包含许多其他的 section——比如调试相关的 `.debug_*` sections、`.comment`、`.ARM.attributes` 等等。这些 section 没有在链接脚本中显式安排，被称为**孤儿 section（Orphan Section）**。  \r\n\r\n完整视频流程: \r\n【MCU 链接脚本 (二)】 https:\/\/www.bilibili.com\/video\/BV1GahD6GENy\/?share_source=copy_web&vd_source=79b2c8de19b1377f95a5ada384ced7f2\r\n---\r\n\r\n## 二、什么是孤儿 Section？\r\n\r\n根据 **GNU LD 官方手册**的说明，孤儿 section 最终会被链接器根据一定规则自动安置：\r\n\r\n- 要么归入同名的 output section\r\n- 要么由链接器创建一个合适的 output section 来存放\r\n\r\n也就是说，即使不做任何处理，孤儿 section 最终也会出现在 ELF 文件中。程序依然能跑。\r\n\r\n**但依赖链接器自动安排，不能适应所有情况：**\r\n\r\n1. **源码可能引用 section 相关的符号。** 比如 `.data` section 就需要把起始符号（`_sdata`）和结束符号（`_edata`）引出，供启动代码拷贝数据使用。这种情况就必须显式安排。\r\n2. **对运行地址有要求。** 比如向量表所在的 section，芯片可能有特殊的地址要求。\r\n3. **链接器自动安排的行为可能随版本变化。** 一旦升级工具链，原本\"恰好能跑\"的东西可能突然出问题。\r\n\r\n所以为了保证所有 section 都能被妥善处理，我们需要先搞清楚：**程序中到底有哪些 section 没有被处理？**\r\n\r\n难道要逐个查看每个目标文件和静态库的 section 清单吗？显然不需要。\r\n\r\n---\r\n\r\n## 三、让链接器主动\"报料\"\r\n\r\n我们可以在链接标志位中添加孤儿 section 处理的警告选项：\r\n\r\n```bash\r\n-Wl,--warn-orphan\r\n```\r\n\r\n这样在链接阶段，凡是没有在链接脚本中显式安排的 input section，链接器都会发出警告，并明确告知该 section 最终被放置到了哪个 output section。\r\n\r\n编译后可以看到大量警告输出，每个目标文件或静态库中没有被安排的 section 都会列出来。**哪些孤儿 section 没处理，一目了然。**\r\n\r\n---\r\n\r\n## 四、Section 的两大分类\r\n\r\n要处理这些孤儿 section，我们需要先建立一个基本认知。**从用途上，section 可以分为两大类：**\r\n\r\n### 第一类：程序运行时需要的\r\n\r\n包括 `.text`、`.rodata`、`.data`、`.bss` 等。\r\n\r\n它们的共同特征是带有 **`SHF_ALLOC`（A 标志）**。使用 `readelf -S` 工具读取 ELF 文件信息时，最后一列 flags 中带有 `A` 的，就是 ALLOC 的意思。\r\n\r\n这些 section 在链接脚本中需要分配具体的内存区域（Flash 或 RAM），并且会被烧录到 Flash 中。程序执行期间会**直接访问**这些 section。\r\n\r\n### 第二类：程序运行时不需要的\r\n\r\n包括调试信息（`.debug_*`）、ARM 属性描述（`.ARM.attributes`）、`.comment` 等。\r\n\r\n它们**不带** `SHF_ALLOC` 标志。程序运行时永远不会访问，仅由调试器、链接器、`readelf` 等工具使用。这类 section 不需要分配运行时地址，也不应被烧录到目标设备。\r\n\r\n### 分类总结\r\n\r\n| 类型 | 特征 | 典型 section | 处理方式 |\r\n|------|------|-------------|----------|\r\n| 第一类 | 带 `SHF_ALLOC`（A 标志） | `.text`、`.rodata`、`.data`、`.bss` | 分配内存区域 |\r\n| 第二类 | 不带 `SHF_ALLOC` | `.debug_*`、`.ARM.attributes`、`.comment`、`.symtab` 等 | 显式列出，不分配内存 |\r\n\r\n**基于这个分类，处理 section 的原则很清晰：第一类的要分配内存区域，第二类的则不需要分配。**\r\n\r\n---\r\n\r\n## 五、逐个处理孤儿 Section\r\n\r\n下面按照从第二类到第一类的顺序，逐个处理。\r\n\r\n### 1. `.ARM.attributes` —— ARM ABI 属性\r\n\r\n`.ARM.attributes` 是 ARM ABI 规定的标准 section，包含了：\r\n\r\n- CPU 架构\r\n- 浮点 ABI（硬浮点 \/ 软浮点）\r\n- 指令集版本\r\n\r\n链接器、调试器和运行时库都会读取这些信息。\r\n\r\n**程序运行时不需要，属于第二类。** 在链接脚本中显式列出来即可，不需要分配任何内存区域：\r\n\r\n```ld\r\n.ARM.attributes 0 : { *(.ARM.attributes) }\r\n```\r\n\r\n### 2. `.debug_*` —— 调试信息\r\n\r\n所有 `.debug_*` 开头的 sections 都是调试器使用的，程序运行时完全不需要。\r\n\r\n同样在链接脚本中显式列出，不分配内存区域：\r\n\r\n```ld\r\n.debug_aranges  0 : { *(.debug_aranges) }\r\n.debug_info     0 : { *(.debug_info) }\r\n.debug_abbrev   0 : { *(.debug_abbrev) }\r\n.debug_line     0 : { *(.debug_line) }\r\n.debug_frame    0 : { *(.debug_frame) }\r\n.debug_str      0 : { *(.debug_str) }\r\n.debug_loc      0 : { *(.debug_loc) }\r\n.debug_ranges   0 : { *(.debug_ranges) }\r\n```\r\n\r\n> **注意：** 调试相关的 section 比较多，**要一个个显式写出来，而不是用通配符一次性囊括**（如 `*(.debug*)`）。这样可以精确控制哪些调试信息被保留、哪些可以丢弃。\r\n\r\n### 3. `.comment` —— 编译器版本信息\r\n\r\n`.comment` section 包含编译器版本字符串，是给开发者查看的，程序运行时不使用。处理方式相同：\r\n\r\n```ld\r\n.comment 0 : { *(.comment) }\r\n```\r\n\r\n### 4. `.symtab`、`.strtab`、`.shstrtab` —— 符号与字符串表\r\n\r\n这些 section 由链接器从目标文件中合并生成，供调试器、`readelf` 等工具使用：\r\n\r\n- **`.symtab`** —— 符号表\r\n- **`.strtab`** —— 保存符号名对应的字符串\r\n- **`.shstrtab`** —— section 名字的字符串表\r\n\r\n这些 section 都不是程序运行时需要的。处理方式同样是显式列出、不分配内存区域：\r\n\r\n```ld\r\n.symtab        0 : { *(.symtab) }\r\n.strtab        0 : { *(.strtab) }\r\n.shstrtab      0 : { *(.shstrtab) }\r\n```\r\n\r\n### 5. `.ARM.exidx` 与 `.ARM.extab` —— 异常处理展开表\r\n\r\n这两个是 **ALLOC 标志的 section**，属于**第一类**。\r\n\r\n它们是 ARM 架构用于 **unwind 机制**进行栈回溯的元数据，主要用于 MCU 崩溃时追踪问题。\r\n\r\n#### 为什么裸机项目也需要关注它？\r\n\r\n虽然 MCU 裸机程序一般不会使用 unwind 机制进行栈回溯（因为太\"重\"了），即使引入 C++，一般也会禁用异常和 RTTI，同样不依赖它。\r\n\r\n**但是——\"用不到\"不代表\"不会生成\"。**\r\n\r\n- 编译器可能默认开启 `-funwind-tables` 标志\r\n- 链接器（如 LLD）可能自动合成该 section\r\n- C\/C++ 标准库内部也可能引用该 section 的起止符号\r\n\r\n因此最稳妥的做法是**在链接脚本中显式安排该 section 并导出起止符号**，避免链接失败。\r\n\r\n#### `.ARM.exidx` 与 `.ARM.extab` 的关系\r\n\r\n`.ARM.exidx` 需要搭配 `.ARM.extab` 一起使用——一个是**索引表**，另一个存放**具体的展开字节码**。只安排其中一个，会导致回溯功能不完整。\r\n\r\n```ld\r\n.ARM.extab :\r\n{\r\n    *(.ARM.extab* .gnu.linkonce.armextab.*)\r\n} > RAM\r\n\r\n.ARM.exidx :\r\n{\r\n    __exidx_start = .;\r\n    *(.ARM.exidx* .gnu.linkonce.armexidx.*)\r\n    __exidx_end = .;\r\n} > RAM\r\n```\r\n\r\n#### 实际项目中的栈回溯方案\r\n\r\n实际 MCU 裸机项目中，崩溃时的问题回溯通常依赖**异常栈帧解析**和**栈扫描（stack walk）**，而不是 unwind 方式。如果对 MCU HardFault 回溯有兴趣，可以查看相关专题。\r\n\r\n---\r\n\r\n## 六、验证：所有警告消除\r\n\r\n现在，所有产生警告的 section 都已显式安排完成。\r\n\r\n删除编译产物，重新编译——**没有任何警告。**\r\n\r\n查看生成的 map 文件，可以确认每个 section 的布局与链接脚本中的描述完全一致。\r\n\r\n---\r\n\r\n## 七、对齐问题：让确定性更强\r\n\r\n### 为什么要关注对齐？\r\n\r\n对于 32 位 MCU，数据访问最好保证 **4 字节对齐**。虽然部分芯片支持非对齐或半字访问，但非对齐访问通常会带来额外的总线周期，降低执行效率。\r\n\r\n所以，`.text`、`.data`、`.bss` 等核心 section 的运行地址和加载地址都应保证 4 字节对齐。\r\n\r\n查看 map 文件，目前这些 section 已经是 4 字节对齐的了。那是不是就没问题呢？\r\n\r\n### 官方手册怎么说？\r\n\r\n查阅官方手册关于 section 对齐的描述：\r\n\r\n> output section 的运行地址会适应该 section 的对齐要求，而且不会低于包含的**任何一个 input section 的对齐要求**。\r\n\r\nsection 的对齐要求通过 `ALIGN` 指定。目前没有显式指定，所以链接器会把 input section 中**最严格的对齐**当作 output section 的对齐要求。\r\n\r\n使用 `readelf` 工具查看 input section，最后一列就是对齐值。可以看到，最严格的对齐要求就是 4，所以最终 output section 按 4 字节对齐。\r\n\r\n### 问题所在\r\n\r\n但如果后面某个目标文件的对齐属性发生变化——比如所有 input section 都是 1 字节对齐的——那这里的 output section 就无法保证 4 字节对齐了。\r\n\r\n**依赖\"恰好对齐\"是不可靠的。** 链接脚本应该描述\"你想要什么\"，而不是\"现在恰好是什么\"。\r\n\r\n### 解决方案：显式声明对齐\r\n\r\n最稳妥的做法是在每个关键 section 定义中，使用 `ALIGN` 显式声明对齐要求：\r\n\r\n```ld\r\n.text :\r\n{\r\n    . = ALIGN(4);\r\n    *(.text*)\r\n} > FLASH\r\n\r\n.data :\r\n{\r\n    . = ALIGN(4);\r\n    *(.data*)\r\n} > RAM AT > FLASH\r\n\r\n.bss :\r\n{\r\n    . = ALIGN(4);\r\n    *(.bss*)\r\n    *(COMMON)\r\n} > RAM\r\n```\r\n\r\n`ALIGN(4)` 会将 output section 的起始地址**向上对齐到 4 字节边界**。即使所有 input section 的对齐要求都小于 4，链接器也会强制 output section 起始地址对齐到 4 字节。\r\n\r\n这是**确定性的、可预期的行为**。\r\n\r\n### 加载地址的对齐\r\n\r\n加载地址默认和运行地址一致，所以只要运行地址对齐，加载地址就是对齐的。\r\n\r\n如果使用 `AT >` 指定内存区域，该加载地址同样会按照 section 的对齐要求进行调整。\r\n\r\n---\r\n\r\n## 八、与 CubeMX 默认脚本的对比\r\n\r\n现在我们的链接脚本已经从\"能跑\"升级到了\"严谨\"。所有 section 都显式安排，对齐行为有明确约束。\r\n\r\n最后对比一下 **CubeMX 默认生成的链接脚本**，看看有什么区别：\r\n\r\n### 1. 堆空间\r\n\r\nCubeMX 工程会分配堆空间：\r\n\r\n```ld\r\n._user_heap_stack :\r\n{\r\n    . = ALIGN(8);\r\n    PROVIDE ( end = . );\r\n    PROVIDE ( _end = . );\r\n    . = . + _Min_Heap_Size;\r\n    . = . + _Min_Stack_Size;\r\n    . = ALIGN(8);\r\n} >RAM\r\n```\r\n\r\n但 **MCU 裸机工程应该尽量避免使用 `malloc`**。动态内存在中断上下文、实时性要求高的场景下不可控，而且容易产生碎片。如果确实需要动态内存，建议用**对象池（memory pool）**或**静态分配**代替。所以我们的脚本中没有堆。\r\n\r\n### 2. `.preinit_array`、`.init_array`、`.fini_array`\r\n\r\n这三个是 C\/C++ **全局构造函数和析构函数**的函数指针表，由 `__libc_init_array()` 在 `main` 函数之前遍历调用（`.fini_array` 则通过 `atexit()` 注册、程序退出时反向调用）。\r\n\r\n- 如果项目需要 C++ 代码，就需要这些 section\r\n- 纯 C 项目如果使用了 `__attribute__((constructor))` 也需要\r\n\r\n我们的工程目前是纯 C 裸机，暂时不需要，但**保留这些可以防止以后引入 C++ 时链接失败**。\r\n\r\n### 3. 线程局部存储（TLS）\r\n\r\n`.tdata` 和 `.tbss` 是**线程局部存储**的 sections：\r\n\r\n- `.tdata` —— 存放每个线程独享的、带初始值的全局变量\r\n- `.tbss` —— 存放未初始化的版本\r\n\r\n裸机单线程环境下完全不需要。但如果使用了 crt 库，crt 内部引用这些 section 相关的符号，所以就需要添加这些 section 和符号。\r\n\r\n这里用 `PROVIDE` 定义相关符号：\r\n\r\n```ld\r\nPROVIDE(__tdata_start = .);\r\n```\r\n\r\n`PROVIDE` 是**弱符号定义**。比起直接定义的方式，`PROVIDE` 只有在源码没有定义该符号时，链接脚本才会定义它；如果源码中已有定义，则优先使用源码版本。所以这些符号也有可能在源码中定义。\r\n\r\n从这里也可以看出，CubeMX 默认工程的链接脚本是**兼容 crt 库**的。crt 库和 MCU 的启动流程有关，不清楚的话可以了解一下相关机制。\r\n\r\n---\r\n\r\n## 九、总结\r\n\r\n链接脚本该涉及的内容基本就这些了。当然还有很多写法是为了兼容旧规范、旧架构，具体情况根据需要再去调整。\r\n\r\n工程实践上最重要的一条：**打开\"孤儿 section\"警告**，才能尽早发现问题。\r\n\r\n把链接脚本写严谨，本质上就是**把一些莫名其妙的问题提前消灭掉**。搞清楚、写明白，后面换芯片、换工具链、加功能，心里都有底。\r\n\r\n---\r\n\r\n## 附录：关键链接器标志参考\r\n\r\n| 标志 | 作用 |\r\n|------|------|\r\n| `--warn-orphan` | 对未显式安排的 section 发出警告 |\r\n| `--orphan-handling=error` | 将孤儿 section 视为错误，直接终止链接 |\r\n| `-Map=output.map` | 生成 map 文件，查看 section 布局 |\r\n\r\n## 附录：常用 readelf 命令\r\n\r\n```bash\r\n# 查看所有 section 的详细信息（含 flags 和对齐）\r\nreadelf -S output.elf\r\n\r\n# 查看 section 到段的映射\r\nreadelf -l output.elf\r\n\r\n# 查看符号表\r\nreadelf -s output.elf\r\n```","updatetime":"2026-10-08 00:01:55","author":"大目熊"}}