动态链接将部分地址选择推迟到加载或调用时。程序记录依赖与符号需求,加载器找到对象、建立映射并完成重定位;这样库可以在保持 ABI 的前提下独立更新。同一个库文件的干净代码页可被多个进程共享,私有数据与写时复制页则不能一概算作共享。
排查失败时应依次区分:库文件是否被找到,目标符号是否在正确作用域内,符号版本是否匹配,最后才是调用时的参数与对象布局是否兼容。文件名相同不等于 ABI 相同。
找库、找符号、写地址是三步
DT_NEEDED 记录依赖名。找到库后,加载器根据动态表定位符号、字符串、版本和重定位信息。重定位告诉加载器“哪处需要怎样计算和写入”,而符号查找决定某个名字在当前作用域中对应哪个定义。
所以加载了 libfoo.so 不等于所有 foo 引用必然绑定到它;可插入的全局符号可能被其他对象的定义满足。符号也不只是函数,数据、TLS 与 IFUNC 等有各自的处理要求。ELF 动态链接
PLT/GOT 为什么支持延迟绑定
传统 x86-64 PLT 桩先通过 GOT 槽位跳转。未解析时它引向加载器的解析路径,解析器确定函数地址并通常更新槽位;以后调用复用这个结果。具体保留槽位数量、指令形式与重定位索引编码是架构实现细节,不应当作所有 ELF 的通用布局。
延迟绑定主要针对适用的函数调用重定位,数据引用不能任意等到第一次读取才补地址。-z now 或 LD_BIND_NOW 请求提前绑定;-fno-plt 等编译选项也可改变调用形式。因此“动态链接的代价就是每个函数第一次慢一点”并不完整。
GOT 也不只是“全局变量地址数组”。不同目标和重定位类型会把函数地址、数据地址及运行支持信息放在相应位置。errno 又是线程局部对象,不能拿 extern int errno 作为普通全局变量的教学例子。
第一次调用改变了哪个槽位?
先连续调用 foo,再调用 bar:第二次 foo 复用自己的解析结果,bar 的第一次调用仍可能进入解析器。画面把“当前槽位”与“最近一次调用路径”分开,避免把缓存状态误读成每次调用都会重复执行的流程。
正在呈现知识画面
第一次调用改变了哪个槽位?延迟绑定按适用的重定位槽位解析。第一次调用更新对应目标,其余未调用符号可以仍未解析;即时绑定提前完成这些工作。
{"id":"plt-slot-binding","title":"第一次调用改变了哪个槽位?","summary":"延迟绑定按适用的重定位槽位解析。第一次调用更新对应目标,其余未调用符号可以仍未解析;即时绑定提前完成这些工作。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"policy\"><button data-mode=\"lazy\" data-i18n=\"lazy\"></button><button data-mode=\"now\" data-i18n=\"now\"></button></div><div class=\"actions\"><button id=\"foo\" data-i18n=\"foo\"></button><button id=\"bar\" data-i18n=\"bar\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"slots\"></h3><div id=\"slots\"></div></section><div class=\"route\" id=\"path\"></div><div class=\"metrics\"><div><span data-i18n=\"lookups\"></span><strong id=\"lookups\"></strong></div><div><span data-i18n=\"calls\"></span><strong id=\"calls\"></strong></div></div><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#slots .tr{grid-template-columns:1fr 2fr}.route{display:flex;align-items:center;justify-content:center;gap:6px;min-height:100px;font-size:14px;overflow-wrap:anywhere}.route span{padding:12px 6px;border:1px solid var(--rule);border-radius:6px;text-align:center;min-width:0}","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;}));}let policy='lazy',resolved={},lookups=0,calls=0,last=null;function reset(){resolved={foo:policy==='now',bar:policy==='now'};lookups=policy==='now'?2:0;calls=0;last=null;draw()}function draw(){pressed(policy);table('slots',['foo','bar'].map(n=>[n,resolved[n]?'lib.so::'+n:t('resolver')]));set('lookups',lookups);set('calls',calls);$('#path').innerHTML=last?['call '+last.n,'PLT',...(last.lookup?[t('resolver')]:[]),'lib.so::'+last.n].map(s=>'<span>'+esc(s)+'</span>').join('→'):esc(t('none'));$('#slots').dataset.resolved=JSON.stringify(resolved);}for(const n of ['foo','bar'])$('#'+n).onclick=()=>{last={n,lookup:!resolved[n]};if(!resolved[n]){lookups++;resolved[n]=true;}calls++;draw()};document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{policy=e.dataset.mode;reset()});$('#reset').onclick=reset;reset();","audio":false,"strings":{"bar":"调用 bar","calls":"调用次数","foo":"调用 foo","heading":"第一次调用改变了哪个槽位?","intro":"分别调用 foo 与 bar,观察它们独立的 GOT 槽位。","lazy":"延迟","lookups":"解析次数","none":"尚未调用","now":"即时","path":"最近调用路径","policy":"绑定方式","reset":"重置","resolver":"解析器","scope":"传统 PLT/GOT 的两个普通函数,无 IFUNC、审计、插入与并发解析。切换绑定方式重新加载模型,不改变运行中对象策略。","scopeLabel":"模型范围与约定","slots":"GOT 槽位"}}
构建一个真正可运行的共享库实验
libfoo.c:
int foo(int x) { return x + 1; }
main.c:
#include <stdio.h>
extern int foo(int);
int main(void)
{
printf("%d\n", foo(41));
return 0;
}
cc -fPIC -shared libfoo.c -Wl,-soname,libfoo.so.1 -o libfoo.so.1
ln -sfn libfoo.so.1 libfoo.so
cc main.c -L. -lfoo -Wl,--enable-new-dtags,-rpath,'$ORIGIN' -Wl,-z,lazy -o dyn-demo
./dyn-demo
readelf -dW dyn-demo
objdump -R dyn-demo
LD_DEBUG=libs,bindings ./dyn-demo
LD_BIND_NOW=1 LD_DEBUG=bindings ./dyn-demo
实验应打印 42。链接时 -lfoo 找到 libfoo.so,库的 SONAME 使可执行文件通常记录 libfoo.so.1;RUNPATH 中的 $ORIGIN 让加载器在主程序所在目录找到它。单引号防止 shell 提前展开 $ORIGIN。LD_DEBUG 中还会出现 libc 等绑定,需要按 foo 定位自己的调用。
搜索路径:RUNPATH 不自动传给孙依赖
下面限定为 glibc 动态加载器。对于不含斜杠的依赖名,概括的搜索顺序是:
- 适用的 DT_RPATH,前提是没有对应的 DT_RUNPATH。
- LD_LIBRARY_PATH,安全执行模式下会被忽略。
- 发起依赖对象的 DT_RUNPATH,用于其直接 DT_NEEDED 依赖。
- /etc/ld.so.cache,包含系统缓存与相关硬件能力目录选择。
- 默认系统库目录,路径和是否跳过受架构及链接选项影响。
包含斜杠的依赖按路径处理,不走同样的名字搜索。RPATH 与 RUNPATH 的关键差别之一是传播范围:主程序的 RUNPATH 能找到 libA,不代表也能找到 libA 所需的 libB;libA 通常需要自己的合适搜索配置。$ORIGIN 按包含该标记的对象位置展开,不是当前工作目录。ld.so(8)
构建期的 -L 不会自动写成运行期路径。部署时复制了库却出现找不到文件,先检查 NEEDED、RUNPATH 和实际文件名,再考虑符号问题。
用两级依赖复现“主程序能找 A,却找不到 B”
在一个新的实验目录中建立 bin 与 lib。这里刻意让两个库位于同一目录,但第一版 A 不设置 RUNPATH。三个源文件分别如下。
chain-b.c:
int b(void) { return 40; }
chain-a.c:
extern int b(void);
int a(void) { return b() + 2; }
chain-main.c:
#include <stdio.h>
extern int a(void);
int main(void) { printf("%d\n", a()); return 0; }
mkdir -p bin lib
cc -fPIC -shared chain-b.c -Wl,-soname,libB.so.1 -o lib/libB.so.1
cc -fPIC -shared chain-a.c -Llib -l:libB.so.1 \
-Wl,-soname,libA.so.1 -o lib/libA.so.1
cc chain-main.c -Llib -l:libA.so.1 -Wl,-rpath-link,lib \
-Wl,--enable-new-dtags,-rpath,'$ORIGIN/../lib' -o bin/app
readelf -dW bin/app
readelf -dW lib/libA.so.1
./bin/app
在没有额外 LD_LIBRARY_PATH、预加载或系统同名库命中的干净环境中,最后一步应报告找不到 libB.so.1。app 的 RUNPATH 已经让它找到 A,但 B 是 A 的直接依赖。链接时的 -rpath-link,lib 只是帮助链接器找到间接依赖,不会把这个目录写成 A 的运行路径。GNU ld 路径选项
只重建 A,为它添加自己的 RUNPATH:
cc -fPIC -shared chain-a.c -Llib -l:libB.so.1 \
-Wl,-soname,libA.so.1,--enable-new-dtags,-rpath,'$ORIGIN' \
-o lib/libA.so.1
./bin/app
此时应输出 42。app 不必重新链接,因为它记录的仍是 libA.so.1。A 内的 $ORIGIN 展开为 lib 所在目录;即使从其他工作目录执行 app,这个相对对象位置的关系仍然成立。相对路径 dlopen("./plugin.so", ...) 则由当前工作目录解释,不能与 $ORIGIN 混用概念。
| 检查位置 | 第一版 | 修复版 |
| app 的 NEEDED | libA.so.1 | 不变 |
| app 的 RUNPATH | $ORIGIN/../lib | 不变 |
| A 的 NEEDED | libB.so.1 | 不变 |
| A 的 RUNPATH | 没有 | $ORIGIN |
| 执行结果 | B 找不到时启动失败 | 本例打印 42 |
把目录统一塞入 LD_LIBRARY_PATH 也可能让本例成功,但那会改变进程的搜索环境,不能据此说明 A 已具备独立部署时需要的路径信息。排查时把环境值与对象内的动态标签分别记录。
谁需要这个库,谁提供搜索路径?
搜索路径附着在发起依赖的对象上。图中两条边分别由 app 和 A 发起,勾选 A 的路径才能补上第二条边;把工作目录改到其他位置并不会改变这里的 $ORIGIN 展开。
正在呈现知识画面
谁需要这个库,谁提供搜索路径?RUNPATH 只用于该对象的直接依赖。主程序找到 A 不等于 A 能找到 B;$ORIGIN 以持有标签的对象目录展开。
{"id":"runpath-direct-dependency","title":"谁需要这个库,谁提供搜索路径?","summary":"RUNPATH 只用于该对象的直接依赖。主程序找到 A 不等于 A 能找到 B;$ORIGIN 以持有标签的对象目录展开。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"app\" checked><span data-i18n=\"app\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"lib\"><span data-i18n=\"lib\"></span></label><svg id=\"graph\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\" data-i18n-label=\"heading\"></svg><section class=\"panel\"><h3 data-i18n=\"search\"></h3><div id=\"search\"></div></section><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div></div><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#search .tr{grid-template-columns:1fr 1.6fr 1fr}#search .tr>*{min-height:8em}#result{min-height:5em}.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 a=$('#app').checked,b=$('#lib').checked;graph('graph',[{name:'app',x:45,y:90},{name:'A',x:180,y:90,cls:a?'on':''},{name:'B',x:315,y:90,cls:a&&b?'on':''}],[[0,1,a],[1,2,a&&b]]);table('search',[['app → A',a?'/demo/bin/../lib':'∅',t(a?'found':'missing')],['A → B',!a?'—':b?'/demo/lib':'∅',t(!a?'unreached':b?'found':'missing')]]);set('result',t(a&&b?'ready':'failed'));$('#result').dataset.ready=a&&b;}document.querySelectorAll('input').forEach(e=>e.onchange=draw);draw();","audio":false,"strings":{"app":"app:$ORIGIN/../lib","failed":"缺少依赖","found":"找到","heading":"谁需要这个库,谁提供搜索路径?","intro":"独立设置 app 和 libA 的 RUNPATH,观察 A→B 的查找。","lib":"libA:$ORIGIN","missing":"找不到","ready":"可继续启动","reset":"重置","result":"启动结果","scope":"app 位于 /demo/bin,A/B 位于 /demo/lib;依赖名无斜杠,忽略环境、RPATH、缓存与系统目录中的同名库。","scopeLabel":"模型范围与约定","search":"依赖边与搜索来源","unreached":"未开始查找"}}
LD_PRELOAD 的作用是符号插入
创建 override.c:
int foo(int x) { return x + 100; }
cc -fPIC -shared override.c -o override.so
LD_PRELOAD="$PWD/override.so" ./dyn-demo
在这个默认可插入符号的例子中应打印 141。它不意味着能覆盖任意代码:静态绑定、隐藏可见性、库内直接调用、编译器内联或某些链接优化,都可能让调用不经过可插入的动态查找。安全执行模式也会限制预加载。
用 LD_PRELOAD 包装 open 可以做观测或适配,但不能据此实现完整隔离:程序可能调用其他接口或直接执行系统调用。理解这里的边界有助于判断“预加载没生效”究竟是搜索问题还是调用路径问题。
dlopen:把对象生命周期也纳入接口
下面继续加载刚才的 libfoo.so.1。POSIX 的 dlsym 结果可用于相应函数指针;这是 POSIX 环境约定,不能推广为任意 ISO C 平台的对象/函数指针转换规则。
#include <dlfcn.h>
#include <stdio.h>
int main(void)
{
void *handle = dlopen("./libfoo.so.1", RTLD_NOW | RTLD_LOCAL);
if (handle == NULL) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}
dlerror();
int (*foo)(int) = (int (*)(int))dlsym(handle, "foo");
const char *error = dlerror();
if (error != NULL) {
fprintf(stderr, "%s\n", error);
dlclose(handle);
return 1;
}
printf("%d\n", foo(41));
if (dlclose(handle) != 0) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}
return 0;
}
编译:cc -Wall -Wextra plugin-host.c -ldl -o plugin-host。运行应打印 42。
RTLD_NOW 提前解析需要的未定义符号,RTLD_LAZY 仅延迟适用的函数绑定;调用者必须选择相应模式,不能笼统说 LAZY 是所有调用的默认值。RTLD_LOCAL 限制符号进入全局作用域,不表示只有这一个 handle 才能看到对象。
先清除旧 dlerror,再在 dlsym 后检查新的错误。仅检查返回指针是否为 NULL 不足以覆盖所有符号情况。dlclose 会减少引用,但不保证立刻卸载;无论是否实际卸载,都不能在生命周期失去保障后继续使用缓存函数指针。dlopen(3)、dlsym(3)
卸载前需要收回的不只有一个函数指针
假设插件创建了一个对象并返回给宿主;对象的析构函数、虚函数表或回调仍指向插件代码。即便宿主已经不调用最初的 foo,析构这个对象时仍可能跳回插件。因此接口最好成对提供 create/destroy,让分配与释放责任明确,并要求对象在关闭最后一个有效库引用前销毁。
可将关闭过程理解为有前置条件的状态变化:
| 状态 | 允许的操作 | 进入下一状态的条件 |
| 接受工作 | 创建对象、提交调用、注册回调 | 停止入口接收新工作 |
| 排空 | 等待已有调用和任务结束 | 插件不再运行,异步回调已撤销且不在执行 |
| 回收 | 调用插件自己的销毁接口 | 所有需要插件代码的数据与回调已释放 |
| 关闭 handle | dlclose,移除宿主缓存入口 | 后续没有任何路径再进入插件 |
这些步骤需要宿主自己的同步协议;RTLD_LOCAL 只影响查找作用域,不会替宿主等待工作线程,也不构成内存隔离。若回调撤销 API 只保证“不再开始新的回调”,宿主还必须等待已经开始的回调返回。为对象保留有效库引用是一种常见设计,但仍要处理对象之间的环和插件退出时的任务收敛。
符号版本与 IFUNC
GNU 符号版本允许同一个名字关联不同版本;foo@@VERS_2.0 通常表示默认版本,foo@VERS_1.0 表示非默认旧版本。调用者记录的版本需求必须被提供,改一个 SONAME 或建软链接不会修复参数、结构布局或版本不兼容。
IFUNC 让解析器选择实际实现,例如根据可用 CPU 特性选择函数。解析时机可能很早,也可能与具体绑定路径相关;解析器不能假定普通应用初始化已经完成。阅读启动异常时,IFUNC 是需要检查的早期执行路径之一。
ABI 兼容需要比“能找到符号”多检查一层
设插件入口仍叫 process,但旧版接收两个整数,新版接收一个结构体指针。加载器可以成功找到这个名字,调用却不符合约定;动态符号表通常不会替你验证 C 函数的完整参数类型。
| 层次 | 例子 | 能通过这一层仍不能证明什么 |
| 文件身份 | SONAME 满足 libfoo.so.1 | 内容实现了原 ABI |
| 名字与版本 | foo、所需版本节点存在 | 参数、返回值和对象布局符合宿主预期 |
| 调用契约 | 固定宽度字段、尺寸、调用约定 | 所有权和并发使用正确 |
| 生命周期契约 | create/destroy 配对、关闭前排空 | 新旧实现业务语义完全相同 |
自定义插件 ABI 可在入口返回的接口表中包含版本号和表大小,以便宿主在读取可选尾部字段前检查是否存在。这个设计要求双方先约定稳定的初始布局和兼容策略;随意给旧结构中间插字段,再提高版本号,并不能让旧宿主自动安全运行。
快速定位
| 现象 | 首先检查 |
| cannot open shared object file | NEEDED、SONAME、路径、目标架构 |
| undefined symbol | 动态符号导出、依赖关系、查找作用域 |
| version not found | readelf -V 输出及部署库版本 |
| preload 无效果 | 可见性、直接调用、内联、安全执行模式 |
| dlclose 后偶发崩溃 | 对象、函数指针与工作线程的生命周期 |
readelf -dW、readelf --dyn-syms -W 和 readelf -rW 分别回答依赖、动态符号与重定位问题。普通 nm 依赖的符号表可能已被移除,查看动态符号可用 nm -D。本文实验以 glibc 为准,musl 的加载器实现和诊断选项应另按其文档核对。