【c++】函数调用过程,4种调用约定

一、什么是栈

1.定义:

栈:数据结构中的栈,是一个容器,先进后出的属性;计算机系统中,栈是具有以上属性的动态内存区域,程序将数据压入栈,也可以从栈顶弹出,压栈使栈增大,出栈使栈减小,栈总是向下增长(由高地址向低地址);

如图4G虚拟地址空间布局:

ZONE_DMA的范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。

ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。

ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。

2.栈的作用、栈中内容

栈,保存了一个函数调用所需要的维护的信息,被称作堆栈帧或活动记录。堆栈帧有如下内容:

(1)函数的返回地址参数

(2)临时变量:函数内的非静态局部变量(static局部变量存放在.data或者.bss段)、编译时生成的其它临时变量

(3)保存上下文:包括函数调用前后需要保持不变寄存器

3.关于esp、ebp寄存器

在i386中,一个函数的活动记录用esp、ebp这两个寄存器划定范围。

esp寄存器始终指向栈顶部,同时也指向当前函数活动记录的顶部;ebp寄存器指向函数活动记录的一个固定位置。

ebp所指向的数据是调用该函数前ebp的值,这样在函数返回的时候,ebp就可以通过这个值恢复到调用前的值(我理解的就是栈帧回退) 。

4.几个汇编指令:

函数调用标准开头

(1)push ebp :把ebp压入栈中(成为old ebp)

(2)mov ebp,esp   :ebp=esp,即把ebp指向esp的位置,这是ebp指向栈顶,此时栈顶就是old ebp

(3)sub exp,xxx  :在栈生分配xxx字节的临时空间

(4)push xxx :如有必要,保存名为xxx的寄存器

函数调用标准结尾

(1)pop xxx :如有必要,回复保存过得寄存器

(2)mov esp,ebp   :esp=ebp,将esp指向ebp的位置,栈帧回退,回复esp同时回收局部变量空间

(3)pop ebp:从栈中恢复保存ebp的值,

(4)ret :从栈中取得返回地址,并跳转到该位置

其他:

(1)call fun1 (0C31127h):call相当于调用函数,这里重新设置了栈底ebp和栈顶esp

(2)add  esp,8   :算数运算指令 加法

(3)lea :装入有效地址

