函数调用与汇编
函数的调用约定(calling convention)有cdecl,standard,fastcall等几种形式。在函数调用的时候,参数从右往左依次入栈,然后是返回地址(eip寄存器中的值,指向下一条要执行的指令)入栈,ebp入栈,ebp指向esp(程序的release版本可能因为优化而没有ebp寄存器入栈),esp向上移动(与内存的方向相反,减去一个数值),分配一个局部空间,容纳局部变量。
栈上第一个参数的位置:ebp+8;第二个参数的位置:ebp+c;第一个局部变量的位置:ebp-4,第二个局部变量的位置:ebp-8,依次类推。
下面定义一个函数myFunc(),然后在main()函数中调用,依次为例来分析在函数调用过程中的汇编代码:
int myFunc(int a, int b)
{
int c = a + b;
return c;
}
int main(void)
{
int a = 0;
int b = 1;
myFunc(a, b);
return 0;
}
调试版汇编代码:
1) 调用者:
mov dword
ptr [ebp-4],0 //[ebp-4]即为a,a
= 0
mov dword
ptr [ebp-8],1 //[ebp-8]即为b,b
= 1
mov eax,dword
ptr [ebp-8] //
b入栈
push
eax
mov ecx,dword
ptr [ebp-4] //
a入栈
push
ecx
call
myFunc // 调用函数
add
esp,8 // 恢复堆栈
2)被调用者:
push ebp //将ebp,ebx,esi,edi等入栈
mov
ebp,esp
sub esp,44h
push ebx
push esi
push edi
lea edi,[ebp-44h]
mov
ecx,11h
//0xCC是调试中断(__asm
int 3)的指令码,所以
//当程序错误的跳转到这个区域进行执行时将产
//生调试中断
mov
eax,0CCCCCCCCh
rep stos
dword ptr [edi]
mov
eax,dword ptr [ebp+8] //获取参数a,ebp+8的位置为第一个参数
add
eax,dword ptr [ebp+0Ch] //获取参数b并与a相加
mov
dword ptr [ebp-4],eax //放入局部变量c中
mov
eax,dword ptr [ebp-4] //将c作为返回值放如eax中
pop
edi //恢复堆栈
pop
esi
pop
ebx
mov esp,ebp
pop
ebp
ret
Release版:
1)调用者
push 1
push 0
call MyFunc
add esp,8
2)被调用者
mov eax,
dword ptr ss:[esp+8]
mov ecx,
dword ptr ss:[esp+4]
add eax,
ecx
ret
函数调用的汇编代码,在不同的调用约定下,会略有不同。因为与参数入栈的方式和栈平衡由谁来负责相关。
4.2.1 cdecl调用汇编
调用约定cdecl又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:
int func (int x ,int y) //默认的C调用约定
int __cdecl func (int x,int y) //明确指出C调用约定
该调用约定遵循下面的规则:
参数入栈顺序:从右到左
还原栈者:调用者修改栈
函数名:前加下划线:_func
由于每次函数调用都要由编译器产生还原栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是 __cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf()和Windows的API
wsprintf()就是__cdecl调用方式。
由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数了。
int __cdecl func2(int x, int y)
{
return x+y;
}
被调用者汇编代码:
int __cdecl func2(int x, int y)//采用cdecl调用约定
{
0042D680 push ebp //ebp入栈
0042D681 mov ebp,esp //ebp指向esp
0042D683 sub esp,
0042D689 push ebx //ebx,esi,edi寄存器入栈
0042D
0042D68B push edi
0042D
0042D692 mov ecx,30h
0042D697 mov eax,0CCCCCCCCh
0040042D
return x+y;
0042D69E mov eax,dword ptr [x]
0042D
}
0042D
0042D
0042D
0042D
0042D
00000042D6AA ret//被调用者直接返回,不用恢复栈平衡,由调用者负责
调用代码:
func2(1, 2);//采用cdecl调用约定,参数从右往左依次入栈,调用者负责栈平衡
//0042D737 push 2//参数从右往左依次入栈,2入栈
//0042D739 push 1//参数从右往左依次入栈,1入栈
//0042D73B call func2 (42B3FCh)
//0042D740 add esp,8 //调用者负责栈平衡,esp+8,等于2个入栈参数的长度
4.2.2 stdcall调用汇编
stdcall调用约定声明的格式:
int __stdcall func(int x,int y)
stdcall的调用约定意味着:
参数入栈规则:参数从右向左压入栈
还原栈者:被调用函数自身修改栈
函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。在微软Windows的C/C++编译器中,常常用Pascal宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。
int __stdcall func1(int x, int y)
{
return x+y;
}
//被调用者代码:
int __stdcall func1(int x, int y)//采用stdcall
{
42D640 push ebp
0042D641 mov ebp,esp
0042D643 sub esp,
0042D649 push ebx
0042D
0042D64B push edi
0042D
0042D652 mov ecx,30h
0042D657 mov eax,0CCCCCCCCh
0042D
return x+y;
0042D65E mov eax,dword ptr [x]
0042D661 add eax,dword ptr [y]
}
0042D664 pop edi
0042D665 pop esi
0042D666 pop ebx
0042D667 mov esp,ebp //ebp(调用前的栈顶)放入esp中,然后出栈,恢复老ebp
0042D669 pop ebp
0042D
//调用者代码
func1(1, 2); //采用stdcall,参数从右往左依次入栈,被调用者负责栈平衡
//0042D72E push 2 //参数从右往左依次入栈,2入栈
//0042D730 push 1 //参数从右往左依次入栈,1入栈
//0042D732 call func1 (42B
4.2.3 fastcall调用汇编
fastcall的声明语法为:
int fastcall func (int x,int y)
该调用约定遵循下面的规则:
参数入栈顺序:函数的第一个和第二个参数通过ecx和edx传递,剩余参数从右到左入栈
还原栈者:被调用者修改栈
函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。以fastcall声明执行的函数,具有较快的执行速度,因为部分参数通过寄存器来进行传递的。
int __fastcall func3(int x, int y, int z)
{
return x+y+z;
}
被调用者汇编代码:
int __fastcall func3(int x, int y, int z)//采用fastcall调用约定
{
0042D
0042D
0042D
0042D
0042D6CA push esi
0042D6CB push edi
0042D6CC push ecx
0042D6CD lea edi,[ebp-0D8h]
0042D6D3 mov ecx,36h
0042D6D8 mov eax,0CCCCCCCCh
0042D6DD rep stos dword ptr es:[edi]
0042D6DF pop ecx
0042D6E0 mov dword ptr [ebp-14h],edx //前2个参数放在了ecx和edx中
0040042D6E3 mov dword ptr [ebp-8],ecx//前2个参数放在了ecx和edx中
return x+y+z;
0042D6E6 mov eax,dword ptr [x]
0042D6E9 add eax,dword ptr [y]
0042D6EC add eax,dword ptr [z]
}
0042D6EF pop edi
0042D
0042D
0042D
0042D
0040042D
调用者代码:
func3(1, 2, 3);//采用fastcall,前2个参数依次放入ecx和edx寄存器,剩余参数从右往左依次入栈,被调用者负责栈平衡
//0042D743 push 3 //剩余参数从右往左依次入栈,3入栈
//0042D745 mov edx,2 //前2个参数,分别送往ecx和edx寄存器,2入edx
//0042D
//0042D
x64默认的调用约定:fastcall
1,一个函数在调用时,前四个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从右至左顺序入栈;
2,浮点前4个参数传入XMM0、XMM1、XMM2 和 XMM3 中。其他参数传递到堆栈中。
3,调用者负责在栈上分配32字节的“shadow
space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),大于64位需要按照地址传递;
4,调用者负责栈平衡;
6,RAX,RCX,RDX,R8,R9,R10,R11是“易挥发”的,不用特别保护(所谓保护就是使用前要push备份),其余寄存器需要保护。(x86下只有eax,
ecx, edx是易挥发的)
7,栈需要16字节对齐,“call”指令会入栈一个8字节的返回值(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCX、RDX、R8、R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。比如sub
rsp,28h
8,对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。