函数调用栈

我们常用函数,知道使用函数时会跳到函数定义的代码段去执行,然后执行完后再返回到调用函数去,但以下的一些问题却仍不清楚。

这个调用过程的原理是什么
调用函数前要做什么事情
函数的参数是如何传递的
如何跳转到被调用函数
执行完被调函数后如何返回调用函数并且保证能接着运行

要知道这些,需要结合代码的反汇编来看。
写了一段简单的函数调用的代码
函数调用堆栈


以下为main函数的反汇编
函数调用堆栈
其中ebp为栈底指针,esp为栈顶指针。
可以看到我们所说的指令 比如int a=15;这个指令,它靠的是ebp栈底指针的偏移量来确定某处有个4个字节的地方存储15这个值的。
具体汇编就是
00EA2C2E  mov    dword ptr [a],0Fh 

00EA2C2E是 mov  dword ptr [a],0Fh 这个指令在代码段的地址,这个地址是虚拟地址空间地址。并非物理地址
0Fh就是15的十六进制
word代表2个字节,dword代表4个字节 
ptr[a]就是a的地址处,其实这个的真正模样应该是 ptr[ebp-4]

这句汇编的意思就是执行将0Fh这个值移动到ebp-4这个地址,占用4个字节,简单来讲就是给栈底上的4个字节后赋值15,也占4个字节。


后面
     int b=10;
00EA2C35  mov         dword ptr [b],0Ah  
   int result=0;
00EA2C3C  mov         dword ptr [result],0  
就是分别压入了2个整型的0入栈

重点来了
到了函数调用这块了,我们看看反汇编

result=sum(a,b);
00EA2C43  mov         eax,dword ptr [b]  
00EA2C46  push        eax  
00EA2C47  mov         ecx,dword ptr [a]  
00EA2C4A  push        ecx  
00EA2C4B  call        sum (0EA143Dh)  
00EA2C50  add         esp,8  
00EA2C53  mov         dword ptr [result],eax

其中call就表示跳到被调用函数指令地址。 然而其实call里面分为两步,我们稍后再说
重点是call的前4行汇编是做什么的?
eax ecx都指的是寄存器。
那么前四行意思就是给eax寄存器赋值为b的值,然后eax压栈,再给ecx寄存器赋值为a的值,然后ecx压栈。
这样看来就是先后把b和a的值压入栈顶。而我们可以发现b和a就是sum函数所需要的实参。
目前来看是这样的


之前说了call其实包含了两步,分别是
1.把调用方 使用调用函数这条指令的下一条指令的地址push压栈
2.跳到call指令里那个的指令地址,即被调函数的指令地址。

00EA2C4B  call        sum ( 0EA143Dh)  
00EA2C50  add         esp,8  
00EA2C53  mov         dword ptr [result],eax

代到这块的汇编就是将 00EA2C50 压栈,然后再跳转到 0EA143Dh
我们看看 0EA143Dh   是什么。

00EA143D  jmp         sum (0EA4450h) 

   括号里   0EA4450h 就是我们刚看的sum定义的地方
