重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
c/c++程序中进行函数调用,需要在进程地址空间的栈区为这个函数申请一块栈空间,这一块栈空间称为函数的栈帧。除了内联函数之外,普通函数的调用都有创建函数栈帧和销毁函数栈帧的过程,函数栈帧的创建与销毁过程均在进程地址空间中进行,执行一个函数,需要CPU进行计算并执行相关指令。
函数栈帧的创建与销毁过程函数的调用过程是CPU执行相关指令的过程,在CPU中存在大量的寄存器,常用的寄存器有:
eax 通用寄存器,用于存放临时数据,例如函数的返回值 ebx 通用寄存器,存放临时数据 ebp 栈底寄存器,保存当前正在调用函数的栈帧的栈底 esp 栈顶寄存器,保存当前正在调用函数的栈帧的栈顶 eip 指令寄存器,指令寄存器中保存CPU当前正在执行的指令的下一条指令的地址,指令寄存器也称为PC指针
以下面的代码为例说明函数栈帧的创建与销毁过程:
#includeint MyAdd(int a, int b)
{int c = a + b;
return c;
}
int main()
{int x = 0xA;
int y = 0xB;
int z = 0;
z=MyAdd(x, y);
printf("%d\n", z);
return 0;
}
main函数为主函数,在Linux下,main函数是由__libc_start_main
函数调用的,__libc_start_main
函数由操作系统进行调用。
int x=0xA
这条语句转化为汇编指令为
mov dword ptr [ebp-8],0Ah
对应如图
此时eip寄存器中存放
mov dword ptr [ebp-14h],0Bh
这条指令的地址
int y=0xB
这条语句转化为汇编指令为
mov dword ptr [ebp-14h],0Bh
对应如图
此时eip寄存器中存放
mov dword ptr [ebp-20h],0
这条指令的地址。此时,局部变量x和y已经在main函数的栈帧中形成。
int z=0
这条语句转化为汇编指令为
mov dword ptr [ebp-20h],0
对应如图
z=MyAdd(x,y)此时eip寄存器中存放
mov eax,dward ptr [ebp-14h]
这条指令的地址。可以看出,在栈上的变量地址不一定是连续的,可能是操作系统在设计的时候出于安全的考虑。
z=MyAdd(x,y)这条语句对应多条汇编指令。
mov eax,dward ptr [ebp-14h]
对应如图
此时eip寄存器中存放
push eax
这条指令的地址
push eax
把eax的值压入栈中,同时栈顶寄存器的值发生相应变化。
对应如图
此时eip寄存器中存放
mov ecx,dward ptr [ebp-8]
这条指令的地址
mov ecx,dward ptr [ebp-8]
把ebp-8地址处的值拷贝一份到ecx通用寄存器中,实际上是将x的值拷贝到ecx中。
对应如图
此时eip寄存器中存放
push ecx
这条指令的地址
push ecx
把ecx的值压入栈中,同时栈顶寄存器esp的值发生变化。
对应如图
此时eip寄存器中存放
call MyAdd
这条指令的地址.以上指令实际上是在进行参数的拷贝,根据实参拷贝形参并把形参压栈,此时MyAdd函数还没有开始正式调用,且通过观察可以发现,在进行实参拷贝时,先拷贝y,在拷贝x,说明形参的实例化顺序是从右到左的。
可以得出2点结论
- 形参实例化的顺序是从右向左的,在传参的过程中,是先实例化右边的参数,在实例化左边的参数。形参实例化的顺序与x,y定义的顺序无关。
- 函数调用需要传参形成临时拷贝,临时拷贝在函数正式调用之前已经完成,是函数正式调用的前置工作。
call MyAdd
call MyAdd这一条汇编指令对应2个操作:
压入返回地址。MyAdd函数执行结束以后需要返回到main函数,返回地址就是返回到main函数栈帧的中对应的地址,在转移至目标函数MyAdd之前,要把这个地址压入栈中。否则MyAdd函数执行完毕就无法回到main函数继续执行main函数后面的语句。在上面的代码中,这个返回地址是
add esp 8
这条指令的地址,在MyAdd函数调用完毕会用到这个地址。
- 转移至目标函数。此时eip中保存的是
jump MyAdd
指令的地址
call MyAdd
指令的关键点是压入返回地址,它确保了在函数调用完毕以后能正常的回到原来的函数的栈帧。
MyAdd函数调用jmp MyAdd
jmp MyAdd并非立马跳到MyAdd函数开始执行MyAdd函数的语句,而是修改eip的值为MyAdd函数第一条汇编指令的地址。执行jmp MyAdd之后,eip中保存的就是MyAdd函数第一条汇编指令的地址,至此,可以开始正式调用MyAdd函数。
push ebp
把当前栈底寄存器的值压入栈中,注意:当前栈底寄存器中保存的是main函数栈底的地址。
对应如图
此时eip寄存器中存放
mov ebp esp
这条指令的地址
mov ebp esp #是把esp的内容拷贝到ebp
把esp的内容拷贝到ebp中,这样ebp就不在指向main函数的栈底。这条指令是把一个寄存器的内容拷贝到另外一个寄存器,没有进行内存的访问,执行速度很快。
对应如图
此时eip寄存器中存放
sub esp,0CCh
这条指令的地址
sub esp,0CCh
将当前esp寄存器中的值减去一个值,让esp和ebp维护一段栈空间,这一段栈空间就是MyAdd函数的栈帧。
对应如图
上面的汇编指令
sub esp,0CCh
是把esp的值减去0CCh,调用不同的函数,这个值是不一样的,并非都是减去0CCh,这个值取决于调用的函数的规模,例如该函数中定义了多少局部对象。编译器是可以提前预知到函数的规模大小的,根据函数的规模,确定调用该函数需要多少的栈空间,编译器可以通过sizeof
计算出函数中所有栈上变量所需空间的总和,从而决定应该为该函数创建多大的栈帧。因此,sizeof
计算变量的大小是在编译时完成的。此时eip寄存器中存放
mov dwrod ptr [ebp-8],0
这条指令的地址
int c=0
mov dwrod ptr [ebp-8],0
对应如图
此时eip寄存器中存放
mov eax,dword ptr [ebp+8]
这条指令的地址
c=a+b
这条语句转化为汇编指令为
mov eax,dword ptr [ebp+8]
把ebp+8位置的值放到eax中,实际上就是把形参的值放到eax中
对应如图
此时eip寄存器中存放
add eax,dword ptr [ebp+0Ch]
这条指令的地址
add eax,dword ptr [ebp+0Ch]
把ebp+0Ch位置的值加到eax上,其实就是把形参b的值加到eax上,此时eax中存放的值就是a+b
对应如图
此时eip寄存器中存放
mov dword ptr [ebp-8],eax
这条指令的地址
mov dword ptr [ebp-8],eax
把eax的值给到ebp-8位置,就是把eax的值给到ebp-8
对应如图
至此,c=a+b计算完成。
此时eip寄存器中存放
mov eax,dword ptr [ebp-8]
这条指令的地址
mov eax,dword ptr [ebp-8]
即把c的值保存到寄存器中,这是在为函数返回做准备。如果函数返回小对象,则是通过寄存器保存的。
对应如图
MyAdd函数栈帧销毁至此MyAdd函数正式调用完毕,需要销毁栈帧和返回main函数。
mov ebp esp
把ebp的值给esp。esp发生变化
对应如图
返回到main函数MyAdd函数的栈帧被释放,但是MyAdd函数的栈帧数据并没有被清空。注意此时ebp和esp指向位置存放的值是main函数的栈底。
此时eip寄存器中存放
pop ebp
这条指令的地址
pop ebp
把栈顶位置的数据(main函数栈底放入地址)给ebp寄存器,同时弹栈,esp发生改变。
对应如图
此时esp指向的栈顶位置的数据就是在
call MyAdd
时压入的返回地址。此时eip寄存器中存放
ret
这条指令的地址
ret
ret是恢复返回地址的汇编指令,在这里ret的作用类似于pop eib,即把栈顶位置的数据放到eip中,此时栈顶位置的数据正是call MyAdd时压入的返回地址。就是
add esp,8
这条指令的地址。对应如图
此时eip寄存器中存放
add esp,8
这条指令的地址
add esp,8
把esp的值+8
对应如图
这样就正式的恢复到了MyAdd函数调用之前的样子。
此时eip寄存器中存放
mov dword ptr [ebp-20h],eax
这条指令的地址
mov dword ptr [ebp-20h],eax
把eax的值给到ebp-20h的位置,eax中的值就是MyAdd函数的返回值。ebp-20h位置对应z,就是把返回值给z.
这就是整个函数栈帧的创建和销毁过程。
总结push ebp
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