标签 · 链接器

33 篇文章,记录每一次探索。

[链接器的世界-实验篇15] 让反例决定质量:差分、变异与鲁棒性
实现可重放的变异与 panic 调查工具,通过原生运行、确定性检查和 GNU ld 差分测试检验此前的链接器后端。
阅读全文
[链接器的世界-实验篇14] 接住 rustc:把链接器变成真正的驱动程序
为累积的 class13 静态 PIE 后端实现命令行驱动,让 rustc 直接调用它,链接一个使用 core::fmt 的 no_std Rust 程序。
阅读全文
[链接器的世界-实验篇13] 地址也有语义:安全 ICF 与代码顺序
在 class12 的 ET_EXEC 与静态 PIE 后端上实现保守 ICF 和代码顺序,重定向引用并修正折叠副本的调试信息。
阅读全文
[链接器的世界-实验篇12] 让调试信息跟着地址走:DWARF、符号表与节头
为 ET_EXEC 与静态 PIE 补全调试节、符号表和节头表,区分图像地址、调试节偏移与 TLS 偏移。
阅读全文
[链接器的世界-实验篇11] 看穿间接层:GOT、指令松弛与静态 TLS
在 class9 的静态 PIE 上构建 GOT,实施课程支持的 mov 松弛,布局 TLS 模板,并用提供的启动代码安装主线程 TLS。
阅读全文
[链接器的世界-实验篇10] 不再原样复制:常量合并与展开表重建
在 class8 的 ET_EXEC 后端中合并重复常量,映射输入偏移,并为存活代码重建 .eh_frame 与搜索索引。
阅读全文
[链接器的世界-实验篇09] 不知道加载地址时怎么链接:自重定位的静态 PIE
在 class8 前端与 class6 布局之上生成静态 PIE,区分静态补丁和加载期指针,并通过提供的启动代码完成自重定位。
阅读全文
[链接器的世界-实验篇08] 只留下被用到的:可达性回收与链接映射
从入口和显式根追踪存活节,建立不依赖输出地址的链接计划,并输出确定、字节安全的 map。
阅读全文
[链接器的世界-实验篇07] 静态库按需取用:成员抽取的不动点
静态库是一组目标文件的容器,而不是一个更大的目标文件。本实验解析 ar 归档格式,按未解析的全局名字逐个抽取成员,并在同一个归档内反复扫描直到不再有新成员被选中。
阅读全文
[链接器的世界-原理篇16] 链接器工程:从能链接,到值得信赖
当输入增长到数万个目标文件,链接器既要提高速度,又要保持结果确定、错误可诊断。从并行阶段的依赖关系出发,讨论结构不变式、运行测试、差分测试、变异测试与模糊测试,分析增量链接和工具链集成怎样改变工程取舍。
阅读全文
[链接器的世界-原理篇15] 走出 ELF:Mach-O 与 PE/COFF 的另一套规则
从文件头、目录记录与内容的关系出发,对照 Mach-O 与 PE/COFF 的符号、重定位和加载机制;通过字节布局与地址换算解释导入、指针链、展开记录、去重及 TLS,并区分文件结构、链接接口与运行时契约。
阅读全文
[链接器的世界-原理篇14] C++ 与 Rust:语言特性留下的链接难题
两个 .cpp 包含同一个头文件,各自生成了同一个 inline 函数和模板实例,链接却不报重复定义。本文从这个谜题出发,读懂 Itanium C++ ABI 的名字修饰(_Z、N...E、模板参数、S_ 替换)、extern "C",以及为什么普通函数不编码返回类型;用汇编拆开 COMDAT 组和弱定义这两种 vague linkage 实现,复现 vtable 卷入的 ODR 违规和 undefined reference to vtable;用链接顺序演示静态初始化顺序问题,看 init_priority 与 .init_array.N 的排序、guard 变量与 __cxa_guard_acquire。Rust 部分拆开 rlib,对比 legacy 与 v0 两套修饰,看 no_mangle、used、link_section,用 --print link-args 逐项读 rustc 交给链接器的命令行和依赖顺序,比较 panic=unwind 与 panic=abort 的大小,最后不带标准库和 C 运行时链接一个最小的 Rust 程序。
阅读全文
[链接器的世界-原理篇13] ABI 演进:库升级了,旧程序怎么办
结构体加了一个字段,没有重新编译的旧程序照样能加载,算出来的数却错了。动态链接只按名字和版本配对,不看类型和布局。本文从这个故障出发,讲 ABI 和 API 的区别、哪些改动会破坏 ABI;libtool 的 current:revision:age 怎样变成 soname 和文件名,Debian 的包名为什么带 soname;符号版本在实践中怎么用:版本节点继承、同一个函数的新旧两个实现、glibc 的 memcpy 和 fmemopen、在新系统上编译的程序为什么到旧系统上报 GLIBC_2.34 not found 以及怎样用旧 sysroot 解决;最后用 abidiff 和 abi-compliance-checker 在发布前查出被改坏的 ABI。实验统一在 x86-64 Linux 原生执行,以 glibc 2.43 和隔离解包的 glibc 2.17 对照。
阅读全文
[链接器的世界-原理篇12] 链接期优化:看见更多代码之后
分开编译形成了优化的信息边界,LTO 通过保留 IR 并协调符号决议跨越这条边界。本文解释定义选择、内部化与代码生成的分工,分析 ThinLTO 的并行与缓存机制,再讨论 ICF 的等价判定、地址身份,以及函数布局如何影响代码局部性。
阅读全文
[链接器的世界-原理篇11] 调试信息:让机器码认回源代码
从代码地址、调试节内偏移与被删除代码的 tombstone 出发,解释 DWARF 如何随链接结果变化;进一步分析索引形式、字符串合并、压缩、独立调试文件和 split DWARF 的引用与分发机制。
阅读全文
[链接器的世界-原理篇10] 链接器脚本:当程序需要自己安排内存
链接器脚本把输入节组织成输出映像,并约定符号地址、存储区域与入口。本文区分 ELF 文件偏移、VMA 和 LMA,解释启动代码如何完成映射、数据复制与 BSS 清零,再以 QEMU、xv6、JOS 和 ROM/RAM 布局分析地址约定失配的后果。最后讨论数据嵌入、节保留、排序、对齐与脚本断言。
阅读全文
[链接器的世界-原理篇09] TLS:同一个变量,每个线程各有一份
thread_local 变量在每个线程里是同一个名字、不同的地址。这个地址由编译器、链接器和动态加载器合作算出:.tdata/.tbss 与 PT_TLS 是模板,%fs 和 TPIDR_EL0 指向每个线程自己的那一份,四种访问模型在通用和快之间取舍,链接器在知道更多信息时把慢序列原地改写成快序列。本文用 clang、ld.lld 和 GNU ld 的真实输出走一遍全过程,最后解释"cannot allocate memory in static TLS block"的根因。
阅读全文
[链接器的世界-原理篇08] 栈展开:沿调用栈找到回去的路
函数一层套一层地调用,返回地址散落在栈上。打印调用栈、抛出 C++ 异常时,运行时要一层层退回调用者,靠的是编译器和汇编器写进 .eh_frame 的一张"每条指令处怎么找回调用者"的表。本章用真实目标文件逐字节拆开 CIE/FDE,讲 CFI 状态机、personality 与 LSDA、两阶段展开,以及链接器为 .eh_frame 和 .eh_frame_hdr 要做的事。
阅读全文
[链接器的世界-原理篇07] 动态链接:把最后的地址留给运行时
共享库的加载地址要到运行时才知道,它引用的符号甚至可能被别的库替换。链接器在链接时能做的,是把所有"现在还填不了"的地址集中到少数几处,再给动态链接器 ld.so 写一份说明书。本文从 PIC、GOT、PLT 讲到几类动态重定位、惰性绑定、RELRO、符号插入、符号版本和 ld.so 的启动顺序,全部配真实的目标文件与链接输出。
阅读全文
[链接器的世界-实验篇06] 文件里的字节与内存里的字节:数据、BSS 与段权限
只有代码的程序几乎没有用。本实验加入只读常量、已初始化全局变量、BSS 和 COMMON,用三个权限不同的加载段描述它们,并让内核替我们检查:写只读数据、执行数据段都会收到 SIGSEGV。
阅读全文
[链接器的世界-原理篇06] 从入口到 main:程序启动之前发生了什么
链接器写完程序头表就退场了,剩下的事归加载器。本文先读 xv6 的 kexec,看一个最小的加载器只用哪几个字段、做哪三道检查;再读 Linux 的 load_elf_binary,看 PT_INTERP、PT_GNU_STACK、基址选择、.bss 的页尾清零与匿名页、按需调页和 brk;然后打印初始进程栈和辅助向量,跟着 musl 的源码从 _start 走到 main,手工复现驱动程序加的五个启动文件,构建一个 static-pie 看 rcrt1.o 怎样自己处理 R_X86_64_RELATIVE;最后在真实内核上改坏 ELF,看加载器怎样守住安全边界。
阅读全文
[链接器的世界-实验篇05] 跨越文件的调用:合并代码,再填写位移
上一阶段的可执行文件只能来自一个没有重定位的目标文件。本实验让汇编写的入口调用另一个文件里用 C 编译的函数:合并多个代码节,计算每个字段的 S+A-P,并保证失败时不留下半写的字节。
阅读全文
[链接器的世界-原理篇05] 布局与 GC:给每个节找位置,让无用代码退场
同一个目标文件,用不同的链接器、不同的选项链接,填进机器码的数字都不一样。这些数字取决于布局:输入节怎么并成输出节、输出节怎么分进段、段放在哪个地址、哪些节被垃圾回收删掉。本文用 GNU ld 和 lld 的真实输出,讲清楚程序头的每个字段、文件偏移与虚拟地址为什么要同余、-z separate-code、PIE 与基址、--gc-sections 的标记过程、__start_/__stop_ 符号,以及链接器脚本和映射文件。
阅读全文
[链接器的世界-实验篇04] 第一个能运行的 ELF:两张程序头与一个入口
前四个阶段只在读输入。本实验第一次写输出:规划一个最小的可执行映像,手工序列化 ELF 头和两个程序头,让 Linux 内核直接运行它并返回 42。
阅读全文
[链接器的世界-原理篇04] 重定位:一次调用,四个字节的距离
从一个真正能运行的 Linux 程序出发,追踪 call 指令里的四个字节:输入节偏移如何变成 P,CPU 为什么从下一条指令算起,S+A-P 又如何落成机器码。再逐步进入负位移、绝对指针、不同加数、溢出和链接期指令改写。
阅读全文
[链接器的世界-实验篇03] 同一个名字,谁说了算:全局符号解析
先选择跨文件的全局定义,再绑定实际引用;保持局部身份、弱定义顺序和 COMMON 的大小与对齐要求,不在解析阶段分配地址。
阅读全文
[链接器的世界-原理篇03] 符号解析:同一个名字,究竟指向谁
从一个能独立运行的加法程序开始,亲手制造缺失定义、重复定义和库顺序错误。沿着“谁需要、谁提供、提供者是否进入链接”理解符号解析,再讨论弱定义、common、类型检查与 C++ 的边界。
阅读全文
[链接器的世界-实验篇02] 符号与重定位记录:目标文件留下的待办事项
解码符号和显式加数重定位,建立字符串表、符号表与被修补节之间的归属关系,保留后续选择与布局所需信息。
阅读全文
[链接器的世界-原理篇02] 拆开目标文件:一份等待组装的程序
从一个 main.c、一个 add.c 和一份手写汇编出发,看汇编器怎样按伪指令把字节摆进各个节,再用 llvm-objdump 和几十行 Python 一个字节一个字节拆开 ELF 目标文件:ELF 头、节头表、程序头表、节的类型与标志、符号表、机器码里留下的零字节和重定位表。
阅读全文
[链接器的世界-实验篇01] 节与名字:把一段字节切成有意义的区域
解码节头、借用文件内容并解析节名,让后续阶段使用带名字的 Section,而不是重复读取原始字段。
阅读全文
[链接器的世界-原理篇01] 地址不能总靠手填:链接器的演化之路
从 1949 年 EDSAC 的子程序库讲起,沿着"一个问题催生一个发明"的线索,看重定位、汇编器、目标文件、ELF、共享库、DWARF、.eh_frame、COMDAT、可执行栈标记和四代链接器是怎么一个接一个出现的,以及它们怎样把链接器推到今天的位置。
阅读全文
[链接器的世界-实验篇00] 先别相信输入:ELF 文件的第一道边界
检查字节范围、支持的 ELF64 头和节头索引,为后续解析建立不会溢出或越界的输入边界。
阅读全文
[链接器的世界-原理篇00] 从一条编译命令出发:程序如何连成整体
从一个正常的跨文件函数调用出发,拆开编译器驱动的预处理、编译、汇编与链接过程,再借类型不一致的程序辨认链接器的信息边界。沿着目标文件、可执行文件与内存映射,建立符号解析、布局、重定位以及编译、链接、加载、运行四个时期的联系,并提供统一的 Linux 本机实验环境。
阅读全文