(参考汇编指令大全博客 https://blog.csdn.net/baishuiniyaonulia/article/details/78504758

二、函数参数带入、栈帧开辟

0.函数参数带入

源代码:

int fun1(int a, int b)
{
	int c = a + b;
	return c;
}
 
int main()
{
	int a = fun1(10, 20);
 
	return 0;
}

 查看汇编代码:首先设置断点,在main开始处,启动调试(VS必须处于调试状态才能看到汇编指令窗口),在当程序运行到断点处停止时,依次点击调试->窗口->反汇编;英文显示为"Debug"下的"Windows"子菜单,选择"Disassembly"。

 汇编代码如下:

main函数

int main()
{
00C31410  push        ebp  
00C31411  mov         ebp,esp  
00C31413  sub         esp,0CCh  
00C31419  push        ebx  
00C3141A  push        esi  
00C3141B  push        edi  
00C3141C  lea         edi,[ebp-0CCh]  
00C31422  mov         ecx,33h  
00C31427  mov         eax,0CCCCCCCCh  
00C3142C  rep stos    dword ptr es:[edi]  
	int a = fun1(10, 20);
00C3142E  push        14h  
00C31430  push        0Ah  
00C31432  call        fun1 (0C31127h)  
00C31437  add         esp,8  
00C3143A  mov         dword ptr [a],eax  
 
	return 0;
00C3143D  xor         eax,eax  
}
00C3143F  pop         edi  
00C31440  pop         esi  
00C31441  pop         ebx  
00C31442  add         esp,0CCh  
00C31448  cmp         ebp,esp  
00C3144A  call        __RTC_CheckEsp (0C3113Bh)  
00C3144F  mov         esp,ebp  
00C31451  pop         ebp  
00C31452  ret  

 调试进入main函数栈帧,EBP = 0075FA14,  ESP = 0075F93C ,ebp作为栈底寄存器,esp为栈顶寄存器,栈顶栈底唯一标志着一个函数调用栈。

如图main栈帧:

将汇编代码执行到函数调用的地方,查看函数调用参数带入的指令。

	int a = fun1(10, 20);
00C3142E  push        14h  
00C31430  push        0Ah  
00C31432  call        fun1 (0C31127h)  
00C31437  add         esp,8  
00C3143A  mov         dword ptr [a],eax  

首先观察这里的汇编代码,参数顺序是10,20,在这里汇编指令首先push 14h也就是20,再push 0Ah也就是10。从这里可以看出来,参数入栈的顺序是从右向左入栈的。

参数入栈示意图如图:

进入call fun

int fun1(int a, int b)
{
009813D0  push        ebp       //将main函数栈帧的栈底地址ebp入栈
009813D1  mov         ebp,esp   //将main函数栈顶地址赋给ebp寄存器,成为fun函数的栈底
009813D3  sub         esp,0CCh  //将main函数的栈顶地址esp减少0xc0,成为fun函数的栈顶
009813D9  push        ebx  
009813DA  push        esi  
009813DB  push        edi  
009813DC  lea         edi,[ebp-0CCh]  //将fun1函数的栈帧空间循环赋值为0xcccc cccc
009813E2  mov         ecx,33h  
009813E7  mov         eax,0CCCCCCCCh  
009813EC  rep stos    dword ptr es:[edi]  
	int c = a + b;
009813EE  mov         eax,dword ptr [a]  
009813F1  add         eax,dword ptr [b]  
009813F4  mov         dword ptr [c],eax  
	return c;
009813F7  mov         eax,dword ptr [c]  
}

 

函数堆栈调用开栈过程:

1.压入实参,自右向左

2.压入下一行指令地址

3.压入调用方函数的栈底地址(本例中为main的栈底esp)

4.调转到被调用方函数栈帧(本例中为 fun函数)

5.开辟被调用方函数所需要运行的空间(本例中fun函数所需空间位C0)

1.<=8KB参数的带入

接下来我们看看参数入栈会不会根据参数的大小而产生区别。

struct Tmp
{
    char a;
};//大小为1字节

int fun1(struct Tmp a, struct Tmp b)
{
    return 0;
}

int main()
{
    struct Tmp tmp1, tmp2;
    tmp1.a = 10;
    tmp2.a = 20;
    int a = fun1(tmp1,tmp2);
    return 0;
}

在这里我们给函数传入两个一字节大小的参数,观察其汇编码。

int a = fun1(tmp1,tmp2);
00991A46  movzx       eax,byte ptr [tmp2] 
00991A4A  push        eax 
00991A4B  movzx       ecx,byte ptr [tmp1] 
00991A4F  push        ecx 
00991A50  call        fun1 (09911E5h) 

在这里,我们看看,首先从tmp2中取出值,然后push,再从tmp2中取值,然后push,由此可见,一个字节的参数,采用的是利用push入栈的方式将参数带入。

我们修改代码,将Tmp结构体的大小改为4个字节,观察其入栈 方式

struct Tmp
{
    int a;
};//大小为4字节

int fun1(struct Tmp a, struct Tmp b)
{
    return 0;
}

int main()
{
    struct Tmp tmp1, tmp2;
    tmp1.a = 10;
    tmp2.a = 20;
    int a = fun1(tmp1,tmp2);
    return 0;
}

 

观察其fun1函数调用的汇编码  

其汇编码和Tmp结构体为一个字节时候无异,说明在参数为4字节时候,函数参数依旧采用push入栈的方式带入。

  int a = fun1(tmp1,tmp2);
00EE365C  mov         eax,dword ptr [tmp2] 
00EE365F  push        eax 
00EE3660  mov         ecx,dword ptr [tmp1] 
00EE3663  push        ecx 
00EE3664  call        fun1 (0EE11E5h) 

接下来来看看Tmp结构体的大小为8字节时候的入栈方式

struct Tmp
{
    int a;
    int b;
};//大小为8字节

int fun1(struct Tmp a, struct Tmp b)
{
    return 0;
}

int main()
{
    struct Tmp tmp1, tmp2;
    tmp1.a = 10;
    tmp1.b = 15;
    tmp2.a = 20;
    tmp2.b = 25;
    int a = fun1(tmp1,tmp2);
    return 0;
}

fun1函数调用时候的汇编代码 

   int a = fun1(tmp1,tmp2);
002D366A  mov         eax,dword ptr [ebp-18h] 
002D366D  push        eax 
002D366E  mov         ecx,dword ptr [tmp2] 
002D3671  push        ecx 
002D3672  mov         edx,dword ptr [ebp-8] 
002D3675  push        edx 
002D3676  mov         eax,dword ptr [tmp1] 
002D3679  push        eax 
002D367A  call        fun1 (02D11E5h) 
当前的ebp值为0x0095FD60,那么ebp-18h是谁呢?我们来看看。
0x0095FD44  14 00 00 00  ....   tmp2.a                        ebp-1ch    tmp2
0x0095FD48  19 00 00 00  ....   tmp2.b                        ebp-18h
0x0095FD4C  cc cc cc cc  ????   缓冲层                        ebp-14h
0x0095FD50  cc cc cc cc  ????   缓冲层                        ebp-10h
0x0095FD54  0a 00 00 00  ....   tmp1.a                        ebp-0ch    tmp1
0x0095FD58  0f 00 00 00  ....   tmp1.b                        ebp-8
0x0095FD5C  cc cc cc cc  ????    vs编译器变量中间会有防止越界的缓冲层  ebp-4
0x0095FD60  b0 fd 95 00  ???.    ebp

首先push的是ebp-18h的四个字节,也就是tmp2.b,然后是tmp2地址上取四个字节,就是tmp2.a。再push的是ebp-8的四个字节也就是tmp1.b,然后是tmp1地址上取四个字节,也就是tmp1.a。由此可见,函数参数是8个字节的时候,依旧是利用push入栈的方式将参数传递。

2.>8KB的参数带入

再看看参数大小如果为12字节时候的参数传递方式。
struct Tmp
{
	int a;
	int b;
	int c;
};//大小为12字节

int fun1(struct Tmp a, struct Tmp b)
{
	return 0;
}

int main()
{
	struct Tmp tmp1, tmp2;
	tmp1.a = 10;
	tmp1.b = 15;
	tmp1.c = 16;
	tmp2.a = 20;
	tmp2.b = 25;
	tmp2.c = 26;
	int a = fun1(tmp1,tmp2);
	return 0;
}
利用调试,将查看函数调用时候的汇编码
	int a = fun1(tmp1,tmp2);
008F3C58  sub         esp,0Ch     //给esp栈顶指针减少12
008F3C5B  mov         eax,esp     //将esp的值给eax寄存器
008F3C5D  mov         ecx,dword ptr [tmp2]  //将tmp.a的值写入ecx寄存器
008F3C60  mov         dword ptr [eax],ecx   //将ecx寄存器的值写入eax寄存器的地址
008F3C62  mov         edx,dword ptr [ebp-20h]  //将tmp.b的值写入edx寄存器
008F3C65  mov         dword ptr [eax+4],edx  //将edx寄存器的值写入eax寄存器的地址
008F3C68  mov         ecx,dword ptr [ebp-1Ch]  //将tmp.c的值写入ecx寄存器
008F3C6B  mov         dword ptr [eax+8],ecx  //将ecx寄存器的值写入eax寄存器的地址
008F3C6E  sub         esp,0Ch  
008F3C71  mov         edx,esp  
008F3C73  mov         eax,dword ptr [tmp1]  
008F3C76  mov         dword ptr [edx],eax  
008F3C78  mov         ecx,dword ptr [ebp-0Ch]  
008F3C7B  mov         dword ptr [edx+4],ecx  
008F3C7E  mov         eax,dword ptr [ebp-8]  
008F3C81  mov         dword ptr [edx+8],eax  
008F3C84  call        fun1 (08F11E5h)  

从上面的汇编代码可以看出,在函数参数为12字节的时候,其参数带入方式和小于等于8字节的时候不同,在这里没有直接的push参数,而是先在main函数的栈顶向上移动12字节,然后将参数的数据拷贝到main函数栈顶开辟的内存中。其方式如图

 

三、函数返回值返回

1.<=8KB

返回值为4字节的时候

int fun1(int a, int b)

{

    int c = a + b;

    return c;

}

int main()

{   int a = 10;

    int b = 20;

    int c = fun1(a,b);

    return 0;

        }

 

        先运行到函数返回值代码处,查看汇编码

return c;

003413E7  mov         eax,dword ptr [c] 

 

接收返回值处查看汇编码

int c = fun1(a,b);

……

00343C44  call        fun1 (03411EAh) 

00343C49  add         esp,8 

00343C4C  mov         dword ptr [c],eax  //将eax寄存器的值写入到c的地址中,写入四字节

如汇编码可知,四字节的返回值利用寄存器带回,然后将寄存器的值写入到接受返回值的变量中

 

返回值为8字节的时候

struct Tmp

{

    int a;

    int b;

};//大小为8字节

 

struct Tmp fun1(int a, int b)

{

    struct Tmp c;

    c.a = a;

    c.b = b;

    return c;

}

int main()

{

    int a = 10;

    int b = 20;

            struct Tmp c;

    c = fun1(a,b);

    return 0;

}

先将代码运行到函数返回值出,查看其汇编代码

    return c;

00B7142A  mov         eax,dword ptr [c] 

00B7142D  mov         edx,dword ptr [ebp-8]

 

将代码运行到接收返回值处,查看其汇编代码

c = fun1(a,b);

……

00D63C44  call        fun1 (0D611EFh) 

00D63C49  add         esp,8 

00D63C4C  mov         dword ptr [c],eax 

00D63C52  mov         dword ptr [ebp-20h],edx

在接收返回值时候,将寄存器的值写入到接收返回值的变量中。

2.>8KB

struct Tmp
{
    int a;
    int b;
    int c;

};//大小为12字节

 struct Tmp __stdcall fun1(int a, int b)
{
    struct Tmp c;
    c.a = a;
    return c;

}
int main()
{
    int a = 10;
    int b = 20;
    struct Tmp c;
    c = fun1(a,b);
    return 0;
 }

  c = fun1(a,b);

00953C3C  mov         eax,dword ptr [b] 

00953C3F  push        eax 

00953C40  mov         ecx,dword ptr [a] 

00953C43  push        ecx 

00953C44  lea         edx,[ebp-0FCh] 

00953C4A  push        edx 

00953C4B  call        fun1 (09511F4h)  

从上面的汇编码可以看出来,当函数的返回值为12字节的时候,在参数入栈的最后还入栈了一个寄存器,该寄存器中存储的是一块位于main栈帧上的内存。

    return c;

00951A44  mov         eax,dword ptr [ebp+8] 

00951A47  mov         ecx,dword ptr [c] 

00951A4A  mov         dword ptr [eax],ecx 

00951A4C  mov         edx,dword ptr [ebp-0Ch] 

00951A4F  mov         dword ptr [eax+4],edx 

00951A52  mov         ecx,dword ptr [ebp-8] 

00951A55  mov         dword ptr [eax+8],ecx 

00951A58  mov         eax,dword ptr [ebp+8]

而在返回值的时候,先从ebp-8位置取值,去除的正好是参数入栈之后入栈的一块main函数栈帧上的地址,然后将返回的数据写入到该块内存上。

由此可见,当返回值大于8字节的时候是预先在调用方的栈帧上预留一块内存,作为函数返回值存储位置,最后返回值的时候,将返回值的数据写入到该段内存。大致结构如图

四、函数栈帧回退

函数栈帧的回退分为两步,一步是函数栈帧的回退,另一步时函数参数的清除。

函数栈帧回退汇编码如下

 

00EC13E0  pop         edi       将栈帧开辟时候入栈的寄存器出栈

00EC13E1  pop         esi 

00EC13E2  pop         ebx 

00EC13E3  mov         esp,ebp     让esp = ebp

00EC13E5  pop         ebp         让ebp等于栈帧开辟时候入栈的main栈帧,并将其出栈

00EC13E6  ret                     返回

 

经过上面的过程,栈帧就已经回退到了main函数,也就是调用方的栈帧。

下一步,函数参数的清除

00EC1484  call        _fun1 (0EC118Bh) 

00EC1489  add         esp,8    让esp+8清除参数内存

六、4种调用约定

1.定义:

(参考《程序员的自我修养》第10章)

何为调用约定?即调用方和被调用方对于函数的如何调用必须有一个明确的约定,只有双方都遵循这样的约定,函数才能被调用。

2.内容:

约定一般有如下几个方面:

(1)函数参数的传递顺序和方式:通过栈传递?使用寄存器传递?从右向左?从左向右?

(2)栈的维护方式:出栈由函数调用方完成?函数本身完成?

(3)名字修饰的策略:c语言中对于fun函数的声明: int _cdcel foo()

3.种类

1.cdecl为c/c++默认调用惯约定(书中将调用约定也叫调用惯例)

2.thiscall调用约定:为c++的一种特殊调用约定,用于对类成员函数的调用。

    VC里是this指针存放在ecx寄存器中,参数从右向左压栈;

   gcc中,thiscall和cdecl完全一样,只是将this看做是函数的第一个参数

发布了89 篇原创文章 · 获赞 68 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/zDavid_2018/article/details/96474689
今日推荐