这一章在干嘛?

全书收尾:掀开「程序运行时」的地板——函数调用的栈帧布局、判断目标环境特征的方法、C 与汇编的接口约定,以及影响运行时效率的因素。读懂它,调试和逆向都上一台阶。

18.1 栈帧:函数调用的内存账本

18.2 C 与汇编的接口

18.3 运行时效率:钱花在刀刃上

18.1 栈帧:函数调用的内存账本

每次函数调用,运行时在栈上压一个栈帧(stack frame),装着这个函数的家当:

void inner(int x)   /* 被调者 */
void outer(void)
{
    int a = 1;                  /* outer 的帧:a 在栈上 */
    inner(a);                   /* 压 inner 的帧:返回地址+形参x */
}                               /* 返回时两个帧依次弹出 */
  • 调用前:调用者把实参压栈/传寄存器,执行 call(压返回地址);
  • 函数内:被调者调整栈指针、给局部变量腾地;
  • 返回前:恢复寄存器、栈指针,ret 弹返回地址——帧弹走后其内存并不擦除,只是失效,这就是「返回局部变量地址」能暂时看起来正常的原因(最阴险的未定义行为)。

18.2 C 与汇编的接口

C 编译好也是汇编,两者能互相调用——前提是遵守同一份调用约定:参数怎么传(寄存器还是栈、从左还是从右压)、返回值放哪、哪些寄存器调用者保存、哪些被调者保存。用 gcc -S 看 C 对应的汇编,是理解约定最直接的办法:

/* add.c: int add(int a, int b) { return a + b; }  → x86-64 SysV 约定 */
/* a 在 edi,b 在 esi,返回值放 eax */
add:
    lea  eax, [rdi + rsi]     /* eax = a + b */
    ret

在 C 里调用汇编函数:按约定在汇编里实现同名标号即可(裸机/embedded 里 extern void startup(void); 直通 .s 文件)。反过来,汇编里调 C 也一样:把参数按约定放好再 call。中断服务函数、启动代码、性能关键内核(FFT、AES)是这门手艺的主战场。

为什么默认别手写汇编:

编译器会做指令调度、寄存器分配、循环展开,普通代码手写汇编很难更快,还丧失可移植性。先测量,确认瓶颈存在再动手。

18.3 运行时效率:钱花在刀刃上

因素影响工程建议
指针 vs 下标老机器指针遍历更快;现代编译器对两者生成的代码基本一样写更清晰的下标版,交给优化器
寄存器变量register 是提示;编译器自己会做寄存器分配且更聪明几乎不用手写
函数调用开销压栈/跳转有成本,小函数频繁调用可建议内联inline / 编译器 O2 自动内联
副作用与优化未定义行为给了优化器「自由」,可能删掉你以为必须的检查远离 UB,优化才可预测

全书总结一句话:

C 的一切开销都摊在明面上——栈帧、拷贝、指针运算、库调用。理解运行时模型,就能预判代码的代价;预判了代价,性能不过是顺手的副产品。至此,从第一行 hello 到栈帧布局的整幅地图你已经走完。