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

     要知道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函数的函数的ebp
00A81451  mov         ebp,esp                        //把esp的值赋值给ebp
00A81453  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相加得到1e
00A81B8B  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的值赋值给c
	return 0;
00A81496  xor         eax,eax                       //异或操作,使得eax清空,为了以后的使用
} 



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

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

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


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值