首页 > 汇编语言 > 汇编与C语句 阅读:57,774

函数调用与汇编

函数的调用约定(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]即为aa = 0

mov         dword ptr [ebp-8],1      //[ebp-8]即为bb = 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]        //获取参数aebp+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.1cdecl调用汇编

调用约定cdecl又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:

int func (int x ,int y)            //默认的C调用约定

int __cdecl func (int x,int y)  //明确指出C调用约定

该调用约定遵循下面的规则:

参数入栈顺序:从右到左

还原栈者:调用者修改栈

函数名:前加下划线:_func

由于每次函数调用都要由编译器产生还原栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是 __cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf()WindowsAPI 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,0C0h//esp
增长0xC0个空间
0042D689 push ebx //ebx,esi,edi
寄存器入栈
0042D68A push esi 
0042D68B push edi
0042D68C lea edi,[ebp-0C0h]// [ebp-0C0h]
即栈顶,将cccccccc拷贝到栈的局部空间中
0042D692 mov ecx,30h 
0042D697 mov eax,0CCCCCCCCh 
0040042D69C rep stos dword ptr es:[edi]//
拷贝开始
    return x+y;
0042D69E mov eax,dword ptr [x] 
0042D6A1 add eax,dword ptr [y] 
}
0042D6A4 pop edi//edi,esi,ebx
出栈
0042D6A5 pop esi
0042D6A6 pop ebx 
0042D6A7 mov esp,ebp//esp
收缩空间
0042D6A9 pop ebp//ebp
出栈
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.2stdcall调用汇编

stdcall调用约定声明的格式:

int __stdcall func(int x,int y)

stdcall的调用约定意味着:

参数入栈规则:参数从右向左压入栈

还原栈者:被调用函数自身修改栈

函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。在微软WindowsC/C++编译器中,常常用Pascal宏来声明这个调用约定,类似的宏还有WINAPICALLBACK

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,0C0h 
0042D649 push ebx 
0042D64A push esi
0042D64B push edi
0042D64C lea edi,[ebp-0C0h] 
0042D652 mov ecx,30h 
0042D657 mov eax,0CCCCCCCCh 
0042D65C rep stos dword ptr es:[edi] 
   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 
0042D66A ret 8 //
被调用者负责栈平衡,ret 8,esp += 8; 

 

//调用者代码

func1(1, 2); //采用stdcall,参数从右往左依次入栈,被调用者负责栈平衡
//0042D72E push 2 //
参数从右往左依次入栈,2入栈
//0042D730 push 1 //
参数从右往左依次入栈,1入栈
//0042D732 call func1 (42B6F4h)

4.2.3fastcall调用汇编

fastcall的声明语法为:

int fastcall func (int x,int y)

该调用约定遵循下面的规则:

参数入栈顺序:函数的第一个和第二个参数通过ecxedx传递,剩余参数从右到左入栈

还原栈者:被调用者修改栈

函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。以fastcall声明执行的函数,具有较快的执行速度,因为部分参数通过寄存器来进行传递的。

int __fastcall func3(int x, int y, int z)
{
    return x+y+z;
}

被调用者汇编代码:

int __fastcall func3(int x, int y, int z)//采用fastcall调用约定 
{
0042D6C0 push ebp 
0042D6C1 mov ebp,esp 
0042D6C3 sub esp,0D8h 
0042D6C9 push ebx 
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个参数放在了ecxedx
0040042D6E3 mov dword ptr [ebp-8],ecx//
2个参数放在了ecxedx
    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 
0042D6F0 pop esi
0042D6F1 pop ebx 
0042D6F2 mov esp,ebp 
0042D6F4 pop ebp 
0040042D6F5 ret 4 //
3个参数占4个字节,从栈上传递,所以栈平衡是弹出4个字节

 

调用者代码:

func3(1, 2, 3);//采用fastcall,前2个参数依次放入ecxedx寄存器,剩余参数从右往左依次入栈,被调用者负责栈平衡
//0042D743 push 3 //
剩余参数从右往左依次入栈,3入栈
//0042D745 mov edx,2 //
2个参数,分别送往ecxedx寄存器,2edx
//0042D74A mov ecx,1 //
2个参数,分别送往ecxedx寄存器,1ecx
//0042D74F call func3 (42B023h)23h)


x64默认的调用约定:fastcall

 

1,一个函数在调用时,前四个参数是从左至右依次存放于RCXRDXR8R9寄存器里面,剩下的参数从右至左顺序入栈;栈的增长方向为从高地址到低地址。

2,浮点前4个参数传入XMM0XMM1XMM2  XMM3 中。其他参数传递到堆栈中。

3,调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);小于64(bit)的参数传递时高位并不填充零(例如只传递ecx),大于64位需要按照地址传递;

4,调用者负责栈平衡;

5,被调用函数的返回值是整数时,则返回值会被存放于RAX;浮点数返回在xmm0中

6,RAXRCXRDXR8R9R10R11是“易挥发”的,不用特别保护(所谓保护就是使用前要push备份),其余寄存器需要保护。(x86下只有eax, ecx, edx是易挥发的)

7,栈需要16字节对齐,“call”指令会入栈一个8字节的返回值(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCXRDXR8R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。比如sub rsp,28h

8,对于 R8R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。

 

                                                  x64 fastcall调用栈示意图

周哥教IT,分享编程知识,提高编程技能,程序员的充电站。跟着周哥一起学习,每天都有进步。

通俗易懂,深入浅出,一篇文章只讲一个知识点。

当你决定关注「周哥教IT」,你已然超越了90%的程序员!

IT黄埔-周哥教IT技术交流QQ群:213774841,期待您的加入!

二维码
微信扫描二维码关注