从源代码到装载映像
跳转到“从源代码到装载映像”编译、链接和装载是三个不同阶段。以 C 和 ELF 工具链为例:
源文件 .c └─ 编译/汇编 → 可重定位目标文件 .o └─ 链接 → ELF 可执行映像或共享对象 ├─ 调试器/装载器读取 └─ 按需导出 .bin 或 Intel HEXELF 可以包含机器码、数据、重定位、符号和调试信息,但这些信息不一定全部存在。裸二进制 .bin 通常只有导出的字节,不保留 ELF 的节表和完整符号表;Intel HEX 以记录形式携带数据与地址,也不是“另一种完整 ELF”。
“程序”还可以指源代码或正在运行的逻辑,不能把这个词严格定义成某一种输出文件。
Section 与 Segment
跳转到“Section 与 Segment”中文资料有时都把它们译成“段”,容易混淆。这里分别写成节(section)和装载段(segment):
| 视角 | 主要服务对象 | 典型查看方式 |
|---|---|---|
| Section:节 | 链接器、调试器等工具 | readelf -S、objdump -h |
| Segment:装载段 | 装载器,根据程序头描述映射文件到内存 | readelf -l |
一个装载段可以覆盖多个节。例如本页实验中,第一个 LOAD 段同时包含 .text 与 .rodata,第二个包含 .data 与 .bss。节名相同也不意味着所有平台都会放在相同物理介质或赋予完全相同的保护属性。ELF 节规范
常见节与初始化
跳转到“常见节与初始化”| 节 | 常见内容 | 要点 |
|---|---|---|
.text | 机器指令 | 实际地址和执行权限由目标平台及链接/装载方案决定 |
.rodata | 只读数据,如保留下来的常量对象 | const 不是“必定产生此节”的指令;对象可能被优化掉 |
.data | 需要保存初值的可写静态数据 | 裸机中常把初值存于 Flash,运行时复制到 RAM |
.bss | 通常为零初始化的静态数据 | 可有非零内存大小,但不保存同样大小的零字节文件内容 |
.symtab | 完整符号表 | 可能在发布时剥离,不等于实际运行对象的清单 |
.debug_* | 调试信息 | 与程序的装载内容不同,可独立保留 |
显式写 int value = 0; 仍然经常进入 .bss。区分 .data 与 .bss 时,不能只看有没有写等号。文件作用域的 int value; 在 C 中还涉及暂定定义和编译器的 common 选项;本页实验固定 -fno-common 以消除这一变量。
.bss 一般使用 ELF 的 SHT_NOBITS 类型:它保留大小等描述,但对应内容不占文件字节。这不表示 RAM 不用初始化。由操作系统装载器还是裸机启动代码完成清零,取决于环境。
.stack 和 .heap 不是所有 ELF 都必须存在的固定节名。调用栈、分配器管理区域和链接脚本中的预留空间是相关但不同的概念;参见调用栈、动态内存。
符号是名字与属性的记录
跳转到“符号是名字与属性的记录”符号表条目可包含名字、值、大小、类型、绑定方式和关联节。符号的值不总是最终运行地址:目标文件中的节内偏移、未定义引用、绝对数值和线程局部符号都有各自含义。并非每个源代码局部变量都会成为链接器可见的符号。
GNU nm 的常见标记如下;具体含义还与文件格式有关。
| 标记 | 常见含义 |
|---|---|
T/t | 代码节中的符号 |
D/d | 已初始化数据节中的符号 |
B/b | BSS 等零初始化数据中的符号 |
R/r | 只读数据中的符号 |
A | 绝对值符号,值不因进一步链接而重定位 |
U | 未定义引用,需要其他对象或运行时提供定义 |
W/w | 弱符号,未特别标记为弱对象 |
V/v | 弱对象符号 |
? | 工具无法分类的符号类型 |
对 T/t 等常见类型,大写通常表示外部符号,小写通常表示局部符号。不能把这个规则套给所有字母,特别是 w、v 等弱符号标记。强定义可以替代同名弱定义;未解析弱引用可能被允许,但这不代表访问或调用它一定安全。GNU nm 文档
一个可复核的 Cortex-M3 ELF 实验
跳转到“一个可复核的 Cortex-M3 ELF 实验”这个实验用于观察编译和链接产物;内存地址是示例布局。完整固件还需要对应芯片的向量表、复位入口与启动初始化,不能直接把实验文件当作可启动固件。
1. 源文件
跳转到“1. 源文件”保存为 sections.c:
#include <stdint.h>
uint32_t initialized = 7;uint32_t explicit_zero = 0;uint32_t implicit_zero;const uint32_t delay_ticks = 1000;
void Default_Handler(void){ for (;;) { }}
void ADC_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));void BDMA_Channel0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void entry(void){ initialized += delay_ticks; explicit_zero = initialized;}两条中断名仅用于展示 GCC 的弱别名机制,并不是这个 Cortex-M3 示例已配置了相应外设。实际启动文件的处理函数名字由芯片和设备支持包确定。
2. 链接脚本
跳转到“2. 链接脚本”保存为 layout.ld:
ENTRY(entry)MEMORY{ FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K}SECTIONS{ .text : { *(.text .text.*) } > FLASH .rodata : { *(.rodata .rodata.*) } > FLASH .data : { . = ALIGN(4); _sdata = .; *(.data .data.*) _edata = .; } > RAM AT > FLASH _sidata = LOADADDR(.data); .bss (NOLOAD) : { . = ALIGN(4); _sbss = .; *(.bss .bss.*) *(COMMON) _ebss = .; } > RAM}VMA 是该节运行时使用的地址;LMA 是其初值装载布局中的地址。> RAM AT > FLASH 使 .data 的运行地址位于 RAM、初值位于 Flash。LOADADDR(.data) 取加载地址;节内的 . 跟踪当前位置的 VMA。GNU ld:LMA
链接器只生成布局和符号。裸机启动代码仍需要用这些边界复制 .data、清零 .bss。将链接符号声明为 extern unsigned char _sdata[]; 一类数组边界,是在声明地址入口,不是在 C 中新分配同名存储对象;具体写法要符合启动代码与工具链约定。
3. 编译、链接和查看
跳转到“3. 编译、链接和查看”arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -std=c11 -O0 -Wall -Wextra -Werror -ffreestanding -fno-common -fno-unwind-tables -fno-asynchronous-unwind-tables -c sections.c -o sections.oarm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -nostdlib -Wl,-T,layout.ld sections.o -o sections.elfarm-none-eabi-objdump -h sections.elfarm-none-eabi-readelf -l sections.elfarm-none-eabi-nm -n sections.elf在 PowerShell 中,可把链接参数写为 '-Wl,-T,layout.ld',避免逗号被命令解析器误处理。
本页使用 Arm GNU GCC 13.3.1 实际生成的结果为:
| 节 | 大小(字节) | VMA | LMA |
|---|---|---|---|
.text | 52 | 0x08000000 | 0x08000000 |
.rodata | 4 | 0x08000034 | 0x08000034 |
.data | 4 | 0x20000000 | 0x08000038 |
.bss | 8 | 0x20000004 | 无需保存对应的初值字节 |
符号输出节选:
08000000 W ADC_IRQHandler08000000 W BDMA_Channel0_IRQHandler08000000 T Default_Handler08000008 T entry08000034 R delay_ticks08000038 A _sidata20000000 D initialized20000004 B explicit_zero20000008 B implicit_zero由此可以分别验证:
- 显式初始化为零与未写初值的两个对象都位于
.bss。 .data的 VMA 与 LMA 不同,运行地址与初值位置不能混用。- 两个弱别名与默认处理函数具有同一个地址,因为源码明确使用了
alias,而不是“所有弱符号天然共用一个地址”。 readelf -l的第二个LOAD段文件大小为 4 字节、内存大小为 12 字节,差额对应本实验的 BSS 空间。
地址会随源代码、编译器和选项改变。表中数字是这次实验的实际输出,不是芯片统一规定的函数地址。
自定义节还需要什么
跳转到“自定义节还需要什么”GCC 的 section 属性可把内容送入指定输入节:
#include <stdint.h>
__attribute__((section(".mycode")))void special_func(void){}
__attribute__((section(".mydata")))uint32_t special_var = 123;链接脚本还需接收这些输入节,并把它们放入有效的 MEMORY 区域。例如希望代码在 RAM 执行而初值保存在 Flash,需要合适的输出节 > RAM AT > FLASH,以及执行前的复制过程;仅有属性不会自动完成搬运。使用 ITCM/DTCM 时,还必须核对芯片是否有相应存储、可执行性、地址和启动配置。
原稿把 C 定义与链接脚本混在同一个代码块中;两者需要分别保存。若开启链接时的无用节回收,保留特殊表或启动入口还可能需要 KEEP() 等规则,不能只依赖自定义节名。
本页主实验已完成真实交叉编译、链接以及节表/程序头/符号表核验;自定义节片段另完成编译并确认 .mycode/.mydata 输入节存在。没有进行硬件烧录或启动验证。