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.solibLIBNAME.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.solibLIBNAME.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++ 编译中很重要,这涉及到符号解析的算法。链接器处理符号解析时是从左到右的顺序扫描:

  1. 从左到右扫描:链接器按顺序处理每个文件/库
  2. 只解析当前未定义的符号:遇到库时,只提取能解决当前未定义符号的部分
  3. 不回溯:一旦扫描过某个库,就不会再回头检查

编译链接时有时碰到undefined reference的错误,这往往是链接顺序的影响。

举例:

$ riscv64-unknown-linux-gnu-gcc -lm main.c -o test

其可能导致编译报错,因为库在源码文件之前,其扫描过程可能如下:

  1. 扫描 -lm:没有未定义符号,跳过大部分内容
  2. 扫描 main.c:发现需要 sinfcosf 等函数
  3. 链接器已经扫描过库,不会再回头 → 符号未定义错误

需要改为如下形式:

$ 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 的作用,简单来说就是 强制链接器对这两个选项之间指定的库,进行多次、循环的扫描,直到所有未解决的符号都被解析,或者不再有新的符号能被解析为止。。这直接打破了链接器不回溯、只扫描一次的规则。

其工作原理如下:

  1. 启用循环扫描:当链接器看到 --start-group 时,它会记住这个位置,并对后续列出的库建立了一个“组”。
  2. 重复解析:链接器会反复扫描这个组内的所有库文件(从左到右),每当有新的未定义符号被发现,它就会再次扫描整个组。
  3. 直到稳定结束:这个循环会一直进行,直到在一次完整的组扫描中,没有产生任何新的未定义符号的解析为止。

总的来说,链接顺序有如下规则:

  1. 源文件在前:.c.o文件放在最前面【必需】
  2. 库按依赖顺序,被依赖的库放在后面,系统库如 -lm-lpthread等放在最后
  3. 如果库之间存在循环依赖,需要将库放到-Wl,--start-group -Wl,--end-group 之中包裹

记住如下一句话:

在大型 C/C++ 项目中,调整链接库的顺序往往是解决 undefined reference(找不到符号)或隐藏 Bug 的终极手段。

2.4 链接过程中的符号处理

编译链接时有时碰到multiple definition of 'xxx'的错误,这可能是不同原因引起的,有两套规则:

场景一:多个独立的 .o 文件直接链接(全局符号解析)

在这个场景下,使用的是"强弱符号规则”:

链接器会把所有参与链接的 .o 文件的符号表汇总在一起,进行全局比对:

  1. 强符号冲突:如果你把 a.ob.o 直接传给链接器,且它们都定义了同名的强符号,链接器会立刻报错:multiple definition of 'x'
  2. 强弱共存:如果 a.o 中是强符号,b.o 中是弱符号(未初始化),链接器会无视弱符号,采用强符号。
  3. 全弱共存:如果多个文件都是弱符号,链接器会从中选择一个(通常是占用内存空间最大的那个)。

场景二:从静态库(.a)中提取 .o 文件(按需加载)

在这个场景下,使用的是“首次出现原则”:

静态库是一个 .o 文件的压缩包。链接器在处理静态库时,采用的是“单遍、从左到右扫描”的机制。

  1. 链接器手里拿着一份“未解析符号清单(U)”。
  2. 它按顺序查看库里的 .o 文件。当遇到第一个能解决清单中某个符号的 .o 文件时,它立刻把这个文件提取出来。
  3. 一旦这个符号被解决,它就从“未解析清单”中划掉了。当链接器继续往后扫描,遇到后面的 .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
posted @ 2026-08-29 16:21  sureZ_ok  阅读(13)  评论(0)    收藏  举报