函数堆栈调用

本文通过分析汇编代码,详细解释了函数调用堆栈的工作原理,包括形参内存分配、入栈顺序、返回值处理、调用结束后的栈帧回退以及如何确定执行下一行指令。讨论了形参由调用方开辟内存,入栈顺序为从右至左,返回值通过eax寄存器带出,函数调用完成后通过栈底指针回退到调用方栈帧,并使用ret指令获取下一行执行地址。
摘要由CSDN通过智能技术生成

关于函数的调用堆栈有如下几个问题:

1.形参开辟内存吗?由谁开辟?

2.形参的入栈顺序?

3.返回值如何带出?

4.被调用方结束后如何知道回退到调用方栈帧上?

5.函数调用完成如何知道执行下一行指令?

测试用例:

#include<stdio.h>

typedef struct Node{
  int data[1];
}Node,*PNode;

Node fun(int a,int b)
{
    Node tmp={0};
	tmp.data[0]=a+b;
   return tmp; 
}

int main()
{
	int a=5;
	int b=6;
    Node tmp=fun(a,b);    
	return 0;
}

上面代码的汇编代码:

 

 

 

 

首先 ebp是栈底指针寄存器(存放调用方栈底指针的地址)

        esp是栈顶指针寄存器 (每次往栈里面压入一个东西,esp就会向上挪动一次,保证esp一直指的是栈顶)

解析:

00401060   push        ebp                     将调用main函数的mainCRTStartup函数的栈底的地址压栈                       

00401061   mov         ebp,esp              将esp的值赋给ebp,就是将ebp所指向的内容改变为mainCRTStartup函数栈的顶部也是

                                                               main函数栈的底部

00401063   sub         esp,4Ch               将esp的值减去16进制数的4Ch个,首先我们要知道内存中栈是由高地址向低地址使用,                                                                  那么减去4Ch就是向再去开辟4Ch大小的空间,esp指向esp-4Ch的地址
00401066   push        ebx                   
00401067   push        esi  
00401068   push        edi                                     分别压入了 ebx,esi和edi

00401069   lea         edi,[ebp-4Ch]                   将ebp-4Ch的地址放进edi中,即让edi指向ebp-4Ch的地址也就是esp指向的地址                                                                               
0040106C   mov         ecx,13h                           将16进制的13放进ecx中

00401071   mov         eax,0CCCCCCCCh        将CCCCCCCC放入eax中
00401076   rep stos    dword ptr [edi]                  从edi指向的地址开始,拷贝eax值 ecx次,一次拷贝dword个字节     

                                                                               (d代表double,word代表两个字节),即一次拷贝4个字节

                                                                                 刚好把开辟的4Ch个空间置为CCCCCCCC

 

16:       int a=5;                        
00401078   mov         dword ptr [ebp-4],5             将5放进从ebp-4开始4个字节大小的地址中

17:       int b=6;
0040107F   mov         dword ptr [ebp-8],6             将6放进从ebp-8开始4个字节大小的地址中

 

18:       Node tmp=fun(a,b);
00401086   mov         eax,dword ptr [ebp-8]          将从ebp-8开始4个字节大小的地址所存放的数据(即 b的值)放进eax中
00401089   push        eax                                      将eax压栈
0040108A   mov         ecx,dword ptr [ebp-4]          将从ebp-4开始4个字节大小的地址所存放的数据(即 a的值)放进ecx中    
0040108D   push        ecx                                      将ecx压栈

由此可知:

问题1:形参开辟内存,由调用方开辟

问题2: 形参的入栈顺序为从右至左

 

0040108E   call        @ILT+0(_fun) (00401005)                  call语句,也就是跳转语句,转去另一条指令

                                                                                           (_fun后面的遗传16进制数字    就是转至的位置)

                                                                                           call在被运行时,首先会将call指令的下一条指令的地址压入栈中

00401005   jmp      (fun )00401020                                    指令已经跳转过来了,但是过来时候是一句jmp语句,

                                                                                          即跳转到后面的函数,也就是fun函数地址为(00401020)

 

进入fun()函数

00401020   push        ebp                                             将调用fun函数的main函数的栈底的地址压栈  
00401021   mov         ebp,esp                                      将esp的值赋给ebp,就是将ebp所指向的内容改变为

                                                                                       main函数栈的顶部也是fun函数栈的底部

