老码识途1之函数调用和局部变量

无论在编程中,还是在面试中,都会遇见调用函数这个东东,但是,要是让你说函数是怎么调用的,你能回答上来吗,接下来就让我们一起探索函数如何在汇编层次上实现调用的
在接下来,我们将有几个问题要去解决

函数调用如何传递参数的
函数调用如何查找调用函数的地址的
函数内部调用过程是怎么样的
函数调用如何返回结果
如果返回值是结构体又如何返回
函数调用结束后,如何返回调用之前的状态

#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可以看出,函数调用参数的传递是从右向左进行压栈。然后调用函数,
老码识途1之函数调用和局部变量
函数调用如何查找调用函数的地址的


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命令之前的各个寄存器内容
老码识途1之函数调用和局部变量
执行了call指令后,汇编指令直接跳转到了下面这张图的状态。EIP和ESP寄存器发生了变化。下面这个图估计就是函数调用表,用来匹对函数和函数的入口,类似中断向量表了
老码识途1之函数调用和局部变量
这里,发现栈中又压入了一个数据0x00071437h,有没有发现这里为什么会突然压入这个数据呢,带着这个疑问继续向下看吧
老码识途1之函数调用和局部变量
老码识途1之函数调用和局部变量
再通过jmp 000713D0进入了函数调用

函数内部调用过程
首先是调用前准备工作
老码识途1之函数调用和局部变量
首先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中断的原因是:防止该空间里的东东被意外执行。
老码识途1之函数调用和局部变量

由于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的地址没关系了,我们继续向下查找,老码识途1之函数调用和局部变量

上一篇:反汇编角度判断函数有几个参数,分别是什么


下一篇:CPU异常分析(以trap00为例)