程序构建的沉默机械
每个程序员都写过编译命令,但很少有人真正理解ld在做什么。当编译器完成了词法分析、语法分析、中间代码生成、优化和目标代码生成后,它产出了一个或多个目标文件。这些文件包含了机器码,但还不能执行——它们之间存在引用关系,全局变量的地址未确定,外部函数的位置未知。链接器的任务就是将这些分散的碎片组装成一个完整的程序。
这个过程远比想象中复杂。现代链接器需要处理符号解析、重定位、库依赖、版本控制等众多问题。而加载器则负责将链接后的程序真正送入内存执行。两者共同构成了程序构建流水线的最后一公里。
flowchart LR
A[源代码] --> B[编译器]
B --> C[目标文件]
C --> D[链接器]
D --> E[可执行文件]
E --> F[加载器]
F --> G[内存中的进程]
符号:链接的基本单元
理解链接器的第一步是理解符号。在编译过程中,每个函数、全局变量都会被赋予一个符号名。编译器在目标文件中维护两张关键表:符号表(.symtab)和重定位表(.rel.text、.rel.data)。
符号表记录了本模块定义的符号及其属性。每个符号表项包含:
st_name:符号名在字符串表中的索引st_value:符号的值(通常是地址或偏移)st_size:符号的大小st_info:符号的类型和绑定属性st_other:可见性等信息st_shndx:符号所在的节索引
符号绑定属性决定了符号的可见性。STB_LOCAL表示本地符号,只能在定义它的目标文件中访问;STB_GLOBAL表示全局符号,可以被其他目标文件引用;STB_WEAK表示弱符号,提供了一种可选的符号定义机制。
当编译器遇到一个外部引用时,它无法确定被引用符号的实际地址。这时它会生成一个重定位条目,告诉链接器:“等你知道这个符号的地址后,把它填到这里”。
重定位条目的结构因体系结构而异,但核心字段包括:
r_offset:需要修改的位置在节中的偏移r_info:包含符号表索引和重定位类型r_addend(某些格式):用于计算的额外值
重定位类型决定了如何计算和修改目标地址。以x86-64为例,R_X86_64_PC32表示PC相对寻址,计算公式为S + A - P,其中S是符号地址,A是加数,P是修改位置。而R_X86_64_32表示绝对寻址,直接将符号地址写入目标位置。
flowchart TB
subgraph 目标文件A
A1[代码节]
A2[数据节]
A3[符号表]
A4[重定位表]
end
subgraph 目标文件B
B1[代码节]
B2[数据节]
B3[符号表]
B4[重定位表]
end
subgraph 链接器
L1[符号解析]
L2[重定位]
L3[节合并]
end
A4 --> L1
B3 --> L1
L1 --> L2
L2 --> L3
符号解析:解谜的艺术
符号解析是链接器最核心的工作之一。它的任务是将每个符号引用与其唯一的定义关联起来。这个过程看似简单——找到名字匹配的定义不就行了——但实际上充满了复杂性。
首先考虑简单情况:如果符号引用只有一个定义,解析就很直接。但当存在多个同名定义时,问题就出现了。链接器需要处理强符号和弱符号的区别。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。链接器的规则是:
- 不允许有多个强符号同名
- 如果一个强符号和多个弱符号同名,选择强符号
- 如果多个弱符号同名,选择任意一个
这些规则可能导致微妙的问题。考虑以下代码:
// file1.c
int x = 1; // 强符号
// file2.c
int x; // 弱符号,会被忽略
// file3.c
int x = 2; // 编译错误:重复定义
符号解析还涉及库的处理。静态库是一组目标文件的归档,链接器按需从中提取目标文件。这里的"按需"非常重要:链接器从左到右处理命令行参数,遇到静态库时,只提取那些能解析当前未定义符号的目标文件。这导致了著名的"链接顺序问题":
# 错误:liba.a中的符号不会被提取,因为它在main.o之前处理
gcc -la main.o -lb
# 正确:main.o中的未定义符号会触发liba.a的提取
gcc main.o -la -lb
对于循环依赖的库,可以使用-Wl,--start-group -la -lb -Wl,--end-group让链接器多次扫描,但这会增加链接时间。更好的做法是重构代码,消除循环依赖。
重定位:填充最后的空白
符号解析完成后,链接器知道了每个符号的最终地址。现在需要进行重定位,将符号地址填入代码和数据中预留的位置。
重定位过程分为两步:节重定位和符号重定位。节重定位将各个目标文件的同名节合并,并为每个节分配运行时地址。符号重定位则处理具体的符号引用。
考虑一个简单的例子:
// main.c
extern int global_var;
extern void external_func(void);
int main() {
int x = global_var;
external_func();
return 0;
}
编译后,main.o中访问global_var和调用external_func的代码会包含占位符。链接时,链接器会:
- 确定所有节的最终地址
- 解析
global_var和external_func的地址 - 根据重定位类型修改代码中的占位符
对于函数调用,重定位类型通常是PC相对寻址。链接器计算目标地址与调用指令地址的差值,将其填入调用指令的操作数字段。对于全局变量访问,可能是PC相对寻址(如x86-64的mov global_var(%rip), %eax)或绝对寻址(如x86-32的mov global_var, %eax)。
重定位不仅发生在静态链接时。动态链接的可执行文件在运行时也需要重定位——这就是加载时重定位和运行时重定位的由来。
ELF:链接与执行的双重视角
可执行与可链接格式(ELF,Executable and Linkable Format)是Unix类系统的标准二进制格式。ELF的精妙之处在于它提供了两种视角:链接视角和执行视角。
链接视角关注节:代码节、数据节、符号表、重定位表等。编译器和链接器从这个视角处理文件。执行视角关注段:代码段、数据段等。加载器从这个视角将文件映射到内存。
+------------------+
| ELF Header |
+------------------+
| Program Headers | <--- 执行视角
+------------------+
| .text节 |
| .data节 | <--- 链接视角
| .symtab节 |
| .rel.text节 |
+------------------+
| Section Headers | <--- 链接视角
+------------------+
节包含链接器需要的所有信息,而段是节到内存映射的描述。一个段可以包含多个节,例如,一个可加载段可能同时包含.text、.rodata和.eh_frame节。
这种设计带来一个重要后果:可执行文件可以剥离调试信息后仍能正常运行。strip命令会删除符号表和调试节,但程序头表保留,加载器仍然能正确加载程序。
ELF文件头包含了文件类型信息:
ET_REL:可重定位目标文件ET_EXEC:可执行文件ET_DYN:共享目标文件
每种类型都有特定的处理方式。可重定位文件的节从地址0开始,需要重定位;可执行文件有固定的加载地址;共享目标文件是位置无关的,可以加载到任意地址。
动态链接:延迟的艺术
静态链接简单直接,但存在明显问题:每个程序都包含一份库代码,浪费磁盘空间;库更新需要重新链接所有程序。动态链接解决了这些问题,但引入了新的复杂性。
动态链接的核心思想是将链接过程推迟到运行时。链接时,链接器只在可执行文件中记录依赖的共享库名称。运行时,动态链接器(ld-linux.so)加载这些库并完成符号绑定。
但这里有一个问题:编译时不知道库会被加载到什么地址,如何生成访问库中符号的代码?解决方案是位置无关代码(PIC)和全局偏移表(GOT)。
位置无关代码
PIC的核心思想是:不使用绝对地址,所有地址引用都相对于当前位置计算。对于函数调用,使用PC相对调用即可。对于全局变量访问,由于x86-64之前的架构不支持PC相对数据寻址,需要借助GOT。
GOT是一张地址表,每个表项存储一个全局符号的地址。代码通过固定偏移访问GOT中的表项,而GOT本身的位置可以通过PC相对寻址获得。运行时,动态链接器填充GOT表项。
+------------------+
| 代码段 |
| mov foo@GOTPCREL(%rip), %rax | -- 获取foo的地址
| mov (%rax), %ebx | -- 访问foo的值
+------------------+
| 数据段 |
| GOT[0]: ... |
| GOT[1]: ... |
| GOT[n]: <foo的实际地址> | <-- 动态链接器填充
+------------------+
过程链接表与延迟绑定
函数调用也面临类似问题。如果每个函数调用都要通过GOT间接跳转,会有性能损失。更重要的是,如果程序只使用了库中的少数函数,在程序启动时解析所有函数地址会延长启动时间。
解决方案是过程链接表(PLT)和延迟绑定。PLT是一小段代码,每个外部函数对应一个PLT条目。GOT被拆分为.got(存放全局变量地址)和.got.plt(存放函数地址)。
首次调用外部函数时:
- 代码跳转到PLT条目
- PLT条目跳转到GOT中存储的地址
- 首次调用时,该地址指向PLT条目的下一条指令(形成循环)
- 执行解析代码,调用
_dl_runtime_resolve - 解析器找到函数实际地址,填入GOT
- 跳转到实际函数
后续调用时,GOT中已有实际地址,直接跳转,无需解析。
sequenceDiagram
participant 代码
participant PLT
participant GOT
participant 动态链接器
participant 目标函数
Note over 代码,目标函数: 首次调用
代码->>PLT: 调用foo@plt
PLT->>GOT: 跳转到*foo@got
Note right of GOT: 存储的是PLT下一条指令地址
GOT-->>PLT: 返回PLT
PLT->>动态链接器: 调用_dl_runtime_resolve
动态链接器->>目标函数: 查找foo地址
动态链接器->>GOT: 写入foo实际地址
PLT->>目标函数: 跳转到foo
Note over 代码,目标函数: 后续调用
代码->>PLT: 调用foo@plt
PLT->>GOT: 跳转到*foo@got
Note right of GOT: 存储的是foo实际地址
GOT->>目标函数: 直接跳转
PLT的巧妙之处在于它将公共的解析代码提取出来。PLT的第一个条目是解析器入口,后续每个条目的格式是:
foo@plt:
jmp *foo@got(%rip) ; 跳转到GOT中的地址
push $index ; 压入符号索引
jmp .plt[0] ; 跳转到解析器
.got.plt的前三个条目有特殊用途:
- GOT[0]:
.dynamic节的地址 - GOT[1]:模块ID(用于解析器)
- GOT[2]:
_dl_runtime_resolve的地址
这样设计使得PLT条目紧凑而高效。
动态链接器:程序启动的幕后英雄
当执行一个动态链接的程序时,内核加载可执行文件后,控制权并不直接转给程序,而是先给动态链接器。动态链接器是一段特殊的代码,它在程序运行前完成最后的链接工作。
动态链接器的工作流程大致如下:
- 自举:动态链接器本身也是共享库,需要先完成自己的重定位
- 加载依赖:解析可执行文件的DT_NEEDED项,递归加载所有依赖库
- 重定位:对每个加载的模块执行重定位
- 初始化:执行各模块的初始化代码(
.init、.init_array) - 移交控制:跳转到程序的入口点
库的搜索顺序是:
- DT_RPATH(已废弃)
- LD_LIBRARY_PATH环境变量
- DT_RUNPATH
- /etc/ld.so.cache
- 默认路径(/lib、/usr/lib等)
DT_RPATH和DT_RUNPATH的区别在于搜索顺序:DT_RPATH优先于LD_LIBRARY_PATH,而DT_RUNPATH在LD_LIBRARY_PATH之后。现代实践推荐使用DT_RUNPATH(通过-Wl,--enable-new-dtags或-Wl,-rpath选项)。
动态链接器还支持预加载(LD_PRELOAD),允许在程序启动前注入指定的库。这在调试、性能分析和某些安全场景中很有用,但也可能被滥用。
flowchart TB
A[内核加载可执行文件] --> B[识别.interp段]
B --> C[加载动态链接器]
C --> D[动态链接器自举]
D --> E[加载依赖库]
E --> F[递归加载传递依赖]
F --> G[执行重定位]
G --> H[运行初始化代码]
H --> I[跳转到程序入口]
复制重定位:效率与简洁的权衡
位置无关代码是共享库的理想选择,但有时库中的全局变量会被可执行文件直接访问。考虑以下场景:
// lib.c
int global_var = 42;
// main.c
extern int global_var;
int main() { return global_var; }
如果main.c编译时没有使用PIC,它会生成直接访问global_var的代码。但global_var在共享库中,运行时地址未知。如何处理?
链接器的解决方案是复制重定位。它在可执行文件的BSS节分配空间,并在动态段创建一个特殊重定位项,告诉动态链接器将库中的变量值复制到这个位置。这样,可执行文件中的代码就能用绝对地址访问变量了。
这带来一些限制:复制重定位的变量不能在库中修改(因为可执行文件有自己的副本),且必须知道变量的大小。因此,共享库中导出的全局变量最好有固定大小。
现代链接器:速度的革命
传统的GNU ld使用BFD库,设计目标是可移植性而非速度。对于大型项目,链接时间可能非常长。这催生了新一代链接器。
gold
Google开发的gold链接器首次将速度作为首要目标。它不使用BFD库,而是直接处理ELF格式,采用更高效的数据结构。gold的符号解析使用哈希表而非链表,内存分配更高效,整体设计更现代。
gold比传统ld快2-5倍,但也带来一些不兼容性。某些依赖于ld特定行为的链接脚本可能需要修改。
LLVM lld
lld是LLVM项目的链接器,采用模块化设计,支持ELF、COFF、Mach-O和WebAssembly格式。它的速度与gold相当或更快,且与LLVM工具链深度集成。
lld的一个独特功能是增量链接优化,可以在链接时进行更多优化,如消除重复代码、合并相似函数。
mold
mold是近年来最激进的链接器创新。由Rui Ueyama开发,它的速度比lld和gold快2-10倍。mold的秘诀包括:
- 高度并行的算法设计
- 内存映射文件的高效使用
- 最小化内存拷贝
- 缓存友好的数据结构
graph LR
A[GNU ld<br/>传统] --> B[gold<br/>Google]
A --> C[lld<br/>LLVM]
B --> D[mold<br/>更快]
C --> D
mold的设计哲学是:链接器不应该成为编译的瓶颈。对于包含数百万行代码的项目,mold可以将链接时间从分钟级降到秒级。
链接时优化:跨越编译单元
传统编译器一次处理一个源文件,优化范围受限。内联、常量传播等优化无法跨越编译单元边界。链接时优化(LTO)打破了这个限制。
LTO的基本思想是:编译时生成中间表示(IR)而非机器码,链接时将所有IR合并,进行全局优化,再生成最终代码。
LLVM的LTO流程:
- 编译器生成包含LLVM bitcode的目标文件
- 链接器识别bitcode,调用LTO插件
- LTO插件合并所有bitcode,执行全局优化
- 生成本地代码,继续常规链接
LTO可以实现跨编译单元的内联、死代码消除、全局常量传播等优化。代价是链接时间增加(因为优化工作转移到链接阶段)和内存使用增加。
flowchart LR
subgraph 传统编译
A1[源文件A] --> B1[目标文件A]
A2[源文件B] --> B2[目标文件B]
B1 --> C[链接]
B2 --> C
C --> D[可执行文件]
end
subgraph LTO
E1[源文件A] --> F1[IR文件A]
E2[源文件B] --> F2[IR文件B]
F1 --> G[合并与优化]
F2 --> G
G --> H[代码生成]
H --> I[可执行文件]
end
LTO的使用很简单,只需添加编译选项:
# GCC/Clang
gcc -flto -O2 *.c
# 更激进的优化
gcc -flto -O3 -ffat-lto-objects *.c
-ffat-lto-objects选项生成同时包含IR和机器码的目标文件,便于不使用LTO时的兼容处理。
链接器脚本:精细控制内存布局
对于嵌入式开发或特殊需求,默认的内存布局可能不适用。链接器脚本提供了对内存布局的完全控制。
一个简单的链接器脚本:
ENTRY(_start)
MEMORY
{
RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M
FLASH (rx) : ORIGIN = 0x0, LENGTH = 1M
}
SECTIONS
{
.text : {
*(.text.entry)
*(.text .text.*)
} > FLASH
.rodata : {
*(.rodata .rodata.*)
} > FLASH
.data : {
_data_start = .;
*(.data .data.*)
_data_end = .;
} > RAM AT > FLASH
.bss : {
_bss_start = .;
*(.bss .bss.*)
_bss_end = .;
} > RAM
}
这个脚本定义了两个内存区域(RAM和FLASH),并将不同节放入适当位置。.data节的AT > FLASH表示数据存储在FLASH中,但运行时位于RAM,启动代码需要负责复制。
链接器脚本还允许定义符号(如_data_start),供启动代码使用。这是嵌入式系统初始化的标准模式。
弱符号与符号版本:灵活性的代价
弱符号是一种特殊的符号定义,可以被强符号覆盖而不产生冲突。它常用于提供默认实现:
// 默认实现
__attribute__((weak)) void log_message(const char* msg) {
printf("[LOG] %s\n", msg);
}
// 用户可以提供自己的实现覆盖
void log_message(const char* msg) {
syslog(LOG_INFO, "%s", msg);
}
弱符号的另一个用途是检测特性是否可用:
// 检查函数是否存在
if (foo_ptr != NULL) {
foo_ptr(); // 函数存在则调用
}
// 声明弱引用
extern void foo(void) __attribute__((weak));
符号版本是另一种高级特性,允许同一库中存在同一符号的多个版本。这对于保持向后兼容性至关重要。例如,glibc中memcpy有多个版本,旧程序使用旧版本语义,新程序使用新版本语义。
// 定义版本化符号
__asm__(".symver old_memcpy,memcpy@GLIBC_2.0");
__asm__(".symver new_memcpy,memcpy@@GLIBC_2.14");
void* old_memcpy(void* dest, const void* src, size_t n) {
// 旧版本可能不处理重叠区域
}
void* new_memcpy(void* dest, const void* src, size_t n) {
// 新版本正确处理重叠
}
版本脚本控制哪些符号被导出以及它们的版本:
VER_1.0 {
global:
foo;
bar;
local:
*; // 其他符号不导出
};
VER_2.0 {
global:
baz; // 新增符号
} VER_1.0; // 继承之前版本
异常处理:链接器的隐藏角色
C++异常处理依赖于链接器配合。当异常被抛出时,运行时需要找到匹配的catch块并正确展开栈。这需要精确知道每个函数的栈帧布局。
.eh_frame节包含调用帧信息(CFI,Call Frame Information),描述了如何展开每个函数的栈帧。编译器生成CFI指令,链接器负责合并它们,确保地址正确。
.eh_frame:
CIE (Common Information Entry)
# 通用信息:返回地址寄存器、栈指针等
FDE (Frame Description Entry) for func1
# func1的栈帧布局
# 位置 -> CFI指令序列
FDE for func2
# func2的栈帧布局
链接器还处理.gcc_except_table,其中包含类型信息和异常处理表。这些表在链接时需要重定位,以便正确指向类型信息。
如果链接器配置错误(如使用--no-eh-frame-hdr),异常处理可能失败。这就是为什么某些嵌入式平台禁用异常——缺少链接器支持。
线程局部存储:每个线程的私有空间
线程局部存储(TLS)允许每个线程拥有全局变量的私有副本。链接器在实现TLS中扮演关键角色。
__thread关键字(或C++11的thread_local)声明TLS变量:
__thread int errno; // 每个线程有自己的errno
thread_local int counter = 0; // C++11
TLS的实现因平台而异。在x86-64 Linux上,TLS块位于线程特定的段寄存器(%fs)指向的区域。访问TLS变量需要计算相对于%fs的偏移。
链接器负责:
- 收集所有TLS变量到
.tdata(已初始化)和.tbss(未初始化)节 - 分配每个变量的偏移
- 生成适当的重定位
动态链接时,动态链接器需要为每个线程分配TLS块,并初始化其内容。
调试信息:从源码到二进制的映射
调试器需要在机器码和源代码之间建立映射。这通过调试信息实现,主要格式是DWARF。
编译器生成多个调试节:
.debug_info:核心调试信息,类型、变量、函数描述.debug_abbrev:缩写表,压缩.debug_info.debug_line:行号信息,地址到源码行映射.debug_str:字符串表.debug_ranges:地址范围表
链接器处理这些节的方式与普通节类似,但需要特殊处理地址引用。当函数地址因重定位改变时,调试信息中的地址也需要更新。
分离调试信息(-gsplit-dwarf)是一种优化:将调试信息放在单独的.dwo文件中,减少链接时处理的数据量。链接器生成.dwo文件的索引,调试器据此找到对应的调试信息。
链接器优化:超越编译器
链接器拥有全局视角,可以看到整个程序。这使其能执行编译器无法完成的优化。
相同代码折叠(ICF)
ICF检测并合并完全相同的函数。这在C++模板代码中特别有效,不同模板实例化可能生成相同的机器码。
template<typename T>
T* get_instance() {
static T instance;
return &instance;
}
// get_instance<int>() 和 get_instance<float>()
// 可能生成相同代码(如果sizeof(int)==sizeof(float))
启用ICF:-Wl,--icf=all(gold、lld、mold支持)。
垃圾回收
--gc-sections选项让链接器删除未被引用的节。这需要编译时使用-ffunction-sections和-fdata-sections,将每个函数和变量放入独立的节。
gcc -ffunction-sections -fdata-sections -Wl,--gc-sections
这对嵌入式系统特别有用,可以显著减少代码大小。
符号剥离
strip命令删除符号表和调试信息。-s或--strip-all完全剥离,--strip-debug只删除调试信息。剥离后的二进制文件更小,但难以调试。
实践中的链接问题
常见错误
未定义符号:最常见错误,通常由缺少库或拼写错误引起。检查库的链接顺序和符号名(C++有名称修饰)。
重复定义:同名符号在多个目标文件中定义。检查是否在头文件中定义变量或函数(应使用extern声明,在一个源文件中定义)。
库版本冲突:程序链接的库版本与运行时找到的版本不同。使用ldd检查依赖,确保库路径正确。
调试技巧
# 查看符号表
nm -C program
# 查看依赖
ldd program
# 查看动态符号
readelf -Ws program
# 查看重定位
readelf -r program
# 查看节
readelf -S program
# 动态链接器调试
LD_DEBUG=bindings program
LD_DEBUG=libs program
LD_DEBUG=versions program
静态链接与动态链接的选择
静态链接优点:
- 简单部署,无依赖问题
- 可预测的性能,无运行时开销
- 可进行更多优化
动态链接优点:
- 节省磁盘和内存空间
- 库更新无需重新链接
- 支持插件架构
现代实践倾向于动态链接系统库,静态链接应用程序特定的依赖。
链接器的未来
链接器技术仍在演进。增量链接可以显著减少大型项目的链接时间,目前已有部分实现。LLVM的 ThinLTO 将LTO增量化,支持分布式编译。
WebAssembly带来了新的链接模型。Wasm模块是独立的,可以动态加载和链接,但需要工具链支持。
Rust和Go等新语言在链接器层面做了创新。Go使用自己的链接器,完全控制链接过程。Rust利用LLVM和LTO,提供跨语言的优化能力。
结语
链接器和加载器是程序构建流水线的沉默工作者。它们将编译器的输出转化为可运行的程序,处理符号解析、重定位、动态链接等复杂任务。理解它们的工作原理,不仅能帮助诊断构建问题,还能更好地理解程序的运行时行为。
从最初的简单重定位到现代的LTO和ICF,链接器技术不断演进。新一代链接器(mold等)证明了即使在这个看似成熟的领域,创新仍然可能。随着软件规模的增长和新平台的出现,链接器仍将是编译工具链中不可或缺的一环。
参考资料
- Beginner’s Guide to Linkers - https://www.lurklurk.org/linkers/linkers.html
- ELF Specification - https://refspecs.linuxbase.org/elf/gabi4+/ch4.reloc.html
- ld.so(8) - Linux manual page - https://www.man7.org/linux/man-pages/man8/ld.so.8.html
- PLT and GOT Workflow in Dynamic Linking - openEuler
- Linker and Libraries Guide - Oracle
- Computer Systems: A Programmer’s Perspective - CSAPP Chapter 7
- Position Independent Code (PIC) in shared libraries - Eli Bendersky
- All about Global Offset Table - MaskRay
- All about symbol versioning - MaskRay
- Symbol processing - MaskRay
- mold: Modern Linker - https://github.com/rui314/mold
- LLVM Link Time Optimization - https://llvm.org/docs/LinkTimeOptimization.html
- Static Library Linking Order - Eli Bendersky
- Linker Scripts Explained - dev.to
- Weak Symbols - Wikipedia
- Thread-Local Storage - Oracle Linker Guide
- Exception Handling - DWARF Debugging Standard
- Identical Code Folding - Google Research Paper
- Understanding ELF File Layout - Low-Level Lore
- Copy Relocations - Oracle Linker Guide
- Dynamic Linking Performance - LWN.net
- Load-time relocation of shared libraries - Eli Bendersky
- Symbol Resolution - Oracle Linker Guide
- Memory Layout of C Programs - GeeksforGeeks
- ELF sections vs segments - Stack Overflow
- Global Constructors and Destructors - OSDev Wiki
- Debugging Options - GCC Documentation
- Relocation Types - Oracle Linker Guide
- Symbol Table Section - Oracle Linker Guide
- C++ Name Mangling - Medium
- Dynamic linker search path - Stack Exchange
- TLS implementation - GCC Documentation
- Stack unwinding - MaskRay
- Linker performance optimization - Microsoft DevBlog
- Wild linker incremental linking - David Lattimore
- Understanding Weak Symbols - Beningo Embedded Group
- Shared library address space - Stack Overflow
- inline function linker handling - Stack Overflow
- COMDAT and ICF - MaskRay
- Embedded system linker scripts - Stack Overflow
- GOT for global variables - Medium
- Modern linker comparison - Phoronix