函数的堆栈调用

让我们带着问题来阅读本篇文章

  1. 型参在哪里开辟内存?
  2. 型参的入栈顺序?
  3. 函数返回值怎么带出来?
  4. 函数的返回值为什么会回退到栈里?
  5. 函数调用结束为什么会沿着调用点继续执行?

我们先来了解一下堆与栈是怎样的一种存在

什么是栈?

栈用于维护函数调用的上下文,离开栈,函数就没有办法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节大小。

栈在程序运行中具有举足轻重的地位。最重要的是,栈保存了一个函数调用所需要的的维护信息,这常常被称为堆帧栈或者活动记录。堆栈帧一般包括以下几个内容:

  • 帧栈是一个main函数的活动空间范围。
  • 函数的返回地址和参数。
  • 临时变量:包括函数的非静态局部变量以及编辑器自动生成的其他临时变量。
  • 保存的上下文:包括在函数调用前后需要保持不变寄存器。

什么是堆?

堆是用来容纳应用程序动态分配内存的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里,堆也可能没有固定统一的存储区域。堆一般比栈大很多,可能有几十数百兆自己的容量。

认识函数堆栈调用的一些简单指令

mov  移值指令

lea    移地址

push  用栈

pop    出栈

call    

  • 压入下一行指令地址
  • jump到被调用方函数

认识寄存器的存在

寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。

寄存器的用途:

1.可将寄存器内的数据执行算术及逻辑运算。

2.存于寄存器内的地址可用来指向内存的某个位置,即寻址。

3.可以用来读写数据到电脑的周边设备。

eax   是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。

add eax,4;      eax+=4

sub eax,4;       eax-=4

ebx   是"基地址"(base)寄存器, 在内存寻址时存放基地址。

ecx   是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。

edx   总是被用来放整数除法产生的余数。

ebp   栈底指针寄存器

esp   栈顶指针寄存器

pc    下一行指令寄存器

在i386中,一个函数的活动记录用ebp(栈底指针寄存器)和esp(栈顶指针寄存器)这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。而相对的,ebp的寄存器指向了函数活动记录的一个固定范围。

函数堆栈调用的实质

用下边的简单代码举例了解一下

int Add(int a,int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int a=10;
	int b=20;
	int c = Add(a,b);
	printf("%d",c);
	return 0;
}

型参开辟内存:调用方(如下代码)

main 函数栈布局:

int main()
{
011943A0  push        ebp  
011943A1  mov         ebp,esp  
011943A3  sub         esp,0E4h  
011943A9  push        ebx  
011943AA  push        esi  
011943AB  push        edi  
011943AC  lea         edi,[ebp-0E4h]  
011943B2  mov         ecx,39h  
011943B7  mov         eax,0CCCCCCCCh  
011943BC  rep stos    dword ptr es:[edi]  
	int a=10;
011943BE  mov         dword ptr [a],0Ah //在栈上开辟空间,存放[a] 
	int b=20;
011943C5  mov         dword ptr [b],14h  //开辟空间,存放[b]
	int c = Add(a,b);
011943CC  mov         eax,dword ptr [b]  //将[b]移入到寄存器eax中
011943CF  push        eax                //将eax压栈
011943D0  mov         ecx,dword ptr [a]  //将a的值移入到寄存器ecx中,
011943D3  push        ecx                 //将ecx压栈
011943D4  call        Add (0119110Eh) //call:调用被调用方函数(Add函数),压入下一行指令地址
011943D9  add         esp,8           //栈顶指针移动
011943DC  mov         dword ptr [c],eax  
	printf("%d",c);
011943DF  mov         esi,esp  
011943E1  mov         eax,dword ptr [c]  //将[c]的值放入eax中
011943E4  push        eax                //eax压栈
011943E5  push        119CC70h  
011943EA  call        dword ptr ds:[11A03B8h]  
011943F0  add         esp,8  
011943F3  cmp         esi,esp  
011943F5  call        __RTC_CheckEsp (011912D5h)  
	return 0;
011943FA  xor         eax,eax  
}
 

被调用方:(如下代码)

Add函数栈帧开辟:

