RISC-V Binutils工具链04之ld工具的使用
RISC-V Binutils工具链04之ld工具的使用
ld 是链接器,目的是将一个或者多个目标文件(.o 文件)链接在一起,得到最终可执行文件。
1 ld 工具常用选项
$ riscv64-unknown-linux-gnu-ld --help
输出log见:riscv64-unknown-linux-gnu-ld_help
1.1 基础与输出控制
| 选项 | 长选项 | 说明 |
|---|---|---|
| -o FILE | --output FILE | 指定最终输出可执行文件名。这是最常用的选项之一 |
| -v | --version | 打印链接器版本信息 |
| -V | 打印版本信息和支持的仿真模式(emulations),对交叉编译很有用 | |
| --oformat TARGET | 指定输出文件的目标格式(如 elf64-littleriscv)。通常由 -m 指定的仿真模式决定 |
|
| @FILE | 从文件 FILE 中读取命令行选项,用于处理超长的链接命令 |
1.2 架构与仿真
| 选项 | 长选项 | 说明 |
|---|---|---|
| -EB | 链接大端(Big-Endian)目标文件 | |
| -EL | 链接小端(Little-Endian)目标文件,RISC-V 默认是小端 | |
| -A ARCH | --architecture ARCH | 设置目标架构(较少用,通常由 -m 决定) |
1.3 符号与入口点
| 选项 | 长选项 | 说明 |
|---|---|---|
| -e ADDRESS | --entry ADDRESS | 设置程序的入口地址(Entry Point)。例如 -e _start |
| -u SYMBOL | --undefined SYMBOL | 将符号 SYMBOL 标记为未定义,强制链接器去库中寻找它。常用于确保某些库被链接 |
| --require-defined SYMBOL | 要求 SYMBOL 必须在最终输出中被定义,否则链接失败 |
|
| --defsym SYMBOL=EXPRESSION | 在链接时定义一个符号,其值为 EXPRESSION。如 --defsym stack_start=0x80000000 |
|
| -y SYMBOL | --trace-symbol SYMBOL | 跟踪符号 SYMBOL 的引用,链接器会报告它在哪个文件中被引用。调试符号问题的利器 |
| --unresolved-symbols=<method> | 控制如何处理未解析的符号。<method> 可以是:ignore-all, report-all, ignore-in-object-files, ignore-in-shared-libs |
1.4 库与搜索路径
| 选项 | 长选项 | 说明 |
|---|---|---|
| -L DIRECTORY | --library-path DIRECTORY | 将 DIRECTORY 添加到库搜索路径中 |
| -l LIBNAME | --library LIBNAME | 搜索并链接名为 libLIBNAME.so 或 libLIBNAME.a 的库 |
| -Bstatic | -static | 后续的 -l 选项优先链接静态库(.a) |
| -Bdynamic | 后续的 -l 选项优先链接共享库(.so) |
|
| --sysroot=<DIRECTORY> | 覆盖默认的 sysroot 路径,在交叉编译中非常关键 | |
| -nostdlib | 不使用标准系统库和启动文件。通常用于裸机(baremetal)程序 |
1.5 构建共享库与位置无关代码
| 选项 | 长选项 | 说明 |
|---|---|---|
| -shared | 创建一个共享库(.so 文件) |
|
| -pie | --pic-executable | 创建位置无关的可执行文件(Position Independent Executable),提高安全性 |
| -no-pie | 创建位置相关的可执行文件(默认) | |
| -G SIZE | --gpsize SIZE | 设置小数据区(GP-relative data)的大小。RISC-V 使用 GP 寄存器快速访问小数据 |
| -Bsymbolic | 在共享库中,将全局符号的引用绑定到本地定义,而不是运行时解析。可以提高性能,但可能改变行为 | |
| -Bsymbolic-functions | 类似 -Bsymbolic,但仅对函数符号生效。更安全 |
1.6 内存布局与段控制
| 选项 | 长选项 | 说明 |
|---|---|---|
| -T FILE | --script FILE | 指定链接器脚本(Linker Script),这是控制内存布局的核心 |
| --default-script FILE | -dT | 指定默认的链接器脚本 |
| --section-start SECTION=ADDRESS | 设置指定段(如 .text, .data)的起始地址 |
|
| -Ttext ADDRESS | 等价于 --section-start .text=ADDRESS |
|
| -Tdata ADDRESS | 等价于 --section-start .data=ADDRESS |
|
| -Tbss ADDRESS | 等价于 --section-start .bss=ADDRESS |
|
| --sort-section name|alignment | 按名称/对齐对输出段进行排序 |
1.7 优化与调试
| 选项 | 说明 |
|---|---|
| --gc-sections | 垃圾回收未使用的段(如未调用的函数、未使用的数据段)。必须配合 -ffunction-sections -fdata-sections 编译选项使用 |
| --print-gc-sections | 在 stderr 中列出被 --gc-sections 删除的段 |
| -O | 优化输出文件(如重排段以减少填充) |
| -M, --print-map | 打印链接器映射文件(Map File)到标准输出,显示符号和段的地址 |
| -Map FILE | 将映射文件输出到 FILE,得到map文件 |
| --cref | 输出交叉引用表(Cross Reference Table),显示符号的定义和引用 |
| --verbose | 输出链接过程中的大量信息 |
| --trace | 跟踪链接器打开的文件 |
| --stats | 打印内存使用统计信息 |
| --relax | 使用链接松弛来减小代码体积 |
| --no-relax | 关闭链接松弛,这在调试时序敏感代码或遇到断点问题时非常有用 |
1.8 高级/特殊用途
| 选项 | 长选项 | 说明 |
|---|---|---|
| --whole-archive | 强制链接静态库中的所有目标文件,即使它们未被引用。 之后需用 --no-whole-archive 关闭。用于链接包含构造函数的库 |
|
-( ... -) |
--start- group --end-group |
将库放在 -( ... -) 中,链接器会多次扫描这些库以解决循环依赖 |
| --allow-multiple-definition | 允许同一个符号有多个定义。链接器会使用第一个找到的定义 | |
| -r, --relocatable | 生成可重定位输出(类似于 ar 合并 .o 文件) |
|
| -s, --strip-all | 剥离输出文件的所有符号,减小输出文件大小 | |
| -S, --strip-debug | 剥离输出文件的调试符号 |
2 ld 常用选项举例
2.1 ld典型使用场景
| 选项 | 说明 |
|---|---|
| -T | 指定链接器脚本(Linker Script),这是控制内存布局的核心 |
| -Map | 将映射文件输出到 FILE,得到map文件 |
| -o | 指定最终输出可执行文件名。这是最常用的选项之一 |
| -e | 设置程序的入口地址(Entry Point)。例如 -e _start |
| -l | 搜索并链接名为 libLIBNAME.so 或 libLIBNAME.a 的库 |
| -L | 将 DIRECTORY 添加到库搜索路径中 |
| -S | 剥离输出文件的调试符号 |
| -s | 剥离输出文件的所有符号,减小输出文件大小 |
| -t | 在处理输入文件时显示它们的名称 |
| -Ttext | 等价于 --section-start .text=ADDRESS |
对于RISC-V 裸机开发,你最可能用到的组合是:
riscv64-unknown-linux-gnu-ld \
-m elf64lriscv \ # 指定仿真
-T linker_script.ld \ # 指定链接脚本
-o output.elf \ # 输出文件
--gc-sections \ # 删除未使用代码
--print-map \ # 输出map映射文件
--defsym stack_start=0x80000000 \ # 定义堆栈起始地址
startup.o main.o lib.a
链接器脚本(-T)是控制内存布局的灵魂,而 -Map 和 --gc-sections 是优化和调试的必备工具。
2.2 创建和链接分离
RISC-V 链接器严格区分创建和链接两个阶段(阶段分离的设计理念),下面是静态库/动态库创建以及链接的区别:
创建阶段:
# 创建静态链接库(c 静默创建,s 生成索引)
$ riscv64-unknown-linux-gnu-ar rcs libmath.a sin.o cos.o tan.o
# 创建动态链接库 (选项:-shared)
$ riscv64-unknown-linux-gnu-ld -shared -o libmath.so sin.o cos.o tan.o
链接阶段:
# 链接静态库
$ riscv64-unknown-linux-gnu-ld -static -o calculator main.o -lmath
# 链接动态库
$ riscv64-unknown-linux-gnu-ld -Bdynamic -o calculator main.o -lmath
2.3 链接顺序的影响
链接顺序在 C/C++ 编译中很重要,这涉及到符号解析的算法。链接器处理符号解析时是从左到右的顺序扫描:
- 从左到右扫描:链接器按顺序处理每个文件/库
- 只解析当前未定义的符号:遇到库时,只提取能解决当前未定义符号的部分
- 不回溯:一旦扫描过某个库,就不会再回头检查
编译链接时有时碰到undefined reference的错误,这往往是链接顺序的影响。
举例:
$ riscv64-unknown-linux-gnu-gcc -lm main.c -o test
其可能导致编译报错,因为库在源码文件之前,其扫描过程可能如下:
- 扫描
-lm:没有未定义符号,跳过大部分内容 - 扫描
main.c:发现需要sinf,cosf等函数 - 链接器已经扫描过库,不会再回头 → 符号未定义错误
需要改为如下形式:
$ riscv64-unknown-linux-gnu-gcc main.c -lm -o test
不过还有可能存在更复杂的依赖关系,比如有多个库之间互相依赖,单纯的从前到后排列可能不满足要求。这个场景需要-Wl,--start-group -Wl,--end-group 链接选项
# --start-group 和 --end-group 可以让链接器重复扫描
$ riscv64-unknown-linux-gnu-gcc main.c -Wl,--start-group -lm -lxxx -Wl,--end-group -o test
-Wl,--start-group -Wl,--end-group 链接选项的作用:
-Wl,--start-group 和 -Wl,--end-group 的作用,简单来说就是 强制链接器对这两个选项之间指定的库,进行多次、循环的扫描,直到所有未解决的符号都被解析,或者不再有新的符号能被解析为止。。这直接打破了链接器不回溯、只扫描一次的规则。
其工作原理如下:
- 启用循环扫描:当链接器看到
--start-group时,它会记住这个位置,并对后续列出的库建立了一个“组”。 - 重复解析:链接器会反复扫描这个组内的所有库文件(从左到右),每当有新的未定义符号被发现,它就会再次扫描整个组。
- 直到稳定结束:这个循环会一直进行,直到在一次完整的组扫描中,没有产生任何新的未定义符号的解析为止。
总的来说,链接顺序有如下规则:
- 源文件在前:
.c、.o文件放在最前面【必需】 - 库按依赖顺序,被依赖的库放在后面,系统库如
-lm、-lpthread等放在最后 - 如果库之间存在循环依赖,需要将库放到
-Wl,--start-group -Wl,--end-group之中包裹
记住如下一句话:
在大型 C/C++ 项目中,调整链接库的顺序往往是解决
undefined reference(找不到符号)或隐藏 Bug 的终极手段。
2.4 链接过程中的符号处理
编译链接时有时碰到multiple definition of 'xxx'的错误,这可能是不同原因引起的,有两套规则:
场景一:多个独立的 .o 文件直接链接(全局符号解析)
在这个场景下,使用的是"强弱符号规则”:
链接器会把所有参与链接的 .o 文件的符号表汇总在一起,进行全局比对:
- 强符号冲突:如果你把
a.o和b.o直接传给链接器,且它们都定义了同名的强符号,链接器会立刻报错:multiple definition of 'x'。 - 强弱共存:如果
a.o中是强符号,b.o中是弱符号(未初始化),链接器会无视弱符号,采用强符号。 - 全弱共存:如果多个文件都是弱符号,链接器会从中选择一个(通常是占用内存空间最大的那个)。
场景二:从静态库(.a)中提取 .o 文件(按需加载)
在这个场景下,使用的是“首次出现原则”:
静态库是一个 .o 文件的压缩包。链接器在处理静态库时,采用的是“单遍、从左到右扫描”的机制。
- 链接器手里拿着一份“未解析符号清单(U)”。
- 它按顺序查看库里的
.o文件。当遇到第一个能解决清单中某个符号的.o文件时,它立刻把这个文件提取出来。 - 一旦这个符号被解决,它就从“未解析清单”中划掉了。当链接器继续往后扫描,遇到后面的
.o文件里也有这个同名符号时,链接器根本不再关心它是强是弱,因为它已经不需要这个符号了,所以直接跳过。
注意:如果链接时使用了--whole-archive 链接选项,意思是强制把静态库里所有的 .o 全部打包进最终文件,无视“未解析符号清单”,不再满足按需加载的规则了。
场景三:两者结合的场景
在实际工程中,我们往往是把 .o 文件和静态库(.a)混在一起链接的。这时候,两套规则是协同工作的。所以要特别注意链接顺序,不要将 .o 文件和静态库(.a)打乱混在一起
再总结一下链接过程中符号解析的两套规则:
| 规则 | 适用对象 | 核心目的 | 行为表现 |
|---|---|---|---|
| 强弱符号规则 | 所有参与链接的、已确定的 .o 文件 |
解决符号冲突,防止逻辑混乱 | 强>弱;全弱选大;多强报错 |
| 首次出现原则 | 静态库(.a)内部的 .o 文件 |
实现静态库的“按需提取”,避免把整个库塞进可执行文件 | 找到第一个就提取,后面的同名符号直接无视 |
2.5 生成map文件( -Wl,-Map=output.map)
链接时加上-Wl,-Map即可生成map文件,加上这个选项后,链接器将所有 .o 文件和库文件 “拼” 成一个可执行文件的过程时会生成一个 .map 文件
# 链接时使用 -Wl,-Map 选项
$ riscv64-unknown-linux-gnu-gcc -o helloworld.elf helloworld.c -Wl,-Map=helloworld.map

浙公网安备 33010602011771号