GNU LD
时间轴
2025-09-27
init
- 链接器是一个程序,将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件
- GNU Linker 采用 AT&T 链接脚本语言
ld 命令
- aarch64-linux-gnu-ld
- 常用参数
- -T 指定链接脚本
- -Map 输出一个符号表文件
- -o 输出最终可执行二进制文件
一个简单的例子
1 | SECTIONS |
基本概念
- 输入段(input section),输出段(output section)
- 每个段包括 name 和大小
- 段的属性
- loadable 运行时会加载这些段的内容到内存
- allocatable 运行时不会加载段的内容
- 段的地址
- VMA(virtual memory address) 虚拟地址,运行时的地址
- LMA(load memory address) 加载地址
- 通常 ROM 的地址为加载地址,而 RAM 的地址为 VMA
链接脚本命令
- ENTRY(symbol) 设置程序的入口函数
链接程序有如下几种方式来设置入口点:
- 使用-e 参数
- 使用 ENTRY(symbol)
- 在.text 的最开始的地方
- 0 地址
INCLUDE filename 引入 filename 链接脚本
- OUTPUT filename 输出二进制文件,类似在命令行里使用“-o filename”
- OUTPUT_FORMAT(bfd) 输出 BFD 格式
- OUTPUT_ARCH(bfdarch) 输出处理器体系结构格式
符号赋值
- 符号也可以像 C 语言一样赋值
- “.” 表示 location counter,表示当前位置
符号的引用
- 高级语言中常常需要引用链接脚本定义的符号
在 C 语言里,定义一个变量并初始化一个变量。例如 int foo = 100
- 编译器会在符号表定义了一个符号 foo
- 编译器会在内存中为符号存储 100
在链接脚本中定义一个变量
- 链接器仅仅在符号表里定义这个符号,没有分配内存来存储变量的值
- 访问链接脚本定义的变量:访问的时变量的地址,不能访问变量的值
- 我们可以在每个段设置一些符号,以方便 C 语言访问每个段的起始地址和结束地址
SECTIONS 命令
- SECTIONS 命令:告诉链接器如何把输入段(input sections)映射到输出段(output sections),以及如何在内存中摆放这些输出段
- 输出 section 的描述符
LMA 加载地址
- 每个段都有VMA(虚拟地址,运行地址)以及LMA(加载地址)
- 在输出段描述符中使用”AT”来指定 LMA
- 如果没有通过”AT”来指定 LMA,通常 LMA=VMA
- 构建一个基于 ROM 的映像文件常常会设置输出段的虚拟地址和加载地址不一致
- data 段的加载地址和链接地址(虚拟地址)不一样,因此程序的初始化需要把 data 段从 ROM 的加载地址复制到 SDRAM 中的虚拟地址中
- 数据加载地址在_etext 起始的地方,数据段的运行地址是在_data 起始的地方,数据段的大小为“_edata-_data”,下面这段代码把数据段从_etext 起始的地方复制到_data 起始的地方
常见的内建函数
ADDR(section)
返回前面已经定义过的段的 VMA 地址
ALIGN(n)
返回下一个与 n 字节对齐的地址,它是基于当前的位置(location counter)来计算对齐地址的
注意这里是 n 个字节,而不是 2^n 个字节(与汇编器的.align 区分)
SIZEOF(section)
返回一个段的大小
MAX(exp1, exp2) / MIN(exp1, exp2)
返回两个表达式的最大值或最小值
实验 1:打印每个段的内存布局
- 链接器导出的符号是地址,不是变量值
链接脚本中的这些符号:
1 | _text = .; |
定义的不是变量本身,而是一个 地址标签(symbol address)。在 C 中没有对应“地址标签”的语法,因此只能通过某种“变量”来间接引用这个地址。
用 char[] 声明它,其实是在说:
“这是一段起始于 _text 的内存区域,我关心的是它的地址,而不是它的具体内容。”
char[] 是一种“单位最小”的内存表示,方便做指针运算
char 是 C 中最小的可寻址单位(1 字节)。
所以用 char[] 类型,我们就可以直接进行精确的地址操作:
1 | extern char _text[], _etext[]; |
如果你写成 int[] 或 void*,这个计算就可能出错,或者无法编译。
- char[] vs char* 的差异:链接器符号是“数组地址”而非指针变量
虽然你也可以写成:
1 | extern char *_text; |
但这其实意味着 _text 是一个“指向字符的变量”,而不是一个地址标签。
char *_text; 说明编译器要去“取变量 _text 的值”,它必须由代码赋值。
而 char _text[]; 是“声明链接器会提供这个地址”,不会生成额外符号或变量。
所以推荐使用:
1 | extern char _text[]; |
实验 2:加载地址不等于运行地址
需要把代码从装载地址拷贝到运行地址
实验 3:分析 Linux5.0 内核的链接脚本
运行地址,装载地址,链接地址
链接地址(Link Address)
定义:
编译器和链接器在生成可执行文件(如 ELF 文件)时,为各段(如 .text, .data, .bss 等)分配的地址。
特征:
- 是编译阶段由 链接器设定的地址。
- 可以通过链接脚本 (ld script) 显式设置,比如 . = 0x80000;。
- 可执行文件中的节表(section headers)或段表(program headers)中就记录了这些地址。
例子:
1 | .text : { *(.text) } > 0x80000 |
表示 .text 段的 链接地址是 0x80000。
装载地址(Load Address)
定义:
可执行文件中的内容被加载到内存中的位置,也就是 操作系统/bootloader 将文件装入内存的位置。
特征:
- 通常等于链接地址,但在某些情况下(如动态链接、加载地址重定位)可以不同。
- 由 操作系统或 bootloader 决定,也可以通过 objcopy 等工具进行重新定位。
例子:
- 你的 ELF 文件 .text 段链接地址是 0x80000,bootloader 将它加载到 0x100000,那么:
- 链接地址 ≠ 装载地址
- 如果没有做重定位,程序运行会出错(因为代码中有绝对地址)
运行地址(Runtime/Execution Address)
定义:
程序在执行时,CPU 实际访问的内存地址。
特征:
通常 = 装载地址(程序加载到哪里就从哪里执行)
如果启用了 MMU(内存管理单元),运行地址是虚拟地址,由 MMU 映射到物理装载地址。
在裸机程序中,一般 = 链接地址 = 装载地址 = 运行地址。