链接
在 Linux 内核及其衍生项目(如 Jailhouse、Xen、U-Boot)中,链接(link)阶段并不是通过显式的 ld 命令完成的,而是由 Kbuild 构建系统通过一套高度抽象、模板化的机制来统一管理。
本文将围绕两个核心问题展开:
- Kbuild 中的
ld是在哪里、如何被调用的? - 在 Kbuild 体系下使用链接器脚本(
.lds)时,有哪些容易踩坑的地方?
一、Kbuild 中并不存在"直接调用 ld 的函数"
在阅读某些 Makefile 时,我们可能会困惑于如下规则:
make
$(obj)/hypervisor.o: hypervisor.lds $(objs)
$(call if_changed,ld)
问题是:
这里并没有看到
ld -T hypervisor.lds ...,那链接器到底是怎么被调用的?
答案是:
- Kbuild 中并不存在一个叫"ld"的函数
ld是一个 命令模板(command template)名 ,由if_changed间接展开
二、if_changed,ld 的真正入口在哪里?
if_changed 宏的定义
if_changed 系列宏定义在内核源码中,这里不深入:
scripts/Kbuild.include
其核心职责是:
- 判断命令行或依赖是否发生变化
- 只有在变化时才执行对应命令
ld 对应的命令模板在哪里?
真正定义 如何调用链接器 的地方在:
scripts/Makefile.lib
(不同内核版本略有差异,但结构一致)
概念化后的定义大致如下:
make
quiet_cmd_ld = LD $@
cmd_ld = $(LD) $(LDFLAGS_$(@F)) $(LDFLAGS) \
-o $@ $(real-prereqs)
关键点在这里:
cmd_ld才是真正的 ld 命令if_changed,ld实际上就是执行cmd_ld- 所有参数都来自 Make 变量拼接,而非写死
三、为什么使用 $(@F) 而不是 $@?
这是理解 Kbuild 的关键点之一。
GNU make 自动变量回顾
| 变量 | 含义 |
|---|---|
$@ |
目标完整路径 |
$(@F) |
目标文件名(不含路径) |
$(@D) |
目标目录 |
例如:
make
$(obj)/arch/riscv/hypervisor.o
$@→build/arch/riscv/hypervisor.o$(@F)→hypervisor.o
Kbuild 的设计约定
Kbuild 规定:
按目标文件名(不含路径)设置专属 flags
make
LDFLAGS_hypervisor.o := --whole-archive -T
因此在模板中只能使用:
make
$(LDFLAGS_$(@F))
否则变量根本匹配不到。
四、链接器脚本 .lds 是如何"传给 ld 的"?
这是一个非常容易被误解的地方。
常见 Kbuild 写法
make
LDFLAGS_hypervisor.o := -T
$(obj)/hypervisor.o: hypervisor.lds $(objs)
$(call if_changed,ld)
你会注意到:
-T hypervisor.lds并没有显式写在命令中.lds只是作为 依赖文件
真正发生了什么?
Kbuild 的行为
- 将
$(LDFLAGS_$(@F))原样拼到 ld 命令前 - 将
$^(所有依赖)原样拼到命令后 - 不解析
.lds,也不理解-T的语义
GNU ld 的行为
GNU ld 的规则是:
text
-T script
Use script as the linker script
即:
-T后面的"下一个参数"会被当作 linker script
由于 .lds 通常是依赖列表中的第一个真实文件,最终形成的命令类似:
bash
ld -T hypervisor.lds -o hypervisor.o a.o b.o
这是 ld 的命令行语义,而不是 Kbuild 的特殊规则
因此,严格来说:
Kbuild 只是按模板拼接参数;
-T与.lds的配对完全依赖 GNU ld 的命令行解析规则。
五、为什么一定要把 .lds 写进依赖?
这是一个工程级的设计决策。
正确做法
make
hypervisor.o: hypervisor.lds
原因
- 修改链接脚本会触发重新链接
- 保证布局、段地址、对齐变化不会被 silently 忽略
- 支持
make的增量构建语义
如果你只在命令行里写 -T hypervisor.lds,但不写依赖,会有以下问题:
- 修改
.lds可能 不会触发重建,这是非常危险的系统级 bug
六、在 Kbuild 中使用链接器脚本的注意事项
1. 永远把 .lds 放进依赖列表
make
target.o: target.lds $(objs)
2. 使用 LDFLAGS_<target>,不要写死命令
make
LDFLAGS_target.o := -T --whole-archive
3. 注意 .lds 在依赖列表中的顺序
- 通常放在最前
- 避免被
-o或其他参数"吃掉"