如何在x86_64 ELF文件中实现最小化CRT?自制编译器需求
自制CRT与编译器参数处理疑问
核心需求
我要为自制编译器实现CRT(C、C++、Rust都会用到CRT),需要明确:
- 为
main准备参数(argc、argv、envp、auxp、program_name、栈)所需的最小函数集 - CRT如何为
malloc、printf等函数准备栈安全空间,以及如何初始化argc这类参数
我的编译器直接生成完整ELF文件而非目标文件,尝试通过栈获取argc等参数,用了这段汇编:
movl %edi, -4(%rbp) ;; argc (subq $16, %rsp) movq %rsi, -16(%rbp) ;; argv (subq $16, %rsp) movq %rdx, -24(%rbp) ;; envp (subq $32, %rsp)
但这段代码无法运行,不过我在Compiler Explorer和readelf里见过类似实现。
多组C代码与汇编输出的疑问
1. 无返回值的空main
C代码:
int main() {}
对应汇编:
main: push %rbp mov %rsp,%rbp xor %eax,%eax pop %rbp ret
2. 显式返回0的空main
C代码:
int main() { return 0; }
对应汇编:
main: push %rbp mov %rsp,%rbp movl $0x0,-0x4(%rbp) xor %eax,%eax pop %rbp ret
疑问:既然可以直接用xor %eax,%eax返回0,为什么还要生成movl $0x0,-0x4(%rbp)创建局部变量?
3. 带argc参数的main
C代码:
int main(int argc) {}
对应汇编:
main: push %rbp mov %rsp,%rbp mov %edi,-0x4(%rbp) xor %eax,%eax pop %rbp ret
这里能看到%edi作为argc的载体被存到-0x4(%rbp)。我后续尝试添加了这段代码:
movq %rsi, -16(%rbp) ;; argv (subq $16, %rsp) movq %rdx, -24(%rbp) ;; envp (subq $32, %rsp)
疑问:subq $<number>, %rsp和add $<number>, %rsp这类指令什么时候会生成?我观察到使用局部变量时会生成,比如下面两个示例:
示例1:使用argv的main
C代码:
int main(int argc, char** argv) { puts(argv[0]); puts(argv[1]); }
对应汇编:
main: push %rbp mov %rsp,%rbp sub $0x10,%rsp mov %edi,-0x4(%rbp) mov %rsi,-0x10(%rbp) mov -0x10(%rbp),%rax mov (%rax),%rdi call 1030 <puts@plt> mov -0x10(%rbp),%rax mov 0x8(%rax),%rdi call 1030 <puts@plt> xor %eax,%eax add $0x10,%rsp pop %rbp ret
示例2:声明了envp但未使用的main
C代码:
int main(int argc, char** argv, char** envp) { puts(argv[0]); puts(argv[1]); puts((const char *)argc); }
对应汇编:
main: push %rbp mov %rsp,%rbp sub $0x20,%rsp mov %edi,-0x4(%rbp) mov %rsi,-0x10(%rbp) mov %rdx,-0x18(%rbp) mov -0x10(%rbp),%rax mov (%rax),%rdi call 1030 <puts@plt> mov -0x10(%rbp),%rax mov 0x8(%rax),%rdi call 1030 <puts@plt> movslq -0x4(%rbp),%rdi call 1030 <puts@plt> xor %eax,%eax add $0x20,%rsp pop %rbp ret
有意思的是:示例2里哪怕没用到envp,只要声明了它,就会生成sub/add $0x20, %rsp指令。
问题解答
关于CRT的最小函数集与参数初始化
main参数准备的核心逻辑- 在x86_64 System V调用约定下,内核启动进程时会把
argc、argv、envp、auxv按顺序放在栈上,CRT的真正入口(通常是_start)需要先从栈上读取这些值,再传递给main。 - 最小函数集至少需要:
_start:程序的启动入口,负责读取栈上参数、初始化栈帧、调用main,最后调用exit向内核返回状态- 栈对齐逻辑:x86_64要求调用函数前栈指针是16字节对齐,CRT需要确保栈满足这个要求
- 堆初始化(若支持
malloc):调用brk或mmap分配堆空间,为内存分配函数提供基础 - 标准IO初始化(若支持
printf):关联stdin/stdout/stderr到对应文件描述符,完成缓冲区初始化
- 在x86_64 System V调用约定下,内核启动进程时会把
你的汇编代码无法运行的原因
- 未建立合法栈帧:必须先执行
push %rbp; mov %rsp, %rbp创建栈帧,否则-4(%rbp)这类偏移指向的是无效内存地址 - 缺少栈空间分配:你注释里标注了
subq $16, %rsp但实际没执行,直接写入-16(%rbp)会覆盖栈上的原有数据(比如之前压栈的%rbp值) - 栈未对齐:x86_64要求栈指针保持16字节对齐,未调整的话后续函数调用会触发崩溃
- 未建立合法栈帧:必须先执行
关于汇编生成的疑问
为什么
return 0会生成局部变量?- 这是编译器无优化(O0)的表现:O0等级下编译器会严格按照代码语义生成指令,
return 0会先把0存入栈帧上的临时存储区,再将值加载到%eax返回。开启O1及以上优化后,这段冗余代码会被直接删除,只保留xor %eax,%eax。
- 这是编译器无优化(O0)的表现:O0等级下编译器会严格按照代码语义生成指令,
sub/add %rsp什么时候生成?- 这类指令用来分配/释放栈帧上的局部变量与参数存储区,同时保证栈对齐:
- 当函数需要将寄存器中的参数存储到栈上(哪怕参数没被使用)、或者存在局部变量时,编译器会计算所需栈空间大小,用
subq分配 - 栈空间大小会被调整为16字节的倍数(满足x86_64调用约定),所以你会看到
0x10、0x20这类16的整数值 - 示例2中即使没用到
envp,但因为声明了该参数,编译器仍会为它在栈上分配8字节空间,加上argc的4字节、argv的8字节,再补全对齐到16字节倍数,最终需要32字节(0x20)的栈空间,因此生成subq $0x20,%rsp
- 当函数需要将寄存器中的参数存储到栈上(哪怕参数没被使用)、或者存在局部变量时,编译器会计算所需栈空间大小,用
- 这类指令用来分配/释放栈帧上的局部变量与参数存储区,同时保证栈对齐:
内容的提问来源于stack exchange,提问作者Tayler
相关产品推荐
相关产品推荐

