首页
社区
课程
招聘
[翻译]调用约定解释
发表于: 2011-10-7 06:34 16816

[翻译]调用约定解释

2011-10-7 06:34
16816
[/CODE]  在长时间辛苦的学习C++的Windows编程的过程中,你可能有时候有点好奇,某个时候在函数声明前会出现奇怪的符号,像 __cdecl, __stdcall, __fastcall, WINAPI, 等等,之后通过MSDN或其他一些参考,你可能会发现,这些符号是用于指定函数的调用约定,在这篇文章中,我将可能尝试解释VC++不同的调用约定(也可能是其他Windows平台的C/C++编译器中使用的约定),我在这里强调一下,如果你想编写可移植代码,就不能使用这些调用约定。

 你可能会问:声明是调用约定?当一个函数被调用的时候,通常会传递参数给它和检查返回值;那么,调用约定就描述了如何传递参数给函数和检查函数的返回值,它还指定了函数名是哪种声明;写优秀的C++程序真的有必要了解调用约定吗;不是,但是它可能会对调试很有帮助;此外它是连接C/C++和汇编代码必须使用的。

  为了理解本文章,你需要一些很基本的汇编语言程序设计知识。
  使用调用约定的时候可能会发生一些下面的情况:
1.所有参数扩大到4字节(当然在Win32平台上),并且储存在合适的内存位置,这个位置筒仓是栈,但也有可能是寄存器,它都是通过调用约定指定的。
2.程序执行跳转到被调用函数的地址
3.在函数内部,保存ESI,EDI,EBX和EBP到栈,执行这些操作代码的部分,被称为函数过程,一般是由编译器生成
4.函数指定的代码被执行,并把返回值的放在EAX寄存器内
5.从堆栈中恢复ESI,EDI,EBX和EBP寄存器,这段代码称为功能终止,与函数过程相对应,在大多数情况下,一般是由编译器生成。
6.从堆栈中移除参数,这个操作称为清理堆栈,可能是调用函数内部执行或调用方执行,这个根据调用约定处理
调用约定的一个例子,我们将会使用一个简单的函数:[CODE]int sumExample (int a, int b)
{
    return a + b;
}
 int c = sum (2, 3);
int __cdecl sumExample (int a, int b);
; // 参数从右到左传递到堆栈中
push        3    
push        2    

; // 调用函数
call        _sumExample 

; // 通过增加ESP寄存器大小清理堆栈参数
add         esp,8 

; // 复制返回值到一个局部变量 (int c)
mov         dword ptr [c],eax
; // function prolog
  push        ebp  
  mov         ebp,esp 
  sub         esp,0C0h 
  push        ebx  
  push        esi  
  push        edi  
  lea         edi,[ebp-0C0h] 
  mov         ecx,30h 
  mov         eax,0CCCCCCCCh 
  rep stos    dword ptr [edi] 
  
; //    return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 

; // function epilog
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp 
  pop         ebp  
  ret

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 6
支持
分享
最新回复 (18)
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
2
尽可能快速的调用约定表明参数可以放置在寄存器里面,而不是放置在堆栈上面;这减少了函数调用的成本,因为寄存器操作速度比堆栈快。
我们可以显式声明一个函数使用__fastcall约定“,如下所示:
int __fastcall sumExample (int a, int b);

我们还可以使用的编译器选项/ GR来指定一些其他的调用约定声明不明确的为__fastcall。
__fastcall调用约定的主要特点是:
1.前两个函数的参数,需要32位或更少被放入寄存器ECX和EDX。他们的其余部分都推由右至左的堆栈。
2.所调用函数的参数是从堆栈中弹出。
3.函数名声明,在函数名前面加上一个“@”字符和附加一个“@”的字节数(十进制)的参数所需的空间。
注:微软保留权利更改在未来版本的编译器传递参数的寄存器。
这里是一个例子:
; // put the arguments in the registers EDX and ECX
  mov         edx,3 
  mov         ecx,2 
  
; // call the function
  call        @fastcallSum@8
  
; // copy the return value from EAX to a local variable (int c)  
  mov         dword ptr [c],eax

函数代码:
// function prolog

  push        ebp  
  mov         ebp,esp 
  sub         esp,0D8h 
  push        ebx  
  push        esi  
  push        edi  
  push        ecx  
  lea         edi,[ebp-0D8h] 
  mov         ecx,36h 
  mov         eax,0CCCCCCCCh 
  rep stos    dword ptr [edi] 
  pop         ecx  
  mov         dword ptr [ebp-14h],edx 
  mov         dword ptr [ebp-8],ecx 
