[转] 函数调用栈
http://kingj.iteye.com/blog/1555017
http://www.cnblogs.com/rain-lei/p/3622057.html
当调用(call)一个函数时,主调函数将声明中的参数表以逆序压栈,然后将当前的代码执行指针(eip)压栈,跳转到被调函数的入口点。
进入被调函数时,函数将esp减去相应字节数获取局部变量存储空间。被调函数返回(ret)时,将esp加上相应字节数,归还栈空间,弹出主调函数压在栈中的代码执行指针(eip),跳回主调函数。再由主调函数恢复到调用前的栈。
为了访问函数局部变量,必须有方法定位每一个变量。变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp
的值保存在ebp中,并事先将原ebp的值压栈保存,以声明中的顺序(即压栈的相反顺序)来确定偏移量。
访问函数的局部变量和访问函数参数的区别:
局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。
函数的返回值不同于函数参数,可以通过寄存器传递。如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,将通过eax寄存 器传递。如果返回值类型是64位变量,如_int64,则通过edx+eax传递,edx存储高32位,eax存储低32位。如果返回值是浮点类型,如 float和double,通过专用的浮点数寄存器栈的栈顶返回。如果返回值类型是struct或class类型,编译器将通过隐式修改函数的签名,以引 用型参数的形式传回。由于函数返回值通过寄存器返回,不需要空间分配等操作,所以返回值的代价很低。基于这个原因,C89规范中约定,不写明返回值类型的 函数,返回值类型默认为int。这一规则与现行的C++语法相违背,因为C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。这种语法 不兼容性是为了加强C++的类型安全,但同时也带来了一些代码兼容性问题。
代码示例
VarType Func (Arg1, Arg2, Arg3, ... ArgN)
{
VarType Var1, Var2, Var3, ...VarN;
//...
return VarN;
}
假设sizeof(VarType) = 4(DWORD), 则一次函数调用汇编代码示例为:
调用方代码:
push ArgN ; 依次逆序压入调用参数
push ...
push Arg1
call Func_Address ; 压入当前EIP后跳转
跳转至被调方代码:
push ebp ; 备份调用方EBP指针
mov ebp, esp ; 建立被调方栈底
sub esp, N * 4; 为局部变量分配空间
mov dword ptr[esp - 4 * 1 ], 0 ; 初始化各个局部变量 = 0 这里假定VarType不是类
mov dword ptr[esp - 4 * ... ], 0
mov dword ptr[esp - 4 * N ], 0
. . . . . . ; 这里执行一些函数功能语句(比如将第N个参数[ebp + N * 4]存入局部变量), 功能完成后将函数返回值存至eax
add esp, N * 4 ; 销毁局部变量
mov esp, ebp ; 恢复主调方栈顶
pop ebp ; 恢复主调方栈底
ret ; 弹出EIP 返回主调方代码
接上面调用方代码:
add esp, N * 4 ; 释放参数空间, 恢复调用前的栈
mov dword ptr[ebp - 4], eax ; 将返回值保存进调用方的某个VarType型局部变量
所有局部变量都在栈中由函数统一分配,形成了类似逆序数组的结构,可以通过指针逐一访问。这一特点具有很多有趣性质,比如,考虑如下函数,找出其中的错误及其造成的结果:
void f()
{
int i,a[10];
for(i=0;i<=10;++i)a[i]=0;/An error occurs here!
}
这个函数中包含的错误,即使是C++新手也很容易发现,这是老生常 谈的越界访问问题。但是这个错误造成的结果,是很多人没有想到的。这次的越界访问,并不会像很多新手预料的那样造成一个“非法操作”消息,也不会像很多老 手估计的那样会默不作声,而是导致一个死循环。
错误的本质显而易见,我们访问了a[10],但是a[10]并不存在。C++标准对于越界访问只是说“未定义操作”。我们知道,a[10]是数组a所在位置之后的一个位置,但问题是,是谁在这个位置上。是i!
根据前面的讨论,i在数组a之前被声明,所以在a之前分配在栈上。但是,I386上栈是向下增长的,所以,a的地址低于i的地址。其结果是在循环的最
后,a[i]引用到了i自己!接下来的事情就不难预见了,a[i],也就是i,被重置为0,然后继续循环的条件仍然成立……这个循环会一直继续下去,直到
在你的帐单上产生高额电费,直到耗光地球电能,直到太阳停止燃烧……呵呵,或者直到聪明的你把程序Kill了……