int sum(int  a,int  b)
{
00EA4450   push        ebp  
00EA4451  mov         ebp,esp  
00EA4453  sub         esp,0CCh  
00EA4459  push        ebx  
00EA445A  push        esi  
00EA445B  push        edi  
00EA445C  lea         edi,[ebp-0CCh]  
00EA4462  mov         ecx,33h  
00EA4467  mov         eax,0CCCCCCCCh  
00EA446C  rep stos    dword ptr es:[edi]  
   int r=0;
00EA446E  mov       
。。。。
。。。
。。

这样是不是就很明晰了


然而函数的一开始又有一大串汇编指令,main函数也有,刚才忽略没讲 ,现在来看,这些指令到底是做什么的?
.......
00EA4450   push        ebp  
00EA4451  mov         ebp,esp  
00EA4453  sub         esp,0CCh  
00EA4459  push        ebx  
00EA445A  push        esi  
00EA445B  push        edi  
00EA445C  lea         edi,[ebp-0CCh]  
00EA4462  mov         ecx,33h  
00EA4467  mov         eax,0CCCCCCCCh
00EA446C  rep stos    dword ptr es:[edi]  
   int r=0;
.......  

 首先ebp压入栈,这个ebp是main的栈底指针的值,然后esp的值给ebp,也就是让栈底指针指向栈顶esp指向的地方,简言这两步就是为了保存原先的main栈底地址,然后让栈底指针ebp移到最上方,这就变成了开辟了新的栈了,新栈就是被调用函数的栈。

00EA4453  sub         esp,0CCh  
让esp栈顶指针sub 减等 0cch,也就是让新栈开辟了 0xcc 字节的空间,即 204 个字节。

之后push了三个寄存器 ebx esi edi 

然后 lea       edi,[ebp-0CCh]   这句指令意思为让edi指向ebp-0cch处的地址,也就是让edi寄存器存储了新栈顶指针的值。
之后又给ecx 存储了33h,eax存储了0cccccccch。
33h的十进制为51。是不是刚好51 *4=204,204是我们新栈开辟的大小。
所以说
00EA4462  mov         ecx,33h  
00EA4467  mov         eax,0CCCCCCCCh
00EA446C  rep stos    dword ptr es:[edi]   
这三行的意思,就是循环ecx次,edi从栈顶向栈底依次赋值为eax。
也就是循环51次,从栈顶向栈底赋值4个字节的数据0cccccccch,直到edi走向栈底了,把栈内的数据全部赋值了。这就是每个函数开始后,创建了栈,把栈内数据全部清理为0cccccccc,我们有时会遇到打印越界的数组出现 烫烫烫烫  其实一对 cc 对应的字符就是 烫。  



栈开辟完了之后,进入函数运算
.....
int r=0;
00EA446E  mov         dword ptr [r],0  
   r=a+b;
00EA4475  mov         eax,dword ptr [a]  
00EA4478  add         eax,dword ptr [b]  
00EA447B  mov         dword ptr [r],eax  
   return r;
00EA447E  mov         eax,dword ptr [r]  

}
00EA4481  pop         edi  
00EA4482  pop         esi  
00EA4483  pop         ebx  
00EA4484  mov         esp,ebp  
00EA4486  pop         ebp  

}
00EA4487  ret  
.......



我们先看这部分
 r=a+b;
00EA4475  mov         eax,dword ptr [a]  
00EA4478  add         eax,dword ptr [b]  
00EA447B  mov         dword ptr [r],eax  
可以看到 这个r=a+b的过程是这样的。把实参a的值先赋值给eax寄存器,然后再让eax寄存器加等实参b的值。
也就是a+b的结果先一步计算好了存储在eax中,然后再把eax里的值赋值给栈中的r。


return r;
00EA447E  mov         eax,dword ptr [r]  
返回r,可以看到是把r中的值给了寄存器,通过寄存器带回调用方函数的。


重点又来了
看看栈的 销毁 是怎么做的
00EA4481  pop         edi  
00EA4482  pop         esi  
00EA4483  pop         ebx  
00EA4484  mov         esp,ebp  
00EA4486  pop         ebp  

}
00EA4487  ret  
首先3个寄存器出栈。
然后让esp的值变为ebp, 也就是让栈顶指针指向栈底 。然后ebp出栈,意思就把存储的main的原先的ebp的值出栈,并赋值给ebp。这样,ebp就重新指向main的栈底了。
然后ret指令就是让栈顶的值出栈,现在的栈顶就是存储那个下一条指令地址的值,出栈就可以跳回到调用方刚执行完函数的地方。就实现了回退并连接上次运行地方的功能。

然后转到主函数汇编

.......
00EA2C4B  call        sum (0EA143Dh)  
00EA2C50  add         esp,8  
00EA2C53  mov         dword ptr [result],eax  
........
让esp加等8,意思就是把两个4个字节累积8个字节的栈帧舍弃。然后将eax里保存的return的结果赋值给result。
流程图如下,红色代表顺序



还有一个遗留问题是刚才的sum函数的参数只有两个四字节数据,因此用的是寄存器带的数据,可是寄存器非常有限的,如果我的实参是个结构体类型,大小远远大于四个字节呢,这是参数该如何带呢?
   小于4个字节时用1个寄存器,大于4小于8时 用2个寄存器
    如果大于8个字节那就不能用寄存器了,而是直接让栈顶指针esp减等参数的大小,然后类似开辟栈时,循环拷贝0ccccccc那样,用2个寄存器。一个记录调用方函数的那个实参的其实地址。一个记录拷贝循环次数。这样循环拷贝进行传参。

   返回值也是通样,如果返回的值大于8个字节时,将在调用方函数的栈内开辟一块返回值临时量区域,然后把return的值循环拷贝回调用方。所以在新栈开辟的时候会多压入一个临时量的地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值