进程的地址空间与函数调用过程

来源:互联网 发布:零基础学qt4编程pdf 编辑:程序博客网 时间:2024/06/05 16:58

     要知道C语言的函数调用过程,首先要明白C语言中的各部分代码都出现在什么段。

首先来看一串代码,代码中的各个部分都有自己对应的段,换句话说每个段都存有C语言中的各个部分代码,而这所有的代码组合起来才成为一个完整的C语言代码。只有在知道C语言各部分代码出现在什么段之后,就可以进一步了解C语言中的函数调用过程。(该程序是在Linux中创建)

   

       当然在知道C语言中的各个部分对应的段之后,我们就可以研究一下C语言中的函数调用过程。但在这之前,有一个知识还是必须知道,那就是当我们程序执行起来之后,可执行文件加载到内存之后如何分布。还是以刚刚的a.out为例。

 

          知道了以上内容之后,下面我们就可以开始核心内容了,函数的调用原理--栈帧

我们都知道栈是C语言中的一个很重要的内容,首先栈是向下生长的,所谓向下生长是指从内存高地址->低地址的路径延伸,所以我们就得到两个很重要的东西,就是栈有栈底和栈顶,由于栈的特性,栈顶的地址要比栈底的低。对于×86体系的CPU而言,其中

------>寄存器ebp(base pointer)可称为“帧指针”或“基址指针”,两者的语义是相同的。

------>寄存器esp(base pointer)可称为“栈指针”。

         要知道的是:

------->ebp在未接受改变之前是一直指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。

------->esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。

我们图文结合简单说明一下:假设函数A调用函数B,我们称A函数为“调用者”,B函数为“被调用者”则函数调用过程可以简单的描述:

(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前的任务信息。

(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用的栈底)。

(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作调用者B的栈空间。

(4)函数B返回之后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置,然后调用者A再从栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B之前的位置,也就是栈恢复函数B调用前的状态。

这个过程由以下两条指令来完成:

         move  %ebp , %esp

         pop   %ebp

这个过程用图简单的可以表示为:

          

          下面以一个简单的函数为例子,Add()函数,实现两个数的相加,源程序很简单。

#include<stdio.h>int Add(int num1, int num2){int z = 0;z = num1 + num2;return z;}int main(){int a = 10;int b = 20;int c = 0;c = Add(a, b);return 0;}
          我们就用这个例子来简单说一说C语言中的函数调用过程。首先我们必须要知道我们经常用的main()函数,实际上也是调用的,main()函数是被__mainCRTStarup(或者说是__TmianCRTStarup)调用。知道这个之后我们就用图来表示:



由于我们的main()函数是被其他函数调用的,所以在这之前栈指针esp和帧指针ebp最初都是如图所示的样子,但是在去调用main()函数之前,esp指针会向上移动,空出来的这个空间放什么呢?这个后面会提到。接着进入main()函数。我们看main()函数的汇编代码:(为了方便直接把图放在汇编语言中)

int main(){00A81450  push        ebp                            //在esp指向的上面开辟4个字节,用来存储调用main函数的函数的ebp00A81451  mov         ebp,esp                        //把esp的值赋值给ebp00A81453  sub         esp,0E4h                       //利用sub指令预开辟开辟一个空间给main()函数00A81459  push        ebx                00A8145A  push        esi  00A8145B  push        edi                            //以上三条指令暂且不用关注,认为放入三个值即可00A8145C  lea         edi,[ebp-0E4h]                 //把ebp-0E4h放入edi之中00A81462  mov         ecx,39h  00A81467  mov         eax,0CCCCCCCCh                 00A8146C  rep stos    dword ptr es:[edi]             //把eax中的值循环拷贝39h次,放入edi开始的地址,与上面的指令连一起起到赋初值0cccccccc的作用int a = 10;00A8146E  mov         dword ptr [a],0Ah                   int b = 20;00A81475  mov         dword ptr [b],14h  int c = 0;00A8147C  mov         dword ptr [c],0                    //依次将a,d,c的值入栈            c = Add(a, b);


依次放入a,b,c的值,当放入a的值之后发现出现一个0A

之后继续放入b和c

不过仔细一点会发这里的a,b,c之间的地址相互差12,不是4,这取决与你是怎么定义的,

帧栈图如下:


00A81483  mov         eax,dword ptr [b]             //放入14h   相当于形参的拷贝00A81486  push        eax                           00A81487  mov         ecx,dword ptr [a]             //放入0Ah   相当于形参的拷贝00A8148A  push        ecx  00A8148B  call        _Add (0A811EAh)                      //下面进入Add()函数_Add:00A811EA  jmp         Add (0A81B60h)            //跳转指令int Add(int num1, int num2){00A81B60  push        ebp                        //与之前类似的操作    00A81B61  mov         ebp,esp  00A81B63  sub         esp,0CCh                   //开辟大小,具体的大小由函数的参数个数决定00A81B69  push        ebx  00A81B6A  push        esi  00A81B6B  push        edi  00A81B6C  lea         edi,[ebp-0CCh]            //开辟栈帧并初始化00A81B72  mov         ecx,33h  00A81B77  mov         eax,0CCCCCCCCh  00A81B7C  rep stos    dword ptr es:[edi]             //这些过程与上面的的过程类似         int z = 0;00A81B7E  mov         dword ptr [z],0      z = num1 + num2;00A81B85  mov         eax,dword ptr [num1]      //将之前压入栈的值10取出00A81B88  add         eax,dword ptr [num2]      //将20取出并于10相加得到1e00A81B8B  mov         dword ptr [z],eax         //将得到的1e赋值给z    return z;00A81B8E  mov         eax,dword ptr [z]         //返回机制,将z也就是ebp-4的地址赋值给eax,由eax携带回去

函数到这里调用基本接近尾声了,下面就开始函数的销毁。

00A81B91  pop         edi  00A81B92  pop         esi  00A81B93  pop         ebx                          //指针撤回,销毁内容00A81B94  mov         esp,ebp               00A81B96  pop         ebp                          //栈里的元素pop出来,并赋值给ebp,栈帧的返回00A81B97  ret                                      //会pop出一个元素,用这个元素找到原先call指令的下一条地址

       当函数执行return z指令之后,会将返回值的值放在eax之中,由eax传给c,所以当调用完成之后c的值变为由eax之中传来的值。所以c的值在这之后会发生改变,变成c = 30。

00A81490  add         esp,8                         //esp+8,将刚刚的形参拷贝销毁00A81493  mov         dword ptr [c],eax             //将eax的值赋值给creturn 0;00A81496  xor         eax,eax                       //异或操作,使得eax清空,为了以后的使用} 



00A81498 pop edi00A81499 pop esi00A8149A pop ebx                                      //指针撤回,销毁内容,每次都有00A8149B add esp,0E4h                                 //main()函数的销毁00A814A1 cmp ebp,esp                                  //编译器在这里做的一个esp的检测00A814A3 call __RTC_CheckEsp (0A8113Bh)               //调用指令,到这里main()函数的调用基本结束了00A814A8 mov esp,ebp                                  //ebp赋值给esp00A814AA pop ebp                                      //ebp出栈00A814AB ret                                          //返回指令会将抛出一个元素,为下一条指令的地址

最后销毁:main()函数即可。

总结起来函数调用过程其实挺复杂的,若是一个复杂的程序会更加复杂。这里只是用一个简单的程序简单分析一下。其实具体更深的内容还得自己多去调试。


1 0