无论在编程中,还是在面试中,都会遇见调用函数这个东东,但是,要是让你说函数是怎么调用的,你能回答上来吗,接下来就让我们一起探索函数如何在汇编层次上实现调用的
在接下来,我们将有几个问题要去解决
函数调用如何传递参数的
函数调用如何查找调用函数的地址的
函数内部调用过程是怎么样的
函数调用如何返回结果
如果返回值是结构体又如何返回
函数调用结束后,如何返回调用之前的状态
#include "stdafx.h"
int func(int a,int b){
int sum ;
sum = a+b;
return sum;
}
int _tmain(int argc, _TCHAR* argv[])
{
int a ;
a = func(1,2);
return 0;
}
(1) 函数调用如何传递参数的
下面是上面调用的反汇编代码
int a ;
a = func(1,2);
0007142E push 2
00071430 push 1
00071432 call func (071028h)
00071437 add esp,8
0007143A mov dword ptr [a],eax
从上面的push 2;push1
可以看出,函数调用参数的传递是从右向左进行压栈。然后调用函数,
函数调用如何查找调用函数的地址的
int func(int a,int b){
000713D0 push ebp
000713D1 mov ebp,esp
000713D3 sub esp,0CCh
000713D9 push ebx
000713DA push esi
000713DB push edi
000713DC lea edi,[ebp-0CCh]
000713E2 mov ecx,33h
000713E7 mov eax,0CCCCCCCCh
000713EC rep stos dword ptr es:[edi]
int sum ;
sum = a+b;
000713EE mov eax,dword ptr [a]
000713F1 add eax,dword ptr [b]
000713F4 mov dword ptr [sum],eax
return sum;
000713F7 mov eax,dword ptr [sum]
}
000713FA pop edi
000713FB pop esi
000713FC pop ebx
000713FD mov esp,ebp
000713FF pop ebp
00071400 ret
上面是函数定义的汇编代码。当函数参数压栈之后,汇编代码执行了call func (071028h)
,下面是执行call命令之前的各个寄存器内容
执行了call指令后,汇编指令直接跳转到了下面这张图的状态。EIP和ESP寄存器发生了变化。下面这个图估计就是函数调用表,用来匹对函数和函数的入口,类似中断向量表了
这里,发现栈中又压入了一个数据0x00071437h,有没有发现这里为什么会突然压入这个数据呢,带着这个疑问继续向下看吧
再通过jmp 000713D0
进入了函数调用
函数内部调用过程
首先是调用前准备工作
首先push ebp
是将ebp压栈,ebp寄存器是上个调用单元的栈低寄存器,压栈是用于保护现场,然后mov ebp,esp
将当前的栈顶作为栈底。sub esp 0CCh
这步是给当前函数调用分配空间,所以将push ebp这部分内存还是属于上个函数调用的空间,执行完sub esp,0CCh
后,接下来栈使用的空间就是属于当前的函数调用栈了,push ebx,push esi,push edi
,继续保护上一个函数调用的现场。而下部分指令是对栈的保护,实际也是一种填充
000713DC lea edi,[ebp-0CCh]
000713E2 mov ecx,33h
000713E7 mov eax,0CCCCCCCCh
000713EC rep stos dword ptr es:[edi]
rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址
如果设置了direction flag, 那么edi会在该指令执行后减小,
如果没有设置direction flag, 那么edi的值会增加.
REP可以是任何字符传指令(CMPS, LODS, MOVS, SCAS, STOS)的前缀.
REP能够引发其后的字符串指令被重复, 只要ecx的值不为0, 重复就会继续.
每一次字符串指令执行后, ecx的值都会减小.
stos((store into String),意思是把eax的内容拷贝到目的地址。
用法:stos dst,dst是一个目的地址,例如:stos dword ptr es:[edi]。dword ptr前缀告诉stos,一次拷贝双字(4个字节)的数据到目的地址。为什么一次非要拷贝双字呢?这和eax寄存器有关,到底神马关系,慢慢道来。。
执行stos之前必须往eax(32为寄存器)放入要拷贝的数据。上图中,eax的内容是cccccccc,不用说都明白int3中断。
这段代码是初始化堆栈和分配局部变量用的,往分配好的局部变量空间放入int3中断的原因是:防止该空间里的东东被意外执行。
由于ecx寄存器是033h=51,一共循环51次,实际这里51x4 = 204,正好把空间填满
int sum ;
sum = a+b;
000713EE mov eax,dword ptr [a]
000713F1 add eax,dword ptr [b]
000713F4 mov dword ptr [sum],eax
000713F7 mov eax,dword ptr [sum]
}
这部分就是取得a和b的值,然后进行运算,将运算结果存储再栈中。最后将结果放入eax的寄存器中,
000713FA pop edi
000713FB pop esi
000713FC pop ebx
000713FD mov esp,ebp
000713FF pop ebp
00071400 ret
上面的也就是函数调用的环境恢复过程,这里主要说一下ret指令过程,还记得我们刚刚在上面说的执行call指令时,会将当前的eip寄存器的值压入栈中,现在ret指令就是将压入栈中的值重新恢复到eip中
pop eip
add esp,4
如果返回值是结构体又如何返回
我们现在都知道返回int类型可以使用eax寄存器,但是,如果返回结构体呢,寄存器能够装的下吗,这是我们就要重新探索函数的返回类型了
#include "stdafx.h"
struct MyStruct
{
int a;
int b;
int c;
int d;
};
MyStruct func(){
MyStruct a;
a.a = 1;
a.b = 2;
a.c = 3;
a.d = 4;
return a;
}
int _tmain(int argc, _TCHAR* argv[])
{
MyStruct a ;
a = func();
return 0;
}
上面的汇编代码如下
MyStruct a ;
a = func();
0064149E lea eax,[ebp-104h]
006414A4 push eax
006414A5 call func (0641195h)
006414AA add esp,4
006414AD mov ecx,dword ptr [eax]
006414AF mov dword ptr [ebp-0ECh],ecx
006414B5 mov edx,dword ptr [eax+4]
006414B8 mov dword ptr [ebp-0E8h],edx
006414BE mov ecx,dword ptr [eax+8]
006414C1 mov dword ptr [ebp-0E4h],ecx
006414C7 mov edx,dword ptr [eax+0Ch]
006414CA mov dword ptr [ebp-0E0h],edx
006414D0 mov eax,dword ptr [ebp-0ECh]
006414D6 mov dword ptr [a],eax
006414D9 mov ecx,dword ptr [ebp-0E8h]
006414DF mov dword ptr [ebp-10h],ecx
006414E2 mov edx,dword ptr [ebp-0E4h]
006414E8 mov dword ptr [ebp-0Ch],edx
006414EB mov eax,dword ptr [ebp-0E0h]
006414F1 mov dword ptr [ebp-8],eax
return 0;
006414F4 xor eax,eax
}
上面可以看到,编译器将[ebp-104h]值压入栈中,这个值到底是什么呢。通过编译发现EAX = 010FFD54,但是&a = 0x010FFE44,估计和a的地址没关系了,我们继续向下查找,