main 是 C 运行时调用的应用入口,内核认识的是 ELF 入口地址。通常链接进程序的启动对象定义 _start,但名字本身没有魔法:链接器可以指定其他入口,不使用 libc 的程序也可以自行实现启动。
execve 成功时替换当前进程的程序映像,不创建一个新 PID,也不会返回到旧程序下一条指令。理解启动路径,首先要分清内核建立映像和用户态运行初始化代码这两个阶段。execve(2)
内核把控制权交给谁
内核依据 PT_LOAD 建立主映像的映射,设置初始栈并提供参数、环境与 auxv。若存在 PT_INTERP,还加载指定的 ELF 解释器并首先进入解释器入口;否则从主映像的入口开始。入口运行时地址还要结合加载偏移,不能把 e_entry 当作文件偏移。
动态解释器通常是 glibc 的 ld-linux。它自身需要完成受限的自举及相应重定位,随后处理主程序依赖、TLS、符号解析和初始化等。它是特殊启动对象,不能因为自身是 ET_DYN 就把它描述成“还得先依赖另一份 ld.so 才能运行”。
映射并不意味着把每一页都立即读入物理内存。首次访问仍可能触发缺页;main 前后看到的缺页和 I/O 不能只归因于源代码所在阶段。具体加载实现可对照 Linux v6.18 binfmt_elf.c。
exec 保留进程身份,却不保留旧 C 程序的现场
把 exec 理解成“调用另一个 main”会误判资源与控制流。旧地址空间里的堆对象、函数指针和 atexit 注册不再是新程序的运行状态;新程序从自己的启动路径开始,不会返回旧 main。Linux 对各类属性的处理并不相同。execve 的属性变化
| 状态 | 成功 exec 后 | 对程序设计的影响 |
| PID | 保持 | 同一进程身份可经历不同程序映像 |
| 旧代码、堆、栈与映射 | 被替换 | 旧地址不能当作新程序可用指针传递 |
| 文件描述符 | 通常保留,FD_CLOEXEC 的关闭 | 需要明确哪些句柄属于交接接口 |
| 信号处理函数 | 已捕获信号的 disposition 重置为默认 | 旧函数地址不能继续作处理入口 |
| 信号屏蔽字 | 保留 | 新程序可能继承被屏蔽的信号 |
| 其他线程 | 被销毁 | exec 不是让旧线程与新 main 并行 |
| 旧 atexit 注册 | 不保留,也不由 exec 执行 | 不能依靠它完成 exec 前的业务清理 |
下面用 execv 加载同一实验目录的目标程序,继承环境但重新指定 argv。保存为 exec-launch.c;成功时看不到“exec failed”,失败时旧程序通常仍能报告错误。Linux 极少数跨过映像替换关键点的失败不能恢复旧程序,不应把“失败必然返回”写成绝对保证。
#include <stdio.h>
#include <unistd.h>
int main(void) {
char *args[] = {"chosen-name", "hello", NULL};
printf("launcher pid=%ld\n", (long)getpid());
if (fflush(stdout) == EOF) return 1;
execv("./exec-target", args);
perror("exec failed");
return 1;
}
exec-target.c:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv) {
printf("target pid=%ld argc=%d argv0=%s arg1=%s\n",
(long)getpid(), argc, argc > 0 ? argv[0] : "<none>",
argc > 1 ? argv[1] : "<none>");
return 0;
}
cc -Wall -Wextra exec-launch.c -o exec-launch
cc -Wall -Wextra exec-target.c -o exec-target
./exec-launch
两行的 PID 应相同,目标收到 argc=2、argv0=chosen-name、arg1=hello。argv[0] 由调用方设置,并不可靠地证明可执行文件的实际路径。启动器先 fflush 是为了让旧 stdio 缓冲中的输出落到文件描述符上;exec 本身不会替旧 libc 刷新输出。
初始栈不是普通函数调用栈
_start 不是通过普通 C call 从 main 的调用者进入的,不存在一个可以随意 ret 返回的上层函数。启动代码应按目标 ABI 读取初始栈并满足调用 C 函数所需的栈对齐。
以原生 x86-64 glibc 为例,_start 准备 __libc_start_main 所需的参数:
| 参数 | System V AMD64 位置 | 来源与作用 |
| main | rdi | 应用 main 的地址 |
| argc | rsi | 初始栈中的参数数量 |
| argv | rdx | 初始栈中的参数指针数组 |
| init | rcx | 新版启动代码传 NULL,由 libc 查找相应初始化信息 |
| fini | r8 | 新版启动代码传 NULL,不是“第八个内部标志参数” |
| rtld_fini | r9 | 保存加载器传来的清理函数 |
| stack_end | 栈上传参 | 初始栈相关边界信息 |
因此,仅抄 pop argc 和对齐 rsp,却漏掉设置 main 地址与 rcx 的汇编,不能作为正确启动示例。glibc 的 x86-64 start.S 还包含 CFI、PIC 与不同数据模型的处理;直接查看目标版本比混合几代源码更可靠。glibc start.S
初始化工作分散在加载器和 libc 中
动态程序的 TLS 等基础条件需要在相应用户代码依赖它们之前就绪,不能把全部初始化都排进 __libc_start_main 的一份伪 C 函数。主程序与依赖库的初始化也不是一个无条件的“遍历所有库,再调用 init 指针”流程。
在当前 glibc 路径中,加载器处理相应依赖与 preinit 工作;libc 启动代码再处理主程序的 DT_INIT/DT_INIT_ARRAY 等。旧启动对象可能传入初始化函数,新启动对象的 init 参数则为 NULL。依赖循环和不同对象之间的构造函数顺序,不能靠文件名或链接命令表面顺序随意假设。glibc libc-start.c
.init/.fini 通常包含指令,而 .init_array/.fini_array 是函数指针数组。C++ 非局部对象初始化和编译器 constructor 属性可以通过这些机制进入 main 前的执行阶段,但 constructor 属性及优先级属于工具链扩展,不是 ISO C 的统一规则。
auxv:内核交给运行时的辅助信息
| 键 | 值的含义 |
| AT_PHDR、AT_PHENT、AT_PHNUM | 主程序 program header 的运行时位置、表项大小和数量 |
| AT_ENTRY | 主程序入口地址;即使先执行解释器,也仍描述主程序 |
| AT_BASE | ELF 解释器的加载基址,不能当成主程序基址 |
| AT_PAGESZ | 系统页大小 |
| AT_SYSINFO_EHDR | 可用时的 vDSO ELF 头地址 |
| AT_RANDOM | 指向 16 字节随机数据,可供运行时初始化保护值 |
| AT_SECURE | 加载器是否应按安全执行条件处理环境等 |
| AT_HWCAP、AT_HWCAP2 | 架构相关的用户态硬件能力位 |
AT_RANDOM 不是控制 ASLR 开关的字段。AT_SECURE 也不仅可由 setuid 一种原因触发。读取辅助向量应优先使用 getauxval;查找不到与合法零值可通过按接口要求检查 errno 区分。getauxval(3)
初始环境指针数组后确实有 auxv,但运行中的 environ 可能经过 setenv 等操作迁移,不应总沿当前 environ 尾部继续扫描内存。读取自身 /proc/self/auxv 或调用 getauxval 更适合运行时查询。
第一条指令与 AT_ENTRY 是同一个地址吗?
把“CPU 现在跳到哪里”和“auxv 描述哪个对象”分开看。改变解释器基址会改变第一条指令的位置,却不改变 AT_ENTRY;这正是动态程序中 starti 与 main 断点落在不同阶段的原因。
正在呈现知识画面
第一条指令与 AT_ENTRY 是同一个地址吗?有 PT_INTERP 时先进入解释器,AT_ENTRY 仍是主程序入口,AT_BASE 描述解释器加载基址。main 由用户态启动代码稍后调用。
{"id":"startup-entry-auxv","title":"第一条指令与 AT_ENTRY 是同一个地址吗?","summary":"有 PT_INTERP 时先进入解释器,AT_ENTRY 仍是主程序入口,AT_BASE 描述解释器加载基址。main 由用户态启动代码稍后调用。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"interp\" checked><span data-i18n=\"interp\"></span></label><div class=\"control\"><label for=\"mainbias\" data-i18n=\"mainbias\"></label><output for=\"mainbias\" id=\"mainbias-value\"></output><input id=\"mainbias\" type=\"range\" min=\"0\" max=\"8\" value=\"2\" step=\"1\"></div><div class=\"control\"><label for=\"ldbias\" data-i18n=\"ldbias\"></label><output for=\"ldbias\" id=\"ldbias-value\"></output><input id=\"ldbias\" type=\"range\" min=\"16\" max=\"24\" value=\"16\" step=\"1\"></div><div class=\"metrics\"><div><span data-i18n=\"first\"></span><strong id=\"first\"></strong></div><div><span data-i18n=\"entry\"></span><strong id=\"entry\"></strong></div><div><span data-i18n=\"base\"></span><strong id=\"base\"></strong></div><div><span data-i18n=\"owner\"></span><strong id=\"owner\"></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}#equation{white-space:pre-line;min-height:6em}","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 yes=$('#interp').checked,B=+$('#mainbias').value*4096,L=+$('#ldbias').value*4096,h=n=>'0x'+n.toString(16);set('mainbias-value',h(B));set('ldbias-value',h(L));$('#ldbias').disabled=!yes;set('entry',h(B+0x1200));set('base',h(yes?L:0));set('first',h(yes?L+0x800:B+0x1200));set('owner',t(yes?'ld':'main'));set('equation','AT_ENTRY = B + 0x1200 = '+h(B+0x1200)+'\\nPC = '+(yes?'AT_BASE + 0x800':'AT_ENTRY')+'\\nmain ≠ e_entry');}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"base":"AT_BASE","entry":"AT_ENTRY","first":"第一条用户指令","heading":"第一条指令与 AT_ENTRY 是同一个地址吗?","interp":"存在 PT_INTERP","intro":"选择有无 ELF 解释器,分别调整两个映像的加载偏移。","ld":"解释器入口","ldbias":"解释器加载偏移(页)","main":"主程序 _start","mainbias":"主程序加载偏移(页)","owner":"首先运行的对象","reset":"重置","scope":"抽象 ELF 布局,e_entry(main)=0x1200、e_entry(interpreter)=0x800,页大小4 KiB。无解释器不等于无 libc 初始化。","scopeLabel":"模型范围与约定"}}
观察 main 前后,而不是只画箭头
下面使用 GCC/Clang 的 constructor 与 destructor 扩展,观察一个主程序中的执行顺序:
#include <stdio.h>
#include <stdlib.h>
#include <sys/auxv.h>
__attribute__((constructor))
static void before_main(void) { puts("constructor"); }
__attribute__((destructor))
static void after_main(void) { puts("destructor"); }
static void at_exit(void) { puts("atexit"); }
int main(void)
{
if (atexit(at_exit) != 0)
return 1;
printf("main: page size=%lu\n", getauxval(AT_PAGESZ));
return 0;
}
cc -g -Wall -Wextra startup.c -o startup
./startup
readelf -hW -lW startup
readelf -dW startup
objdump -d --disassemble=_start startup
LD_SHOW_AUXV=1 ./startup
在该 glibc 示例中,constructor 先于 main;main 正常返回后会执行 atexit 回调与相应终结工作。此实验不证明不同共享库之间存在任意可依赖的全局初始化顺序。实际页大小应从输出读取,勿固定写成 4096。
正常返回 main 对应 libc 的 exit 流程,会调用退出处理并刷新 stdio。_Exit/_exit 跳过这些用户态清理,进程崩溃或被强制终止也不会保证完成它们。原始 Linux exit 系统调用与 libc 的进程退出封装还涉及线程范围差异,见 系统调用 ABI。exit(3)
将退出方式与输出缓冲分开验证
保存为 exit-mode.c。显式全缓冲让实验不依赖 stdout 是否连接终端;退出回调用标准错误输出标记,便于与普通输出分别检查。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char output_buffer[1024];
static void finish(void) { fputs("atexit ran\n", stderr); }
int main(int argc, char **argv) {
if (setvbuf(stdout, output_buffer, _IOFBF, sizeof output_buffer) != 0)
return 1;
if (atexit(finish) != 0) return 1;
fputs("pending", stdout);
if (argc == 2 && strcmp(argv[1], "immediate") == 0)
_Exit(7);
return 7;
}
cc -Wall -Wextra exit-mode.c -o exit-mode
./exit-mode > normal.out 2> normal.err
./exit-mode immediate > immediate.out 2> immediate.err
两次退出状态都应为 7。正常返回时 normal.out 为 pending,normal.err 有 atexit ran;立即退出时两个文件都为空。若脚本启用了 set -e,应显式接收预期的非零状态,否则可能在第一次运行后就停止。
这说明退出码与用户态清理是两条独立信息:相同状态码不能证明执行了相同的收尾动作。正常退出刷新 stdio 也不等于数据已持久化到存储设备;需要持久化语义的程序还要按文件系统与接口约定安排同步。exit(3)、_exit(2)
静态链接仍然有启动工作
| 构建形式 | 常见入口安排 | 仍需关注 |
| 动态 PIE | PT_INTERP,解释器先运行 | 依赖映射、重定位、初始化 |
| 静态非 PIE | 通常无 PT_INTERP,直接主入口 | libc 启动、TLS、构造函数、可能的 IFUNC |
| 静态 PIE | 通常无 PT_INTERP,直接主入口 | 自重定位及运行时初始化 |
| 自定义无 libc 程序 | 自定义入口 | 自己遵守 ABI 与退出协议 |
静态链接通常从归档抽取需要的成员,不是把整个 libc 无条件塞入文件。静态程序仍可能含 GOT/PLT 相关结构或需要 IFUNC 重定位,静态 PIE 更明确需要处理位置变化。“静态就无重定位、启动必然快十倍”都不能成立;应在同功能、同环境下比较。
main 前崩溃怎样定位
先用 readelf 确认主入口、解释器与依赖,再用 gdb ./startup 的 starti 从第一条用户态指令停下;动态程序此时可能处于解释器。随后可在 main、构造函数或 __libc_start_main 设置断点。内部符号是否可见取决于调试符号和构建方式。
strace -e trace=execve,openat,mmap,mprotect ./startup 能显示加载阶段的系统调用,不能显示所有用户态初始化。若依赖已成功映射而 main 尚未到达,继续检查重定位、IFUNC、构造函数及 TLS 使用,而不只是盯着 main 第一行。