函数调用栈

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

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

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


以下为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的值循环拷贝回调用方。所以在新栈开辟的时候会多压入一个临时量的地址。