; // return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 
;// function epilog  
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp 
  pop         ebp  
  ret
这是调用约定快速,对比下__cdecl和__stdcall,设置编译器选项/ GR,比较执行时间。我没有找到__fastcall必须比其他调用调用约定更快的了,但你可能会得出不同的结果。
2011-10-7 06:35
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
3
Thiscall是默认调用C + +类(除了那些带有可变数量的参数)的成员函数调用约定。
thiscall调用约定的主要特点是:
1.从左向右传递参数放置到堆栈,这个是放在ECX里面
2.是由被调用函数的堆栈清理
这个调用约定的例子有点不同。首先,作为C + +而不是C编译的代码,我们有一个成员函数的结构,而不是一个全局函数。
struct CSum
{
    int sum ( int a, int b) {return a+b;}
};
函数调用的汇编代码看起来像这样:
  push        3   
  push        2   
  lea         ecx,[sumObj]
  call        ?sum@CSum@@QAEHHH@Z            ; CSum::sum
  mov         dword ptr [s4],eax
这个函数它自己产生下面的代码:
  push        ebp  
  mov         ebp,esp
  sub         esp,0CCh
  push        ebx  
  push        esi  
  push        edi  
  push        ecx  
  lea         edi,[ebp-0CCh]
  mov         ecx,33h
  mov         eax,0CCCCCCCCh
  rep stos    dword ptr [edi]
  pop         ecx  
  mov         dword ptr [ebp-8],ecx
  mov         eax,dword ptr [a]
  add         eax,dword ptr
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp
  pop         ebp  
  ret         8
如果我们现在有一个一个可变数目的参数的成员函数,会发生什么情况?在这种情况下,使用_cdecl,这个是压入堆栈的最后。
2011-10-7 06:48
0
雪    币: 278
活跃值: (709)
能力值: ( LV15,RANK:520 )
在线值:
发帖
回帖
粉丝
4
原文:During the long, hard, but yet beautiful process of learning C++ programming for Windows, you have probably been curious about the strange specifiers that sometime appear in front of function declarations, like __cdecl, __stdcall, __fastcall, WINAPI, etc. After looking through MSDN, or some other reference, you probably found out that these specifiers specify the calling conventions for functions. In this article, I will try to explain different calling conventions used by Visual C++ (and probably other Windows C/C++ compilers). I emphasize that above mentioned specifiers are Microsoft-specific, and that you should not use them if you want to write portable code.

So, what are the calling conventions? When a function is called, the arguments are typically passed to it, and the return value is retrieved. A calling convention describes how the arguments are passed and values returned by functions. It also specifies how the function names are decorated. Is it really necessary to understand the calling conventions to write good C/C++ programs? Not at all. However, it may be helpful with debugging. Also, it is necessary for linking C/C++ with assembly code.

To understand this article, you will need to have some very basic knowledge of assembly programming.

No matter which calling convention is used, the following things will happen:

All arguments are widened to 4 bytes (on Win32, of course), and put into appropriate memory locations. These locations are typically on the stack, but may also be in registers; this is specified by calling conventions.
Program execution jumps to the address of the called function.
Inside the function, registers ESI, EDI, EBX, and EBP are saved on the stack. The part of code that performs these operations is called function prolog and usually is generated by the compiler.
The function-specific code is executed, and the return value is placed into the EAX register.
Registers ESI, EDI, EBX, and EBP are restored from the stack. The piece of code that does this is called function epilog, and as with the function prolog, in most cases the compiler generates it.
Arguments are removed from the stack. This operation is called stack cleanup and may be performed either inside the called function or by the caller, depending on the calling convention used.
As an example for the calling conventions (except for this), we are going to use a simple function:

Collapse | Copy Codeint sumExample (int a, int b)
{
    return a + b;
}The call to this function will look like this:

Collapse | Copy Code    int c = sum (2, 3);For __cdecl, __stdcall, and __fastcall calling conventions, I compiled the example code as C (not C++). The function name decorations, mentioned later in the article, apply to the C decoration schema. C++ name decorations are beyond the scope of this article.

C calling convention (__cdecl)
This convention is the default for C/C++ programs (compiler option /Gd). If a project is set to use some other calling convention, we can still declare a function to use __cdecl:

Collapse | Copy Codeint __cdecl sumExample (int a, int b);The main characteristics of __cdecl calling convention are:

Arguments are passed from right to left, and placed on the stack.
Stack cleanup is performed by the caller.
Function name is decorated by prefixing it with an underscore character '_' .
Now, take a look at an example of a __cdecl call:

Collapse | Copy Code; // push arguments to the stack, from right to left
push        3   
push        2   

