尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

剖析rt0最小C运行时_start入口:6行内联汇编如何完成argc、argv、envp解析与栈对齐

剖析rt0最小C运行时_start入口:6行内联汇编如何完成argc、argv、envp解析与栈对齐 剖析rt0最小C运行时_start入口6行内联汇编如何完成argc、argv、envp解析与栈对齐【免费下载链接】rt0A minimal C runtime for Linux i386 x86_64项目地址: https://gitcode.com/gh_mirrors/rt/rt0rt0是一个面向 Linux i386 与 x86_64 的最小 C 运行时minimal C runtime整个库仅约 202 行 C 代码。它的核心就是入口函数_start用 6 行 x86_64 内联汇编i386 为 9 行完成argc、argv、envp的解析和 16 字节栈对齐随后把控制权交给你的main。这篇文章带你逐行读懂这套极简启动代码 为什么程序启动时还需要一段入口汇编普通开发中_start由 glibc 的crt0代劳初始化堆、传递参数、调用main、处理退出。而 rt0 把这些全部砍到最小只保留三件事从内核压入的栈上取出argc、argv、envp把栈顶对齐到 16 字节call main结束后用系统调用退出进程入口实现位于 src/lib/00_start.c全部逻辑浓缩在下面这段函数里void _start( void ) { __STARTUP_PREAMBLE__(); __asm__ volatile( __STARTUP_ASM__ call main ); _exit( __STARTUP_RETVAL__ ); for(;;); }真正的精华在宏__STARTUP_ASM__中。下面以 x86_64__LP64__分支为例逐行拆解。x86_64 的 6 行内联汇编逐行解析当内核启动你的可执行文件时栈顶布局是这样的[RSP0] argc ← 命令行参数个数 [RSP8] argv[0..n] ← 指向参数的指针数组 [...] envp[0..m] ← 指向环境变量字符串的指针数组NULL 结尾而 System V AMD64 ABI 规定调用函数前 3 个整型参数依次放入RDI、RSI、RDX且栈指针必须 16 字节对齐。所以_start的任务就是把栈上的数据搬到这三个寄存器里。pop %rbp ; ① 丢弃内核压入的返回地址RSP 上移 8 字节 mov %rsp, %rbp ; ② RBP 指向栈顶即 argc 的地址 mov (0)(%rbp), %rdi ; ③ RDI [RBP] → argc lea (8)(%rbp), %rsi ; ④ RSI RBP 8 → argv lea (8)(%rsi,%rdi,8), %rdx ; ⑤ RDX argv 8*argc 8 → envp and $-16, %rsp ; ⑥ 栈顶向下对齐到 16 字节 call main ; ⑦ 按 ABI 约定把三参数传给 main几个关键点第①行pop %rbp内核通过返回地址机制把控制权交给_start栈顶第一个 8 字节其实是返回地址指向内核用不上直接弹出。第③~⑤行是纯算术argv紧跟在argc后面偏移 8envp则跳过整个 argv 数组8*argc字节再偏移 8。一次lea指令就算出来了无需循环。第⑥行and $-16, %rsp-16的二进制是低位 4 位全 0 的掩码与运算直接抹掉 RSP 低 4 位一次完成 16 字节对齐。为什么必须对齐因为 SSE 系列指令如movaps要求栈 16 字节对齐不对齐直接触发段错误——这也是 glibc 启动代码里同样要做的事。第⑦行之后main的返回值留在RAX中由__STARTUP_RETVAL__捕获随后调用_exit()发起exit系统调用结束进程。for(;;);只是兜底理论上_exit永不返回函数声明带noreturn属性。i386 版本9 行汇编与 GCC 的好心偏移32 位下调用约定完全不同参数通过栈传递而非寄存器且内核把参数放在ECXargc、EDXargv、EAXenvp中。更麻烦的是GCC 在生成_start序言时会自作主张地压入若干寄存器来做自己的 16 字节对齐导致栈顶相对内核布局多出了 280x1c字节。于是 i386 分支这样处理mov %esp, %ebp ; EBP ESP add $0x1c, %ebp ; 跳过返回地址 GCC 额外压入的寄存器 mov (0)(%ebp), %ecx ; ECX argc lea (4)(%ebp), %edx ; EDX argv lea (4)(%edx,%ecx,4), %eax ; EAX envp32 位指针按 4 字节步进 and $-16, %esp ; 同样对齐到 16 字节 push %eax ; 按 32 位 ABI 把参数压回对齐后的栈 push %edx push %ecx注意最后的 3 条push由于 32 位 ABI 要求参数在栈上对齐后的新栈是干净的必须把解析出的argc/argv/envp重新压回去call main才能读到。这正是源码注释push args onto re-aligned stack的含义。解析结果都给了谁除了传给mainrt0 在 src/lib/00_start.c 中还顺带定义了两个全局符号char **__environ指向环境变量数组供#include rt0/rt0.h的用户代码使用见 include/rt0/rt0.hint errno全局错误码供 src/lib/syscall.c 中的系统调用错误处理__SYSCALL_CHK_ERROR__宏写入退出路径则由 src/lib/_exit.c 实现_exit(r)内部执行syscall1( SYS_exit, r )把main的返回值作为进程退出码交给内核。上手体验编译并运行测试用例仓库自带一套验证argc/argv/envp的测试程序目录t/下包括 t/hello.c、t/argc.c、t/argv.c、t/envp.c 以及综合示例 t/test.c。在仓库根目录执行make # 构建 librt0.a make test # 编译全部测试程序 make runtest # 逐一运行比如t/test.c会先打印命令行参数列表再打印所有环境变量——正好覆盖了_start解析出的全部数据。如果想自己写程序只需cc -c prog.c -nostdlib -Iinclude -o prog.o cc prog.o -nostdlib -Lsrc/lib -lrt0 -o prog得益于极小的运行时Hello World最终只有 608 字节i386/ 792 字节x86_64而 glibc 版本超过 2900 字节——对比数据见 STATS.md。核心文件速查 文件作用src/lib/00_start.c_start入口、启动内联汇编、__environ/errno定义src/lib/_exit.cexit系统调用封装src/lib/syscall.csyscall0~syscall6内联系统调用include/rt0/rt0.h用户侧头文件__environ、_exit、段符号include/rt0/syscall.h系统调用号与syscall*接口t/hello.c、t/argc.c、t/argv.c、t/envp.c验证参数解析的测试程序Makefile构建脚本支持 gold/ld、WITH_FAST_SYSCALL等选项总结rt0 的_start用一个极简的公式回答了C 程序是如何启动的弹出返回地址 → 寄存器算术取参 → 掩码对齐栈 → 调用 main → 系统调用退出。x86_64 下只需 6 条指令i386 下 9 条——读懂它你就同时理解了 Linux 进程初始栈布局、System V 调用约定和栈对齐三大基础概念 ✅ 对于想深入理解 C 运行时、编写极简系统程序的学习者这份 202 行的代码库是最好的入门材料之一。【免费下载链接】rt0A minimal C runtime for Linux i386 x86_64项目地址: https://gitcode.com/gh_mirrors/rt/rt0创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表