C/C++函数调用过程--函数栈(二)

来源:互联网 发布:梦想小镇绿钞淘宝 编辑:程序博客网 时间:2024/06/05 23:40

上一篇比较简单的理解函数调用时的参数进出栈,这里转载一篇别人的文章,通过汇编比较完整的讲解进出栈的过程。看是看觉得略难理解,但仔细看完很有收获。


阅读本文之前补充一个概念:栈帧

每个函数的栈称为一帧,也就是该函数的栈帧。函数栈的基地址(EBP)称为栈帧指针,访问函数中的参数或局部变量,都是通过EBP加上偏移量来获得。


函数调用--函数栈

函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢?
 
对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈
代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
数据段:保存初始化的全局变量和静态变量,可读可写不可执行
BSS:未初始化的全局变量和静态变量
(注释:.DATA和.BSS c++并不明确区分,统一理解为静态数据段就好)
堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行
命令行参数和环境变量:前面没讲到,位于堆栈的上面
如图所示
寄存器
EAX:累加(Accumulator)寄存器,常用于函数返回值
EBX:基址(Base)寄存器,以它为基址访问内存
ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器
EDX:数据(Data)寄存器,常用于乘除法和I/O指针
ESI:源变址寄存器
EDI:目的变址寄存器(纠正原文写错的地方,目的寄存器是EDI)
ESP:堆栈(Stack)指针寄存器,指向堆栈顶部
EBP:基址指针寄存器,指向当前堆栈底部
EIP:指令寄存器,指向下一条指令的地址
源代码
复制代码
int print_out(int begin, int end){ printf("%d ", begin++); int *p; p = (int*)(int(&begin) - 4); if(begin <= end)  *p -= 5; return 1;} int add(int a, int b){ return a+b;} int pass(int a, int b, int c) { char buffer[4] = {0}; int sum = 0; int *ret; ret = (int*)(buffer+28); //(*ret) += 0xA; sum = a + b + c; return sum;} int main(){ print_out(0, 2); printf("\n"); int a = 1; int b = 2; int c; c = add(a, b); pass(a, b, c); int __sum; __asm {  mov __sum, eax } printf("%d\n", __sum); system("pause");}
复制代码

 

函数初始化
复制代码
  28: int main()    29: {011C1540 push ebp //压栈,保存ebp,注意push操作隐含esp-4011C1541 mov ebp,esp //把esp的值传递给ebp,设置当前ebp011C1543 sub esp,0F0h //给函数开辟空间,范围是(ebp, ebp-0xF0)011C1549 push ebx011C154A push esi011C154B push edi011C154C lea edi,[ebp-0F0h] //把edi赋值为ebp-0xF0011C1552 mov ecx,3Ch //函数空间的dword数目,0xF0>>2 = 0x3C011C1557 mov eax,0CCCCCCCCh011C155C rep stos dword ptr es:[edi] //rep指令的目的是重复其上面的指令.ECX的值是重复的次数.//STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址,然后EDI+4
复制代码
一般所用函数的开头都会有这段命令,完成了状态寄存器的保存,堆栈寄存器的保存,函数内存空间的初始化

个人理解:
1. ebp入栈之后,开辟内存空间应该不是固定值0xF0吧,应该是编译的时候编译器计算好的函数局部变量占用空间大小。
2. 开辟局部变量空间后,push ebx, esi ,edi 很多文章没提到,其后几个指令用来对局部变量初始化成0xcc,不过这个操作应该是debug版本才会做,release是随机值

函数调用
 30: print_out(0, 2);013D155E push 2 //第二个实参压栈013D1560 push 0 //第一个实参压栈013D1562 call print_out (13D10FAh)//返回地址压栈,本例中是013D1567,然后调用print_out函数013D1567 add esp,8  //两个实参出栈//注意在call命令中,隐含操作是把下一条指令的地址压栈,也就是所谓的返回地址
 
除了VS可能增加一些安全性检查外,print_out的初始化与main函数的初始化完全相同

(这里返回地址就是函数print_out的返回地址,实际上也等同于下一条指令的地址,在函数结束时把返回地址放入EIP,就能在函数返回后继续调用下条指令,也就是把函数参数出栈。
注意:返回地址放的是下条指令的地址,而不是函数返回值,函数返回值放在eax寄存器中)
 
被调用函数返回
复制代码
013D141C mov eax,1  //返回值传入eax中013D1421 pop edi   013D1422 pop esi   013D1423 pop ebx //寄存器出栈013D1424 add esp,0D0h //以下3条命令是调用VS的__RTC_CheckEsp,检查栈溢出013D142A cmp ebp,esp013D142C call @ILT+315(__RTC_CheckEsp) (13D1140h)013D1431 mov esp,ebp //ebp的值传给esp,也就是恢复调用前esp的值013D1433 pop ebp //弹出ebp,恢复ebp的值013D1434 ret  //把返回地址写入EIP中,相当于pop EIP
复制代码

 

call指令隐含操作push EIP,ret指令隐含操作 pop EIP,两条指令完全对应起来 
写到这里我们就可以分析一下main函数调用print_out函数前后堆栈(Stack)发生了什么变化,下面用一系列图说明
 
  
接下来是返回过程,从上面的013D1431 行代码开始
    
 
   
 
print_out函数调用前后,main函数的栈帧完全一样,perfect!
下面我们来看看print_out函数到底做了什么事情
int *p;p = (int*)(int(&begin) - 4);if(begin <= end)  *p -= 5;
根据上面调用print_out函数后的示意图,可以知道p实际上是指向了函数的返回地址addr,然后把addr-5,这又会发生什么?
再回头看一下反汇编的代码,
013D1560 push 0 //第一个实参压栈013D1562 call print_out (13D10FAh)//返回地址压栈,本例中是013D1567,然后调用print_out函数013D1567 add esp,8  //两个实参出栈
分析可知,返回地址addr的值是013D1567 ,addr-5为013D1562 ,把返回地址指向了call指令,结果是再次调用print_out函数,
从而print_out函数实现了打印从begin到end之间的所有数字,可以说是循环调用了print_out函数
 
对于add函数,主要是为了说明返回值存放于寄存器eax中。
 
另外,VS自身会提供一些安全检查
CheckStackVar安全检查http://blog.csdn.net/masefee/article/details/5630154,通过ecx和edx传递参数, 局部变量有数组时使用
__security_check_cookie返回地址检查, 数组长度大于等于5时使用
__RTC_CheckEsp程序栈检查,printf函数用使用
0 0