; // call the function
call        _sumExample

; // cleanup the stack by adding the size of the arguments to ESP register
add         esp,8

; // copy the return value from EAX to a local variable (int c)
mov         dword ptr [c],eaxThe called function is shown below:

Collapse | Copy Code; // function prolog
  push        ebp  
  mov         ebp,esp
  sub         esp,0C0h
  push        ebx  
  push        esi  
  push        edi  
  lea         edi,[ebp-0C0h]
  mov         ecx,30h
  mov         eax,0CCCCCCCCh
  rep stos    dword ptr [edi]
  
; //    return a + b;
  mov         eax,dword ptr [a]
  add         eax,dword ptr

; // function epilog
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp
  pop         ebp  
  retStandard calling convention (__stdcall)
This convention is usually used to call Win32 API functions. In fact, WINAPI is nothing but another name for __stdcall:

Collapse | Copy Code#define WINAPI __stdcallWe can explicitly declare a function to use the __stdcall convention:

Collapse | Copy Codeint __stdcall sumExample (int a, int b);Also, we can use the compiler option /Gz to specify __stdcall for all functions not explicitly declared with some other calling convention.

The main characteristics of __stdcall calling convention are:

Arguments are passed from right to left, and placed on the stack.
Stack cleanup is performed by the called function.
Function name is decorated by prepending an underscore character and appending a '@' character and the number of bytes of stack space required.
The example follows:

Collapse | Copy Code; // push arguments to the stack, from right to left
  push        3   
  push        2   
  
; // call the function
  call        _sumExample@8

; // copy the return value from EAX to a local variable (int c)  
  mov         dword ptr [c],eaxThe function code is shown below:

Collapse | Copy Code; // function prolog goes here (the same code as in the __cdecl example)

; //    return a + b;
  mov         eax,dword ptr [a]
  add         eax,dword ptr

; // function epilog goes here (the same code as in the __cdecl example)

; // cleanup the stack and return
  ret         8Because the stack is cleaned by the called function, the __stdcall calling convention creates smaller executables than __cdecl, in which the code for stack cleanup must be generated for each function call. On the other hand, functions with the variable number of arguments (like printf()) must use __cdecl, because only the caller knows the number of arguments in each function call; therefore only the caller can perform the stack cleanup.

Fast calling convention (__fastcall)
Fast calling convention indicates that the arguments should be placed in registers, rather than on the stack, whenever possible. This reduces the cost of a function call, because operations with registers are faster than with the stack.

We can explicitly declare a function to use the __fastcall convention as shown:

Collapse | Copy Codeint __fastcall sumExample (int a, int b);We can also use the compiler option /Gr to specify __fastcall for all functions not explicitly declared with some other calling convention.

The main characteristics of __fastcall calling convention are:

The first two function arguments that require 32 bits or less are placed into registers ECX and EDX. The rest of them are pushed on the stack from right to left.
Arguments are popped from the stack by the called function.
Function name is decorated by by prepending a '@' character and appending a '@' and the number of bytes (decimal) of space required by the arguments.
Note: Microsoft have reserved the right to change the registers for passing the arguments in future compiler versions.

Here goes an example:

Collapse | Copy Code; // put the arguments in the registers EDX and ECX
  mov         edx,3
  mov         ecx,2
  
; // call the function
  call        @fastcallSum@8
  
; // copy the return value from EAX to a local variable (int c)  
  mov         dword ptr [c],eaxFunction code:

Collapse | Copy Code; // function prolog

  push        ebp  
  mov         ebp,esp
  sub         esp,0D8h
  push        ebx  
  push        esi  
  push        edi  
  push        ecx  
  lea         edi,[ebp-0D8h]
  mov         ecx,36h
  mov         eax,0CCCCCCCCh
  rep stos    dword ptr [edi]
  pop         ecx  
  mov         dword ptr [ebp-14h],edx
  mov         dword ptr [ebp-8],ecx
; // return a + b;
  mov         eax,dword ptr [a]
  add         eax,dword ptr
;// function epilog  
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp
  pop         ebp  
  retHow fast is this calling convention, comparing to __cdecl and __stdcall? Find out for yourselves. Set the compiler option /Gr, and compare the execution time. I didn't find __fastcall to be any faster than other calling conventons, but you may come to different conclusions.

Thiscall
Thiscall is the default calling convention for calling member functions of C++ classes (except for those with a variable number of arguments).

The main characteristics of thiscall calling convention are:

Arguments are passed from right to left, and placed on the stack. this is placed in ECX.
Stack cleanup is performed by the called function.
The example for this calling convention had to be a little different. First, the code is compiled as C++, and not C. Second, we have a struct with a member function, instead of a global function.

