现在,让我们看看程序调用过程中汇编操作,一个过程调用包括将数据(以过程参数和返回值的形式), 和控制从代码的一部分传递到令一部分。另外,它还必须在进入是为过程的局部变量分配空间,并在退出时释放这些空间。
栈帧结构
IA32程序用程序栈来支持过程调用。机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储,为整个过程分配的那部分栈称为栈帧(stack frame)。最顶端的栈帧以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。当程序执行时,栈指针是可以移动的,因此绝大多数信息的访问都是相对于帧指针的。
转移控制
call指令:其效果是将返回值地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址。这样当被调用函数返回时,执行会从此处继续。ret指令从栈中弹出地址,并跳转到这个位置。例如下面代码:
int accum = 0;
int sum(int x,int y);
int main()
{
return sum(1.3);
}
int sum(int x,int y)
{
int t = x + y;
accum += t;
return t;
}
经过反汇编后,节选处call部分的代码如下图所示:
在main函数中我们可以看到,在main函数中,地址为0x080483dc的call指令调用函数sum,指明了栈指针%esp和程序计数器%eip的值。我们可以看到在main函数中,地址0x080483dc的下一个执行地址是是0x080483e1,这一点很重要。call指令的效果是将返回地址0x080483e1压入栈中,并跳到函数sum的第一条指令,地址为0x8048394。函数sum继续执行,直到遇到地址为0x080483a4的ret指令。这条指令从栈中弹出值0x080483e1,然后跳转到这个地址,就在调用sum的call函数之后,继续main函数的执行。
寄存器使用示例
int swap_add(int* xp,int* yp);
int caller()
{
int arg1 = 534;
int arg2 = 1057;
int sum = swap_add(&arg1 , &arg2);
int diff = arg1 - arg2;
retur sum * diff;
}
int swap_add(int* xp,int* yp)
{
int x = * xp;
int y = * yp;
*xp = y;
*yp = x;
return x + y;
}
我们先来看看caller函数调用swap_add正在运行时的栈帧结构。有些指令访问的栈位置是相对于栈指针%esp的,而另一些的访问的栈位置是相对于基地址指针%ebp的。
caller:
push1 %ebp
movl %esp,%ebp
sub1 $24, %esp
movl $534,-4(%ebp)
movl $1057,-8(%ebp)
leal -8(%ebp),%eax //compute &arg2
movl %eax,4(%esp)
leal -4(%ebp),%eax //compute &arg1
movl %eax,(%esp)
call swap_add
我们可以看到,在调用swap_add之前,我们用push1指令把%ebp的数据压入栈,然后为这个栈分配24字节。caller的帧栈包括局部变量arg1和arg2的存储,其位置相对于帧指针是-4和-8。这些变量必须存在栈中,因为我们必须为他们生成地址。
在这里我们可以看到分配给栈帧的24个字节中,8个用于局部变量,8个用于向swap_add传递参数,还有8个未使用。因为GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍,包括保存%ebp值的4个字节和返回值的4个字节,caller一共使用了32个字节,采用这个规则是为了保证访问数据的严格对齐。