You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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的最小函数集与参数初始化

  1. 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到对应文件描述符,完成缓冲区初始化
  2. 你的汇编代码无法运行的原因

    • 未建立合法栈帧:必须先执行push %rbp; mov %rsp, %rbp创建栈帧,否则-4(%rbp)这类偏移指向的是无效内存地址
    • 缺少栈空间分配:你注释里标注了subq $16, %rsp但实际没执行,直接写入-16(%rbp)会覆盖栈上的原有数据(比如之前压栈的%rbp值)
    • 栈未对齐:x86_64要求栈指针保持16字节对齐,未调整的话后续函数调用会触发崩溃

关于汇编生成的疑问

  1. 为什么return 0会生成局部变量?

    • 这是编译器无优化(O0)的表现:O0等级下编译器会严格按照代码语义生成指令,return 0会先把0存入栈帧上的临时存储区,再将值加载到%eax返回。开启O1及以上优化后,这段冗余代码会被直接删除,只保留xor %eax,%eax。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 04:38:14