CSAPP 第三章「程序的机器级表示」读完了,整理几点最颠覆认知的收获。
寄存器约定:谁负责保存现场
x86-64 的寄存器分为调用者保存和被调用者保存两组。被调用者可以「放心使用」调用者保存的寄存器,用完即弃;如果要动用被调用者保存的寄存器,必须先压栈、返回前恢复。理解这条约定,才能看懂编译器生成的函数开头为什么总是在 push/pop。
leaq:编译器的小心思
leaq 名义上是「加载有效地址」,实际被编译器当成一个免费的加法器用:leaq 8(%rdi,%rdi,4), %rax 这类指令,实际上是在算 rdi * 5 + 8。看到 leaq 先别想内存,想想它在算什么算术。
栈帧与递归
每次 call 会压入返回地址,函数再依次压入被调用者保存的寄存器和局部变量,形成一帧。递归深度过大导致栈溢出,在汇编视角下非常直观——就是返回地址把栈写到了别的内存区域。缓冲区溢出攻击利用的也是同一机制。
一次真实的调试
用 gdb 逐帧查看 rbp 链,定位了一次数组越界写:越界写入恰好覆盖了父栈帧的返回地址,导致函数返回时跳转到野地址。以前只觉得「段错误」是个玄学错误,现在能从头到尾讲清楚它是怎么发生的。
小结
这一章最大的价值是建立「C 代码即汇编」的直觉。下次看到奇怪的性能问题或诡异崩溃,至少知道该去翻哪一层。