我是靠谱客的博主 昏睡金针菇,最近开发中收集的这篇文章主要介绍进程的地址空间与函数调用过程,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

     要知道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()函数即可。

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


最后

以上就是昏睡金针菇为你收集整理的进程的地址空间与函数调用过程的全部内容,希望文章能够帮你解决进程的地址空间与函数调用过程所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(31)

评论列表共有 0 条评论

立即
投稿
返回
顶部