链接把目标文件中的符号、节和重定位组织成可执行映像或共享对象;加载把映像放入进程地址空间,并完成运行所需的动态准备。两者相互配合,但不是同一个阶段。
本文的具体格式与公式以 Linux ELF、x86-64 System V ABI 为例。其他平台的对象格式、调用约定和动态链接策略应查其自身规范。
目标文件里除了机器码还有什么
内容 作用
.text、.data、.rodata 代码及不同属性的数据
.bss 类 NOBITS 节 描述需要零初始化的空间,不存等量零字节载荷
符号表 定义、未定义引用、绑定与可见性等
重定位项 告诉后续工具如何修补地址相关字段
调试信息 把机器位置与源码、类型关联
节是链接视角的组织单位;程序头描述加载视角的段。运行时常按 PT_LOAD 等程序头映射,不是按每个节名逐个 mmap。.bss 有元数据,只是不需要在文件里保存它表示的每个零字节。
符号解析与布局:先决定引用谁
一个目标文件定义 helper,另一个目标文件引用 helper。链接器根据绑定、可见性、版本和链接规则决定该引用对应哪个定义,再布局相关内容并应用重定位。
同名不总是冲突:局部符号有自己的作用域,弱符号与强符号有不同规则,动态符号还受可见性及查找范围影响。反过来,也不能把动态符号解析简化成“哪个库最早加载,永远用哪个”。
重定位:每种类型都有准确公式
令 S 为目标符号地址,A 为 addend,P 为重定位字段所在地址。x86-64 的 R_X86_64_PC32 使用 S+A−P,R_X86_64_64 使用 S+A。不同类型还规定字段宽度与溢出检查。AMD64 ABI 重定位表
PC 相对重定位:P 是待修补字段,不是下一条指令
call 操作码
地址 0x1000
4 字节位移字段
P = 0x1001
下一条指令
地址 0x1005
S = 0x2000,A = −4:S + A − P = 0xFFB
CPU 从下一条地址加位移:0x1005 + 0xFFB = 0x2000。不能再额外减一次 4。
在典型 call rel32 编码中,CPU 相对的是下一条指令,而 ABI 公式使用字段位置 P,常见 addend −4 已负责两者差值。若公式再写成 S−(P+4)+A,同时 A 又为 −4,就重复扣了 4。
外部函数调用可能使用 R_X86_64_PLT32,其目标涉及 PLT 入口并可能被链接器放松为直接调用;不能要求每个编译器的外部调用都显示 PC32。理解公式后,应该检查实际重定位类型。
写入四字节前,先检查有符号范围
rel32 位移是有符号 32 位数。目标在调用点之前时位移可以为负,写入小端字段用补码;目标太远时不能简单截断到低 32 位,否则 CPU 会跳到另一个地址。下面只模型化已经确定直接目标的 PC32 计算,不模拟符号查找或 PLT 选择。
def pc32 ( symbol , addend , place ) :
displacement = symbol + addend - place
if not - ( 1 < < 31 ) <= displacement < ( 1 < < 31 ) :
raise OverflowError ( " PC32 relocation out of range" )
return displacement . to_bytes ( 4 , " little" , signed = True )
assert pc32 ( 0x 2000, - 4 , 0x 1001) == bytes . fromhex ( " fb 0f 00 00" )
backward = pc32 ( 0x 1000, - 4 , 0x 2001)
assert 0x 2005 + int . from_bytes ( backward , " little" , signed = True ) == 0x 1000
for displacement in ( - ( 1 < < 31 ) , ( 1 < < 31 ) - 1 ) :
assert int . from_bytes ( pc32 ( displacement , 0 , 0 ) , " little" , signed = True ) == displacement
for displacement in ( - ( 1 < < 31 ) - 1 , 1 < < 31 ) :
try :
pc32 ( displacement , 0 , 0 )
except OverflowError :
pass
else :
raise AssertionError ( " truncated relocation accepted" )
共同平移 S 和 P 不改变位移,这解释了模块内部某些 PC 相对引用为何无需随加载基址重新修补;但模块外定义、绝对地址字段和 TLS 等仍有各自规则。不能从一条内部相对调用推广出“PIC 没有重定位”。
P、下一条指令和 −4,谁已经补过距离?
先标出四字节字段 P,再标出下一条指令 P+4,addend 的作用就不再含糊。下面既能将目标放到调用点之前,也能整体平移模块,还能故意重复减 4。位移越界时不生成截断后的四字节,因为一个“仍能写进文件的数字”不等于 CPU 还能抵达正确地址。
d = S + A − P , A = − 4 , ( P + 4 ) + d = S
正在呈现知识画面 重新加载
P、下一条指令和 −4,谁已经补过距离? PC32 写入 S+A−P,典型 call 的 A=−4 已把字段位置转换到下一条指令基准。S 与 P 一起平移不改变位移;重复减4会跳错。
{"id":"pc-relative-relocation","title":"P、下一条指令和 −4,谁已经补过距离?","summary":"PC32 写入 S+A−P,典型 call 的 A=−4 已把字段位置转换到下一条指令基准。S 与 P 一起平移不改变位移;重复减4会跳错。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"target\" data-i18n=\"target\"></label><output for=\"target\" id=\"target-value\"></output><input id=\"target\" type=\"range\" min=\"0\" max=\"1024\" value=\"512\" step=\"16\"></div><div class=\"control\"><label for=\"base\" data-i18n=\"base\"></label><output for=\"base\" id=\"base-value\"></output><input id=\"base\" type=\"range\" min=\"4096\" max=\"16384\" value=\"4096\" step=\"4096\"></div><label class=\"check\"><input type=\"checkbox\" id=\"double\"><span data-i18n=\"double\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"far\"><span data-i18n=\"far\"></span></label><div id=\"positions\" class=\"table\"></div><div class=\"metrics\"><div><span data-i18n=\"patch\"></span><strong id=\"patch\"></strong></div><div><span data-i18n=\"destination\"></span><strong id=\"destination\"></strong></div></div><div id=\"bytes\" class=\"track\"></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.metrics strong{font-size:17px}#positions .tr{grid-template-columns:1fr 1fr}#equation{white-space:pre-line;min-height:5.4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function draw(){const base=+$('#base').value,P=base+257,S=$('#far').checked?P+4+2**31:base+ +$('#target').value,A=-4,patch=S+A-P-($('#double').checked?4:0),valid=patch>=-(2**31)&&patch<2**31,dest=P+4+patch;set('target-value',$('#target').value);set('base-value','0x'+base.toString(16));table('positions',[['P','0x'+P.toString(16)],['P+4','0x'+(P+4).toString(16)],['S','0x'+S.toString(16)]]);set('patch',valid?patch:'overflow');set('destination',valid?'0x'+dest.toString(16):'—');cells('bytes',Array.from({length:4},(_,i)=>({text:valid?((patch>>>0)>>>8*i&255).toString(16).padStart(2,'0'):'—',cls:valid?'on':'gold'})));set('equation','S + (−4) − P'+($('#double').checked?' − 4':'')+' = '+patch+'\\n'+(valid?'P+4+disp '+(dest===S?'= S ✓':'≠ S ✗'):'−2³¹ ≤ disp < 2³¹ ✗'));$('#destination').dataset.valid=String(valid&&dest===S);}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"base":"共同加载基址","bytes":"小端四字节","destination":"CPU 实际落点","double":"错误地重复减去 4","far":"目标超出 rel32 正向范围","heading":"P、下一条指令和 −4,谁已经补过距离?","intro":"字段位置 P 固定在模块偏移0x101,call 下一条位于 P+4。移动目标或整体加载基址,核对修补位移与 CPU 落点。","patch":"写入位移","reset":"重置","scope":"x86-64 ELF 直接 PC32 教学模型,rel32 小端补码,范围检查为有符号32位。符号绑定已确定,不模拟 PLT32、放松或所有 PIC 重定位。","scopeLabel":"模型范围与约定","target":"目标模块偏移 S−base"}}
一个最小目标文件实验
在单独的实验目录创建两个文件:
extern int twice ( int ) ;
int main ( void ) { return twice ( 21 ) == 42 ? 0 : 1 ; }
int twice ( int x ) { return x * 2 ; }
cc - O0 - c caller.c - o caller.o
cc - O0 - c twice.c - o twice.o
readelf - sW caller.o
readelf - rW caller.o
objdump - dr caller.o
cc caller.o twice.o - o demo
./demo
先找未定义的 twice 符号,再看对应重定位,最后观察链接后的程序如何运行。具体偏移、指令与 PIE 默认值随工具链变化,实验重点是将符号引用与修补字段对应起来。
静态库:按规则抽取目标成员
.a 是目标文件归档。传统 Unix 链接器通常在处理归档时按尚未解决的引用抽取成员,因此命令行顺序可能重要:引用者放在提供定义的静态库之前,是常见写法。
循环依赖可以通过库分组重复扫描等机制处理;GNU ld 和其他链接器支持相应选项,但具体扫描策略不同。LLD 还会保存归档中的懒符号并在后续引用时提取,不能把所有链接器都描述成完全相同的左到右一次扫描。
链接器 GC 按节及重定位等引用关系保留从根可达的内容,根还可包含导出符号和链接脚本 KEEP。它不只是从 _start 反汇编追踪 call 指令。按函数或数据拆节有助于细粒度回收,但不是所有输入天然已拆好。
一个成员进了链接,不等于里面每个函数都留下
继续使用前面的 caller.c,把 twice.c 扩展为两个函数:twice 被 main 调用,unused_helper 没有任何引用。用 GNU ld.bfd 做下面的实验,固定链接器以避免将其归档扫描策略误当成所有工具的规则。
int twice ( int x ) { return x * 2 ; }
int unused_helper ( void ) { return 99 ; }
gcc - O0 - ffunction-sections - fdata-sections - c twice.c - o twice.o
ar rcs libtwice.a twice.o
gcc - fuse-ld= bfd caller.o ./libtwice.a - o with-unused
gcc - fuse-ld= bfd caller.o ./libtwice.a - Wl ,--gc-sections - o trimmed
nm with-unused
nm trimmed
./with-unused
./trimmed
第一次链接因 caller.o 对 twice 的引用抽取整个 twice.o,所以两个函数都进入输入集合。第二次仍会抽取同一个成员,但节回收发现 .text.unused_helper 不在保留图中,可以将它移除。两份程序均返回 0,nm 中 unused_helper 则只应出现在未回收版本里。这里使用 O0 且未启用 LTO,避免把前端内联/删除效果混入解释。
阶段 粒度 本例决定
搜索归档 目标成员 为满足 twice 引用提取 twice.o
节回收 输入节及其引用图 保留 twice 所在节,删除未引用函数节
最终布局与重定位 保留内容的地址和字段 为 main 的调用修补最终位移
若把 libtwice.a 放到 caller.o 之前,GNU ld.bfd 在当时没有 twice 的未解决引用,可能不抽取成员,最终报 undefined reference。恢复顺序即可;为解决这个顺序问题而无条件使用 --whole-archive 会扩大输入范围,而且它本身不保证所有节免于后续 GC。GNU ld 归档与节回收选项
两次选择:先让对象成员进入,再决定保留哪些节
caller.o
未解决引用:twice
提取 twice.o
twice + unused_helper
--gc-sections
只留下可达的 twice
-ffunction-sections 提供细粒度边界;根与引用图决定保留结果。
没有抽取进来的归档成员,不会仅因其内部存在某个节就自动参与后续布局。
抽取一个 .o,为什么还能只留下其中一个函数?
把“已经进入链接输入”和“最终活下来”画成不同集合,可以同时解释库顺序与节回收。传统归档扫描时若还没有 twice 的未解决引用,就可能不抽取成员;强制抽取后,unused 所在的独立节仍可能在 GC 中删除。这里固定 GNU ld.bfd 的教学边界,避免把结果直接套给 LLD 等不同实现。
正在呈现知识画面 重新加载
抽取一个 .o,为什么还能只留下其中一个函数? 归档抽取按成员,节回收按根可达引用图。whole-archive 强制成员进入输入,也不等于所有函数节必须保留;传统归档顺序错误则可能根本没抽到定义。
{"id":"archive-and-section-reachability","title":"抽取一个 .o,为什么还能只留下其中一个函数?","summary":"归档抽取按成员,节回收按根可达引用图。whole-archive 强制成员进入输入,也不等于所有函数节必须保留;传统归档顺序错误则可能根本没抽到定义。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"order\" checked><span data-i18n=\"order\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"whole\"><span data-i18n=\"whole\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"gc\" checked><span data-i18n=\"gc\"></span></label><svg id=\"refs\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\"></svg><section class=\"panel\"><h3 data-i18n=\"extracted\"></h3><div id=\"extracted\"></div></section><section class=\"panel\"><h3 data-i18n=\"kept\"></h3><div id=\"kept\"></div></section><section class=\"panel\"><h3 data-i18n=\"unresolved\"></h3><div id=\"unresolved\"></div></section><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#extracted,#kept,#unresolved{font:14px/1.8 var(--viz-font-mono);min-height:3.6em;overflow-wrap:anywhere}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function draw(){const extracted=$('#order').checked||$('#whole').checked,gc=$('#gc').checked;set('extracted',extracted?'caller.o, twice.o':'caller.o');set('unresolved',extracted?'∅':'twice');set('kept',!extracted?'link error':gc?'main, twice':'main, twice, unused');graph('refs',[{name:'main',x:55,y:85,cls:'on'},{name:'twice',x:180,y:85,cls:extracted?'on':'gold'},{name:'unused',x:305,y:85,cls:extracted&&!gc?'on':''}],[[0,1,true]]);$('#kept').dataset.count=extracted?(gc?2:3):0;}document.querySelectorAll('input').forEach(e=>e.onchange=draw);draw();","audio":false,"strings":{"extracted":"已抽取成员","gc":"启用函数节 GC","heading":"抽取一个 .o,为什么还能只留下其中一个函数?","intro":"caller.o 引用 twice,归档成员同时定义 twice 与 unused。分别切换输入顺序、whole-archive 和节回收,观察三个集合。","kept":"最终保留函数","order":"caller.o 放在归档之前","reset":"重置","scope":"教学模拟 GNU ld.bfd 的传统单次归档处理;每函数独立节,根为 main,未启用 LTO、KEEP、额外导出或库分组。不是所有链接器规则。","scopeLabel":"模型范围与约定","unresolved":"未解决引用","whole":"whole-archive:强制抽取"}}
GOT 与 PLT:间接访问和解析时机分开看
GOT 提供地址槽,PLT 提供调用桩,是某些位置无关与动态调用路径的一部分。在经典惰性函数绑定中,第一次调用经过解析器查找目标并更新相应槽,后续调用利用已经保存的地址。
立即绑定把相关解析提前到加载阶段,例如 Linux 动态链接器可通过 LD_BIND_NOW 控制。它不会仅因解析提前,就自动把原来的 PLT 调用指令改成无间接开销的直接调用。是否使用 PLT、能否放松,以及是否通过 GOT 调用,还取决于编译与链接选择。Linux ld.so
动态符号的 interposition 也受查找作用域、绑定与可见性影响。它给替换实现和预加载带来灵活性,同时可能限制内联或本地绑定假设;不能断言所有 malloc 调用都必然经过完全相同的路径。
加载与 dlopen
动态 ELF 启动:映射与重定位之后才进入程序逻辑
内核读取可执行文件的程序头
按可加载段建立地址空间,识别解释器
1
动态链接器处理依赖与必要重定位
解析符号、TLS 等,按策略处理绑定
2
执行运行时初始化
共享库初始化与语言启动逻辑
3
程序入口继续到 main 等语言入口
main 不是内核直接跳入的唯一入口概念
4
图示范围为常见 Linux 动态 ELF;静态、其他格式与平台流程不同。
dlopen 在进程运行中加载共享对象,dlsym 查找符号,dlclose 减少加载引用等。实际插件接口还要定义 ABI、所有权、线程与卸载条件,并检查错误;不能在仍有函数指针或线程执行库代码时随意卸载。dlopen 手册
动态加载现成库与 JIT 发布机器码不同。JIT 可以在内存中处理对象、重定位和可执行权限,不要求统一走“写 .o、链接 .so、dlopen”的文件流程。见 JIT 编译 。
LTO:让优化看到跨编译单元信息
普通独立编译限制优化器能看到的函数体。LTO 保留 IR 或相关中间信息,在链接阶段结合符号解析结果进行跨模块分析和优化,然后生成机器码继续链接。即使没有 LTO,链接器仍执行重定位、节回收和其他链接优化,不能称作只会拼接。
Full LTO 与 ThinLTO 的组织方式不同。ThinLTO 结合模块摘要进行全局分析与有选择的函数导入,再并行执行模块后端,还可使用缓存。Clang 以 -flto=thin 等配置显式选择该模式;它不是所有 gold/LLD 调用天然的默认 LTO 方式。Clang ThinLTO 文档
是否内联由成本模型与可见性等约束决定,不限于“只导入热点调用”。LTO 也未必知道所有动态插件和外部定义,所谓全程序边界需要结合链接与符号假设。性能收益、内存和链接耗时应在实际项目测量,不给统一提升百分比。
定位问题速查
现象 优先检查
undefined reference 是否生成定义、库顺序、名称修饰、ABI 与可见性
relocation overflow 重定位宽度、布局与代码模型
启动找不到共享库 依赖记录、搜索路径与平台加载规则
同名符号绑定到意外实现 查找作用域、版本、预加载与可见性
LTO 无预期效果 编译和链接配置、IR 可用性与优化边界
参考
Keywords: ELF, symbol resolution, relocation, PC32, PLT32, archive, GOT, PLT, lazy binding, dlopen, LTO, ThinLTO