CSAPP要点总结 第3章 程序的机器级表示 part4 procedures
前言
对于子函数(过程)的机器级实现,除了跳转到相应代码处执行,还要为子函数中的局部变量分配临时空间,退出时释放空间。
对于大多数机器,包括IA32,调用子函数时包含两个部分,一个是跳转,一个是分配和释放程序栈。
栈帧的结构
如下图所示,为栈帧的通用结构。
注意return address 即4(%ebp)
指的是code内存片段的返回地址,从而指导CS:IP指向调用者下一句。
而(%ebp)
则是指的上一帧(调用者)的old %ebp地址。
通常汇编代码中会包含如下指令实现栈帧结构的创建:
pushl %ebp # 将原%ebp寄存器的值放入栈,参见栈帧结构的"被保存的%ebp"
.cfi_def_cfa_offset 8
.cfi_offset 5, -8 # 猜测此处的伪码是用于将%esp偏移,
# 以存放函参和返回地址
movl %esp, %ebp # 将%ebp指向栈顶
.cfi_def_cfa_register 5
andl $-16, %esp # 将%esp,存放临时变量等
需要说明的是,这是通用的结构,而有些时候编译器优化会把有些简单的子函数的临时变量用寄存器代替,
而有些情况下,编译器是没有办法优化的,例如:
- 临时变量过多,而寄存器数量有限
- 当引用临时变量时
&temp
,因为寄存器不能被引用,只能算内存空间,因此不能用寄存器代替临时变量 - 数组、结构等临时变量必须通过数组或结构的引用来访问
转移控制
相关指令包括:
(1) call Label
以及call *Operand
过程调用。多数是Label
;*Operand
指的是0x1(%eax, %ebx, 4)
之类的操作数,即地址。call有push 返回地址的过程,然后才是jump
(2) leave
为返回准备栈。类似如下代码,将%esp,%ebp返回调用层的原来的位置。
movl %ebp, %esp
popl %ebp
(3) ret
返回。有些困惑的是,在学习王爽《汇编语言》过程中,ret也负责pop的任务,而这里貌似把该任务交给了leave。这里ret貌似只是jump?
而call是有push 返回地址的过程,不仅仅是jump
寄存器的使用规定
寄存器是唯一能被所有过程共享的资源,那么,需要规划使用规则避免不同过程之间相互影响。比如子过程不能影响了调用者使用的寄存器。那么,就有约定俗成的规定。
寄存器%eax
、%ecx
、%edx
被划分为调用者保存寄存器(caller-save-registers)。
即调用者在call之前先保存这些值,将这些寄存器腾出来供被调用者使用。
当过程L1调用过程L2时,L2可以覆盖这些寄存器。
寄存器%ebx
、%esi
、%edi
被划分为被调用者保存寄存器(callee-save-registers)
即当过程L1调用过程L2时,L2先把它们保存到栈中,使用该值,并在返回前恢复他们为L1的值。这是因为通常L1会继续用到这些值。
那么,通常情况下%eax
、%ecx
、%edx
保存一些计算值较为合适。
而%ebx
、%esi
、%edi
保存当前指针或者数组index较为合适。
这也是他们的常用用法。当然,也不限于上述应用场合。
总的而言,就是将寄存器划分为caller-save-registers和callee-save-registers
递归过程
开销很大,会不断加栈,C语言中递归总的有一个if来返回,汇编也一样,需要cmp比较厚条件跳转。
递归有较大优化的空间。