Collapse | Copy Codestruct CSum
{
    int sum ( int a, int b) {return a+b;}
};The assembly code for the function call looks like this:

Collapse | Copy Code  push        3   
  push        2   
  lea         ecx,[sumObj]
  call        ?sum@CSum@@QAEHHH@Z            ; CSum::sum
  mov         dword ptr [s4],eaxThe function itself is given below:

Collapse | Copy Code  push        ebp  
  mov         ebp,esp
  sub         esp,0CCh
  push        ebx  
  push        esi  
  push        edi  
  push        ecx  
  lea         edi,[ebp-0CCh]
  mov         ecx,33h
  mov         eax,0CCCCCCCCh
  rep stos    dword ptr [edi]
  pop         ecx  
  mov         dword ptr [ebp-8],ecx
  mov         eax,dword ptr [a]
  add         eax,dword ptr
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp
  pop         ebp  
  ret         8Now, what happens if we have a member function with a variable number of arguments? In that case, __cdecl is used, and this is pushed onto the stack last.
2011-10-7 06:57
0
雪    币: 7354
活跃值: (4557)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
楼主这么早就在学习了,佩服佩服,惭愧惭愧啊。。。
2011-10-7 08:22
0
雪    币: 154
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
lz说下fpo的调用约定吧。
2011-10-7 08:31
0
雪    币: 123
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
韬哥用实际行动告诉我们,什么叫做废寝忘食,什么叫做刻苦学习、、我觉得韬哥更像是通宵未眠啦
2011-10-7 08:35
0
雪    币: 37
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
看了楼主的文章,获益匪浅~~~
2011-10-7 10:27
0
雪    币: 50
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
辛苦了,支持下。
2011-10-7 10:46
0
雪    币: 435
活跃值: (1282)
能力值: ( LV13,RANK:388 )
在线值:
发帖
回帖
粉丝
10
调用约定只是表示参数传递方式和栈平衡方式,和开不开fpo没什么关系
开fpo的函数和未省略栈指针函数的区别在于,局部变量、参数等通过esp+x寻址而不是通过ebp+、-x来寻址
当然最明显的区别就是没有了 标准的那几条栈帧构建指令
对于调试来说,开fpo之后,就没办法通过 ebp 来回溯调用关系了,开了fpo的函数会漏掉
2011-10-7 13:21
0
雪    币: 154
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
开了fpo参数的传递有可能不再通过栈而是寄存器来传递。
2011-10-8 14:28
0
雪    币: 435
活跃值: (1282)
能力值: ( LV13,RANK:388 )
在线值:
发帖
回帖
粉丝
12
我确实没见过开fpo导致通过寄存器传参的状况,还特意google了一番
但还是发现是否开fpo和传参方式是没什么关系的,传参方式只和调用约定有关

想想就应该是这样,从模块重用的角度看,使用模块的人顶多知道调用约定,他怎么可能知道你的模块有没有fpo优化,如果优化导致传参方式变化,那不知道有没有优化,就没办法传参了
2011-10-8 22:00
0
雪    币: 154
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
写两个非导出非内联函数自己调用,开fpo就会发现了。
2011-10-11 11:51
0
雪    币: 188
活跃值: (85)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
14
我也觉得fpo不会倒置参数传递方式变化, 但leeonegor说的是非导出非内联 那就很难说了

不过那样是不是也就意味着如果开了fpo, 我在c++代码中内嵌汇编调用的“内部”函数可能导致程序执行出错? 还是编译器发现有这种调用也会中止优化, 那函数指针呢?

还有一点, 我一般开了优化后,就找不到对应的函数了,windbg 里面反汇编都解析不到函数名,这个如何知道pfo后的函数对应的代码?
2011-10-12 15:21
0
雪    币: 12
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
韬哥V5,好好学习
2011-12-30 13:29
0
雪    币: 57
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
mark.....
2011-12-30 15:02
0
雪    币: 585
活跃值: (573)
能力值: ( LV13,RANK:290 )
在线值:
发帖
回帖
粉丝
17
。。。。。。。。。。。。。。。。。。
管它什么调用,反正保证堆栈平衡就是了。
2012-3-1 23:57
0
雪    币: 807
活跃值: (2273)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
好帖留名,谢谢楼主
2012-3-2 01:34
0
雪    币: 110
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
1.从左向右传递参数放置到堆栈,这个是放在ECX里面,为什么下面的汇编代码是push 3,push2,这个应该翻译成this指针吧?
2012-4-19 22:49
0
游客
登录 | 注册 方可回帖
返回
//