链接器的两个相关任务是安排布局和修补引用:脚本决定输入 section 如何进入输出及位于哪里,重定位记录决定某个指令或数据字段应填什么。文件中的位置、运行时地址和加载时复制位置必须分开讨论。
重定位记录携带什么
ELF 重定位至少指定待修补位置、类型与相关符号;RELA 形式还显式保存 addend,REL 则由被修补字段提供隐式加数。具体使用哪种形式取决于目标 ABI,不能把所有平台都写成同一种 RELA。
设 S 为符号地址,A 为加数,P 为重定位字段位置。x86-64 的典型公式包括:
| 类型 | 主要计算 | 解释 |
| R_X86_64_64 | S + A | 写入 64 位绝对值 |
| R_X86_64_PC32 | S + A - P | 写入经范围检查的 32 位 PC 相对值 |
| R_X86_64_PLT32 | L + A - P | L 为 PLT 入口;满足条件时可直接绑定目标 |
| R_X86_64_GOTPCREL | GOT 条目地址 + A - P | 得到存放目标地址的条目位置 |
对一个 5 字节的相对 call,位移字段位于指令起点之后 1 字节,CPU 使用的是下一条指令地址。RELA 中常见 A=-4,正是把 P 调整到这个基准。例如指令在 0x1000、P=0x1001、目标在 0x1100,位移为 0x1100 - 4 - 0x1001 = 0xfb;执行时 0x1005 + 0xfb = 0x1100。完整链接例子见 链接与加载。
PLT32 不意味着最终一定经过 PLT:目标可直接绑定时,链接器可以解析为直接调用。动态链接也不是“所有未定引用都进入 .rela.plt”,数据、GOT、TLS 等会采用相应动态重定位,部分引用则在静态链接阶段已解决。AMD64 ABI
从 .o 中的占位到最终位移
下面实验针对 x86-64 ELF,使用 GNU 风格汇编。在独立目录保存 caller.s:
.text
.globl caller
caller:
call callee
ret
.section .note.GNU-stack,"",@progbits
再保存 callee.s:
.text
.globl callee
callee:
ret
.section .note.GNU-stack,"",@progbits
as --64 caller.s -o caller.o
as --64 callee.s -o callee.o
readelf -rW caller.o
objdump -dr caller.o
ld -m elf_x86_64 -Ttext=0x1000 -e caller caller.o callee.o -o layout.elf
nm -n layout.elf
objdump -d layout.elf
layout.elf 只用于观察布局,不要直接执行:caller 的 ret 不是一个合法进程启动/退出协议。输入 caller.o 的位移字段在 .text 偏移 1,常见记录为 R_X86_64_PLT32 callee - 4。此实验 callee 可直接绑定,按输入顺序紧接 caller 的 6 字节代码,最终地址为 0x1006。
最终 call 位移为 1,对应小端字节 01 00 00 00。读取 .o 时,偏移 1 是相对于该输入 section 的位置;最终链接后才能结合输出布局得到 P=0x1001。不能把输入重定位表的 offset 无条件当成进程虚拟地址。
如果最终目标距离超出对应带符号位移范围,链接器必须按目标机制处理或报错,不能把高位截掉“凑出一个可执行文件”。改变代码模型、布局或选择合适的间接序列才是有语义的修复。
A=-4 到底补偿了哪四个字节?
先用默认目标验证 0xfb,对应字段 fb 00 00 00;再把目标移到指令之前,观察负位移的二补码字节。最后只改变整个代码块基址:S 与 P 同时平移,位移不变。这个不变量比孤立记住一条公式更有助于理解位置无关引用。
D=S+A−P,A=−4,RIPnext=P+4,RIPnext+D=S
正在呈现知识画面
A=-4 到底补偿了哪四个字节?重定位位置 P 指向4字节位移字段,CPU 的 RIP 相对基准却在字段之后。A=-4 把二者接起来,整体平移不改变相对位移。
{"id":"relocation-pc32-bytes","title":"A=-4 到底补偿了哪四个字节?","summary":"重定位位置 P 指向4字节位移字段,CPU 的 RIP 相对基准却在字段之后。A=-4 把二者接起来,整体平移不改变相对位移。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"delta\" data-i18n=\"delta\"></label><output for=\"delta\" id=\"delta-value\"></output><input id=\"delta\" type=\"range\" min=\"-256\" max=\"256\" value=\"256\" step=\"1\"></div><div class=\"control\"><label for=\"base\" data-i18n=\"base\"></label><output for=\"base\" id=\"base-value\"></output><input id=\"base\" type=\"range\" min=\"1\" max=\"8\" value=\"1\" step=\"1\"></div><div class=\"track-row\"><span data-i18n=\"bytes\"></span><div id=\"bytes\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"place\"></span><strong id=\"place\"></strong></div><div><span data-i18n=\"next\"></span><strong id=\"next\"></strong></div><div><span data-i18n=\"target\"></span><strong id=\"target\"></strong></div><div><span data-i18n=\"disp\"></span><strong id=\"disp\"></strong></div></div><p class=\"formula\" id=\"equation\"></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.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.metrics strong{font-size:17px}","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).classList.add('table');$('#'+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 band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const b=+$('#base').value*4096,delta=+$('#delta').value,S=b+delta,P=b+1,D=S-4-P,h=n=>'0x'+n.toString(16),v=D>>>0;set('delta-value',delta);set('base-value',h(b));set('place',h(P));set('next',h(b+5));set('target',h(S));set('disp',D);cells('bytes',[{text:'E8',cls:'gold'},...Array.from({length:4},(_,i)=>({text:((v>>>(8*i))&255).toString(16).padStart(2,'0'),cls:'on'}))]);set('equation','D = S + A − P = '+h(S)+' − 4 − '+h(P)+' = '+D+'\\nRIP_next + D = '+h(b+5)+' + ('+D+') = '+h(S));$('#bytes').dataset.hex=Array.from({length:4},(_,i)=>((v>>>(8*i))&255).toString(16).padStart(2,'0')).join('');}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"base":"指令基址(页)","bytes":"机器码:E8 + rel32","delta":"目标距指令起点(B)","disp":"有符号位移 D","heading":"A=-4 到底补偿了哪四个字节?","intro":"改变目标距离与整个代码块的基址,看位移字段和 CPU 落点。","next":"下一条指令 RIP","place":"重定位位置 P","reset":"重置","scope":"x86-64 E8 rel32,直接可绑定目标,距离限制在安全32位范围。展示小端字段,不代表任意 call 都使用该编码。","scopeLabel":"模型范围与约定","target":"符号 S"}}
AArch64:页地址与页内偏移配合
AArch64 的 ADRP 用页粒度构造地址,随后 ADD 或加载指令处理页内部分。R_AARCH64_ADR_PREL_PG_HI21 对应 ADRP 的页相对计算,不是把 ADR 和 ADRP 混作同一个指令。
R_AARCH64_ADD_ABS_LO12_NC 提供低 12 位;R_AARCH64_LDST64_ABS_LO12_NC 对应 64 位加载或存储的缩放立即数,不能忽略对齐与缩放。R_AARCH64_ADR_GOT_PAGE 只负责 GOT 页相关部分,通常还需配套加载的低位重定位,不是单独一个记录就完成“页地址加加载”。Arm ELF ABI
跨过页边界,ADRP 与 ADD 如何分工?
默认目标是0x1fff,低12位为0xfff;向右移动一个字节到0x2000,低位变为0,ADRP 负责的页基址增加0x1000。这里的“页”是 ADRP 固定的地址运算粒度,即使系统采用其他页大小,也不能把公式里的12位换掉。
Page(x)=⌊4096x⌋4096,S=Page(P)+[Page(S)−Page(P)]+(Smod4096)
正在呈现知识画面
跨过页边界,ADRP 与 ADD 如何分工?ADRP 计算目标页与指令所在页的差,ADD 补齐目标低12位。跨边界时低位归零、页差增加,最终地址仍连续。
{"id":"aarch64-page-relocation","title":"跨过页边界,ADRP 与 ADD 如何分工?","summary":"ADRP 计算目标页与指令所在页的差,ADD 补齐目标低12位。跨边界时低位归零、页差增加,最终地址仍连续。","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=\"4096\" max=\"16384\" value=\"8191\" step=\"1\"></div><div class=\"control\"><label for=\"p\" data-i18n=\"p\"></label><output for=\"p\" id=\"p-value\"></output><input id=\"p\" type=\"range\" min=\"4096\" max=\"16384\" value=\"4608\" step=\"256\"></div><div class=\"track-row\"><span data-i18n=\"map\"></span><div id=\"map\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"page\"></span><strong id=\"page\"></strong></div><div><span data-i18n=\"low\"></span><strong id=\"low\"></strong></div><div><span data-i18n=\"base\"></span><strong id=\"base\"></strong></div><div><span data-i18n=\"final\"></span><strong id=\"final\"></strong></div></div><p class=\"formula\" id=\"equation\"></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.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#map{height:40px;position:relative;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0}#map i{border:1px solid var(--rule);background:var(--surface)}#map b{position:absolute;top:0;bottom:0;width:2px;background:var(--accent)}.metrics strong{font-size:17px}","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).classList.add('table');$('#'+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 band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const s=+$('#target').value,p=+$('#p').value,sp=Math.floor(s/4096)*4096,pp=Math.floor(p/4096)*4096,lo=s%4096,h=n=>'0x'+n.toString(16);set('target-value',h(s));set('p-value',h(p));set('page',(sp-pp)/4096);set('low',h(lo));set('base',h(sp));set('final',h(sp+lo));$('#map').innerHTML='<i></i><i></i><i></i><b style=\"left:calc('+(s-4096)/12288*100+'% - 1px)\"></b>';set('equation','ADRP = Page(P) + [Page(S) − Page(P)] = '+h(sp)+'\\nADD = '+h(sp)+' + '+h(lo)+' = '+h(s));}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"base":"ADRP 得到的地址","final":"ADD 后地址","heading":"跨过页边界,ADRP 与 ADD 如何分工?","intro":"把目标在页边界两侧移动,分别观察页差和低12位。","low":"ADD 的低12位","map":"目标在固定页中的位置","p":"指令位置 P","page":"页相对差(4 KiB页)","reset":"重置","scope":"AArch64 ADRP+ADD,Page(x)=x&~0xfff 是指令定义的4 KiB粒度,不随操作系统页大小改变;只算地址,不编码指令立即数字段。","scopeLabel":"模型范围与约定","target":"目标地址 S"}}
section 与 segment 分工
输出 section 名称本身不决定运行时页权限;程序头、属性、页对齐及链接器布局共同形成加载视图。检查布局时同时看 readelf -SW 与 readelf -lW,不要只看到 .text、.data 就认为读写执行权限一定正确。
位置计数器 . 表示当前输出位置,ALIGN 调整对齐,通配模式收集输入 section。*(.text) 不会自动包含 .text.foo,启用每函数 section 时通常还需 .text.*。
完整默认脚本还处理启动数组、异常展开、TLS、动态信息等。不要用仅含 .text/.data/.bss 的教学脚本直接替换普通 Linux 程序默认布局。先运行 ld --verbose 查看当前目标的默认脚本,不能假定固定安装路径。GNU ld 脚本
给默认布局增加一个自定义区段
下面的 GNU ld 扩展脚本保留默认布局,只插入应用元信息。保存为 extra.ld:
SECTIONS
{
.appmeta : ALIGN(8)
{
__appmeta_start = .;
KEEP(*(.appmeta .appmeta.*))
__appmeta_end = .;
}
}
INSERT AFTER .rodata;
KEEP 防止匹配 section 被链接垃圾收集删除,但不能保证编译器一定生成原始变量,所以源代码也用 used 属性。保存为 meta.c:
__attribute__((section(".appmeta"), used))
const unsigned int app_version = 7;
int main(void) { return 0; }
gcc -O2 -ffunction-sections -fdata-sections meta.c \
-Wl,--gc-sections,-T,extra.ld,-Map,meta.map -o meta
readelf -SW meta
readelf -lW meta
readelf -x .appmeta meta
INSERT 使这个脚本增强默认 SECTIONS,而不是普通 -T 替换完整默认脚本的行为。两个导出符号描述地址,不会自动分配 C 变量存储;从 C 使用它们时应按链接器符号约定声明和取址。GNU ld INSERT
这份示例适用于所述 GNU ld 用法;换链接器需核对支持细节和产物。LLD 兼容很多 GNU 脚本行为,但有明确的实现差异。LLD 脚本说明
三道不同关卡:生成变量、提取归档、保留 section
used、归档提取和 KEEP 发生在不同阶段。used 要求编译器保留定义,KEEP 保护已进入链接处理的匹配 section;如果定义藏在静态归档里,那个成员尚未被提取,KEEP 不能替代提取条件。
为此另外准备 registry.c,只包含元信息定义;准备 entry.c 作为应用入口:
__attribute__((section(".appmeta"), used))
const unsigned int app_version = 7;
int main(void) { return 0; }
gcc -O2 -fdata-sections -c registry.c -o registry.o
ar rcs libregistry.a registry.o
gcc entry.c -L. -lregistry -Wl,--gc-sections,-T,extra.ld -o not-extracted
gcc entry.c -Wl,-u,app_version -L. -lregistry \
-Wl,--gc-sections,-T,extra.ld -o extracted
nm not-extracted
nm extracted
readelf -x .appmeta extracted
第一份应用没有对 app_version 的未定义引用,归档成员通常不会被提取。第二份用 -u 引入符号需求,从归档提取 registry.o,再由 KEEP 保留元信息区段。大型插件注册表也会遇到这类问题;--whole-archive 可以扩大提取范围,但应及时用 --no-whole-archive 限定范围,避免把无关归档全部拉入。GNU ld 选项
还可以把 extra.ld 复制成 drop.ld,仅移除 KEEP 包装,直接链接 registry.o 与 entry.c 并开启 --gc-sections。此时编译器确实生成了变量、对象也已输入链接,但没有可达引用的 .appmeta 仍可被垃圾收集;这第三种情况不能用 used 修复。
VMA 与 LMA:运行位置不一定等于装载来源
嵌入式固件可能把初始化数据存于 Flash,启动时复制到 RAM。该数据 section 的 VMA 是运行时地址,LMA 是初始内容的装载地址,可通过 AT 或 AT> 指定关系。脚本只描述布局,启动代码仍需执行复制和 .bss 清零。
Linux ELF 用户程序主要按程序头完成映射,不应把裸机 Flash/RAM 复制模型直接套到动态加载器。NOLOAD、SHT_NOBITS、文件大小和内存大小也应按工具与加载模型分别核对。GNU ld:输出 section LMA
用地址账理解初始化复制
假设一段 .data 的运行地址 VMA 为 0x20000000,初始内容在 Flash 的 LMA 为 0x08004000,大小 16 字节。上电后访问变量使用前一个地址;启动代码应把后一个地址开始的 16 字节复制过去。若只把 VMA 改成 RAM,却没有相应初始内容和复制动作,变量不会自动得到编译时初值。
| 对象 | 镜像中是否需要初始字节 | 启动阶段动作 |
| 已初始化 .data | 需要 | 从装载来源复制到运行位置 |
| 零初始化 .bss | 常用 NOBITS 表示,不保存同等大小的零字节 | 按目标运行模型清零 |
| .text / 只读常量 | 需要 | 就地执行/读取,或按平台需要复制 |
脚本可以用 LOADADDR、ADDR、SIZEOF 导出这些边界,启动代码再按目标约定使用。对普通 Linux ELF,则应查看 PT_LOAD 的 p_filesz 和 p_memsz 如何定义文件与内存范围;并不是动态加载器普遍执行这张 Flash/RAM 复制表。输出 section 的装载地址
Relaxation:已知最终布局后改写指令序列
链接放松利用最终距离、符号绑定和重定位类型,将某些序列替换成等价且更合适的序列。例如目标能直接绑定时,x86-64 的某些 GOT 访问可以放松成直接地址计算;RISC-V 远调用序列可在距离合适时缩短。
x86 指令是变长编码,并非“固定大小所以没有 relaxation”。能否放松取决于目标 ABI、重定位标记、链接器支持和选项;指令数量减少也不保证整个程序一定更快。
排查顺序
readelf -rW file.o
objdump -dr file.o
nm -u file.o
gcc file.o other.o -Wl,-Map,output.map,--trace -o prog
先确认引用哪个符号、哪个文件提供定义,再检查输入顺序、可见性、版本、归档提取和布局。重定位溢出通常意味着编码范围或代码模型不满足,不应随意截断结果。符号已解析但运行时错位,还需核对 ABI、PIE、加载映射和动态重定位。
选择 GNU ld、LLD 或其他链接器时,应基于目标平台、脚本、LTO 格式、诊断和实际链接资源测量,避免固定“快几倍”的结论。
关键词:linker script, relocation, REL, RELA, GOT, PLT, section, segment, VMA, LMA, KEEP, relaxation, LLD