00401023   sub         esp,44h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]                                       这和main函数创建时的代码是一样的,目的是为函数开辟空间

                                                                                                    和将所开辟的空间初始化为CCCCCCCC,让ebp从指向main函数                                                                                                         栈底改为指向fun函数的栈底

 Node tmp={0};
00401038   mov         dword ptr [ebp-4],0                                 将0放进从ebp-4开始4个字节大小的地址中
10:       tmp.data[0]=a+b;
0040103F   mov         eax,dword ptr [ebp+8]                            将从ebp+8开始4个字节大小的地址所存放的数据(即 形参a的值)放进                                                                                                         eax中
00401042   add          eax,dword ptr [ebp+0Ch]                      将从ebp+0C开始4个字节大小的地址所存放的数据(即 形参b的值)                                                                                                      与eax相加并将结果放入eax中  
00401045   mov         dword ptr [ebp-4],eax                          将eax的值放到从ebp-4开始4个字节大小的地址(即 tmp的内存中)
11:      return tmp;
00401048   mov         eax,dword ptr [ebp-4]                        将ebp-4开始4个字节大小的地址所存放的数据(即 tmp的值)

}                                                                                              放到eax中

 

由此可知:

问题3:通过将返回值放入eax寄存器中将返回值带出 但这只适用于返回值大于0 并且小于等于4个字节之间。

 

当返回值大于四个字节并且小于等于8个字节时:

将结构体Node中data数组的长度改为2测试:

从图中可以看出当返回值大于四个字节并且小于等于8个字节时 :通过eax和edx两个寄存器将返回值带出

 

如果当返回值大于8个字节时:

将结构体Node中data数组的长度改为3测试:

main函数:

lea          eax,[ebp-20h]            的含义是将ebp-20的地址赋给eax寄存器

push        eax                           将ebp-20的地址压栈

fun函数

上面代码用图描述:

 

从图中可以看出:

当返回值大于8个字节时,将调用方函数栈帧的一部分内存当做临时变量并将这个临时变量地址压栈,将返回值数据

存入此临时变量内存中,并将此临时变量的地址存入eax寄存器中,利用临时变量将返回值带出。

 

下面继续: 
0040104B   pop         edi                                                    将esp当前所指的内容赋给edi,再将其弹出,即esp向下移四个字节
0040104C   pop         esi                                                    将esp当前所指的内容赋给esi,再将其弹出,即esp向下移四个字节
0040104D   pop         ebx                                                   将esp当前所指的内容赋给ebx,再将其弹出,即esp向下移四个字节
0040104E   mov         esp,ebp                                           将ebp 的内容赋给esp,即让esp直接指向fun函数的栈底
00401050   pop         ebp                                                   当前ebp和esp指向的都是fun函数的栈底,所以如果再进行pop指令

                                                                                          弹出的就是刚才为fun函数开辟空间之前压入的main函数的栈底的地                                                                                                     址,所以现在这句指令正好就是将ebp重新指向main函数的栈底。

由此可知:

问题4:通过将调用方函数的栈底的地址压入被调用方函数的栈底实现被调用函数结束后回退到调用方函数栈帧上。

 

00401051   ret                                                                ret 这个指令的意思是再次弹出一个值并付给 PC寄存器

                                                                                       而在跳转到fun函数将main函数栈底地址压进fun函数栈底之前

                                                                                        在main函数中,将call指令的下一句指令地址压栈,

                                                                                       即此时出栈的数据为main函数中调用fun函数的下一行指令的地址

 由此可知:

问题5:在将调用方函数栈底的地址压入被调用方函数栈底前先将调用方的call指令下一行指令的地址压栈实现函数调用完成后知道执行下一行指令的地址。

00401093   add         esp,8                                                   将esp向下移动8个字节,刚好把main中为形参a,b开辟的空间销毁

                                                                                               由此也知,系统销毁空间时只是将栈顶指针指向的位置进行修改

                                                                                               而并没有将使用过的内存中的数据进行清空。                                  

00401096   mov         dword ptr [ebp-0Ch],eax                    将eax中的值放进从ebp-0C开始4个字节大小的地址中

                                                                                                   (即 tmpd的内存中)

 



 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值