int Add(int a,int b)
{
011928F3  sub         esp,0CCh  
011928F9  push        ebx  
011928FA  push        esi  
011928FB  push        edi  
011928FC  lea         edi,[ebp-0CCh]  
01192902  mov         ecx,33h  
01192907  mov         eax,0CCCCCCCCh  
0119290C  rep stos    dword ptr es:[edi]  
	int c = a + b;                    //将运算结果保存到局部变量中
0119290E  mov         eax,dword ptr [a]  
01192911  add         eax,dword ptr [b]  
01192914  mov         dword ptr [c],eax  
	return c;
01192917  mov         eax,dword ptr [c]  //通过寄存器eax将返回值带回
}
0119291A  pop         edi  
0119291B  pop         esi  
0119291C  pop         ebx  
0119291D  mov         esp,ebp  //释放Add函数的栈帧空间
0119291F  pop         ebp  
01192920  ret  

函数调用过程

  1. 开辟型参的内存并初始化为0xcccccccc
  2. 压入下一行指令地址
  3. 压入调用方栈底指针的值
  4. 开辟局部变量所需要的栈空间并初始化

函数调用完成后的清栈

  1. 清理栈开辟的局部变量
  2. pop ebp(栈底指针寄存器)   ebp回退到调用方栈底
  3. ret(pop pc)

ret包含两个动作:
1、出栈 ,栈顶指针向下移。出栈元素是下一行指令地址,赋给PC寄存器,PC寄存器永远存放下一行指令的地址。
2、ret运行完以跳转到下一行指令的地址

清栈的意义:告诉寄存器内存可以再次被分配。

函数的调用约定:

(1)thiscall: 类成员方法的调用约定,this指针存放于CX寄存器,参数从右到左压。

          内置类型的调用约定(Calling convention):

(2)_cdecl:C标准的调用。型参由调用方开辟,被调用方清理。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。

(3)__stdcall:windows标准的调用约定,型参由调用方开辟,被调用方清理。

(4)__fastcall:快速调用约定,它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。

约定的意义:

1)约定了函数符号的生成。(不同的函数约定,函数符号的生成不一样)

2)约定了函数参数的压栈顺序。

3)约定了型参的开辟和清理方式。

接下来我们解决文章开头的问题???

1、型参在哪里开辟内存?

    型参在栈上开辟内存,由调用方开辟。

2、型参的入栈顺序?

   型参从右向左入栈,如果从左至右的话,不知道实参的传递个数。

3、函数返回值怎么带出来?

  函数值存放到寄存器中,由寄存器带回

eax带回返回值:

(1)0<返回值<=4       由eax带回

(2)4<返回值<=8       由eax edx  两个寄存器带回

(3)8<返回值             由临时量带回(非类类型)

非类类型的返回方式:

  • 临时量地址压栈
  • 返回值拷到临时量里
  • 临时量带回返回值

4、函数的返回值为什么会回退到栈里?

  因为压栈压的是调用方的栈底指针

5、函数执行完成后怎么控制沿用点继续执行?

  通过call指令继续。

call指令的作用:

(1)压入下一行指令的地址

(2)jump到被调用方函数

Tips:

实参与型参:

  • 型参:用来接收调用该方法时传递的参数,被调用时分配空间,调用结束后就释放。
  • 实参:传递给被调用方法的值,预先创建并赋予确定值。
int Add(int a,int b)//型参,用来接收调用该方法时传递的参数
{
	int c = a + b;
	return c;
}
int main()
{
	int a=10;//实参,传递给被调用方法的值
	int b=20;
	int c = Add(a,b);
	printf("%d",c);
	return 0;
}

函数定义与函数说明:

  • 声明:说明函数作用

int Add(int a,int b)  //求两个整型数之和

  • 定义:定义函数的实现功能,实现函数功能时,函数的名称,返回值,参数表必须要与此函数声明时一致。

int Add(int a,int b)//实现函数
{
    int c = a + b;
    return c;
}
 

  • 当一个函数的定义在这个函数的调用之前,则不用声明,否则需要声明这个函数。

不需要声明

int Add(int a,int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int a=10;
    int b=20;
    int c = Add(a,b);
    printf("%d",c);
    return 0;
}

需要声明

int Add(int a,int b);//函数声明,需要加分号
int main()//被调用的函数在调用之后
{
    int a=10;
    int b=20;
    int c = Add(a,b);
    printf("%d",c);
    return 0;
}

int Add(int a,int b)
{
    int c = a + b;
    return c;
}

 

猜你喜欢

转载自blog.csdn.net/FoXiShaoNv/article/details/84554837