跳转到内容
新建笔记

ELF 节、装载段与符号:编译链接实证

编译、链接和装载是三个不同阶段。以 C 和 ELF 工具链为例:

源文件 .c
└─ 编译/汇编 → 可重定位目标文件 .o
└─ 链接 → ELF 可执行映像或共享对象
├─ 调试器/装载器读取
└─ 按需导出 .bin 或 Intel HEX

ELF 可以包含机器码、数据、重定位、符号和调试信息,但这些信息不一定全部存在。裸二进制 .bin 通常只有导出的字节,不保留 ELF 的节表和完整符号表;Intel HEX 以记录形式携带数据与地址,也不是“另一种完整 ELF”。

“程序”还可以指源代码或正在运行的逻辑,不能把这个词严格定义成某一种输出文件。

中文资料有时都把它们译成“段”,容易混淆。这里分别写成节(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/bBSS 等零初始化数据中的符号
R/r只读数据中的符号
A绝对值符号,值不因进一步链接而重定位
U未定义引用,需要其他对象或运行时提供定义
W/w弱符号,未特别标记为弱对象
V/v弱对象符号
?工具无法分类的符号类型

对 T/t 等常见类型,大写通常表示外部符号,小写通常表示局部符号。不能把这个规则套给所有字母,特别是 w、v 等弱符号标记。强定义可以替代同名弱定义;未解析弱引用可能被允许,但这不代表访问或调用它一定安全。GNU nm 文档

一个可复核的 Cortex-M3 ELF 实验

跳转到“一个可复核的 Cortex-M3 ELF 实验”

这个实验用于观察编译和链接产物;内存地址是示例布局。完整固件还需要对应芯片的向量表、复位入口与启动初始化,不能直接把实验文件当作可启动固件。

保存为 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 示例已配置了相应外设。实际启动文件的处理函数名字由芯片和设备支持包确定。

保存为 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 中新分配同名存储对象;具体写法要符合启动代码与工具链约定。

终端窗口
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.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -nostdlib -Wl,-T,layout.ld sections.o -o sections.elf
arm-none-eabi-objdump -h sections.elf
arm-none-eabi-readelf -l sections.elf
arm-none-eabi-nm -n sections.elf

在 PowerShell 中,可把链接参数写为 '-Wl,-T,layout.ld',避免逗号被命令解析器误处理。

本页使用 Arm GNU GCC 13.3.1 实际生成的结果为:

节大小(字节)VMALMA
.text520x080000000x08000000
.rodata40x080000340x08000034
.data40x200000000x08000038
.bss80x20000004无需保存对应的初值字节

符号输出节选:

08000000 W ADC_IRQHandler
08000000 W BDMA_Channel0_IRQHandler
08000000 T Default_Handler
08000008 T entry
08000034 R delay_ticks
08000038 A _sidata
20000000 D initialized
20000004 B explicit_zero
20000008 B implicit_zero

由此可以分别验证:

  1. 显式初始化为零与未写初值的两个对象都位于 .bss。
  2. .data 的 VMA 与 LMA 不同,运行地址与初值位置不能混用。
  3. 两个弱别名与默认处理函数具有同一个地址,因为源码明确使用了 alias,而不是“所有弱符号天然共用一个地址”。
  4. 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 输入节存在。没有进行硬件烧录或启动验证。