C语言中的函数(或称为方法或者过程)是通过进程的栈空间来进行管理的,一个个函数在栈空间的表现就像是一幅一幅的图片,称为栈帧(stack frame)。其中寄存器%ebp始终指向栈帧的开始,而寄存器%esp则像游标一样在相邻两个栈帧中滑动来存取值。
下面以一个实际例子来说明:
由上图可知函数compare调用函数max, 不同颜色代表不同的栈帧,我们来开始分析汇编代码:
首先我们假设此时esp寄存器为某个值Vbase(上图中第二个箭头指向的位置);
pushl %ebp ;将%ebp寄存器的值入栈,此时Vesp = Vbase - 4;
movl %esp, %ebp; 将%esp的值赋值给%ebp,则Vebp = Vbase -4;
subl $8, %esp; 将%esp的值减去8,即分配栈空间,用于保存临时变量,这里是x和y,
;此时Vesp = Vbase-4-8 = Vbase - 12
movl $7, 4(%esp); 这里将y的值7存到Vesp+4指定的地址
movl $5, %esp;这里将x的值5存储到Vesp指定的地址
call max;这里调用max函数,call指令会首先将函数执行完后的返回地址(即call后面第一条指令的虚拟地址)压入栈
;中,此时Vesp = Vbase - 16
//*************************************这里进入max函数********************************************************//
pushl %ebp;将%ebp寄存器的值入栈,此时Vesp = Vbase - 20;
movl %esp, %ebp; 将%esp的值赋值给ebp,则Vebp = Vbase -20;
movl 12(%ebp), %eax;将Vbase - 8地址即y的值存到%eax寄存器中
cmpl %eax, 8(%ebp);将y 与Vbase - 12地址即x的值作比较
setg %al;如果x大于y设置%al为1,相反设置为0
movzbl %al, %eax;用零扩展位方式将al的值扩展到%eax中,即清除%eax的高24位
popl %ebp;将进入函数时的%ebp值出栈存入%ebp寄存器中,此时Vesp = Vbase -16,指向“返回地址”
ret;将Vesp指向的地址的值转入%eip寄存器,然后让%esp+4,即出栈,此时Vesp = Vbase -12,指向x
//*************************************这里退出max函数********************************************************//
testl %eax, %eax;这里比较max的返回值是不是为1,函数的返回值一般存在eax寄存器中
mov $35, %edx; 这里编译器将x*y直接优化成35,将该值存到%edx寄存器中
cmovne %edx, %eax;这里用到了tesl的值,如果返回值为1,则将%edx的值赋值到%eax中
leave;将%ebp的值(即compare函数第二个语句中的%ebp值)赋值给%esp,即回收分配给x和y的空间,
;然后将%esp指向的栈顶地址中的值赋值给%ebp
ret;将Vesp指向的地址的值转入%eip寄存器,然后让Vesp+4,即出栈,此时Vesp = Vbase
扩展知识:由于芯片中寄存器是被所有方法共有的,为了不让被调用者破坏调用者的数据,必须遵循下面的规则:
1.寄存器%eax,%edx和%ecx作为调用者保存寄存器,如果函数P调用函数Q,那么Q可以任意使用这些寄存器,不用
担心会损坏P的数据
2.寄存器%ebx,%esi和%edi为被调用者保存寄存器,如果函数P调用函数Q,如果Q需要用到这些寄存器的时候,必须先
保存,当函数退出时恢复该寄存器的值。