函数调用约定
2013-12-02 22:15
316 查看
函数调用约定解析
http://www.allaboutprogram.com/index.php?option=content&task=view&id=29&Itemid=31
HouSisong@263.net 2004.11.07整理
文章来源于abp论坛中的一篇帖子:http://bbs.allaboutprogram.com/viewtopic.php?t=1245
(原文章写于2003.12.01,这里补充了函数返回值传递方式的说明)
前言:
文章讲述了几种主要程序语言中的函数调用约定;详细说明时主要以VC6中的函数调用约定为主,阐释方式主要是以C++程序编译后得到的汇编代码来进行说明;
我所使用的编译器和平台:WindowsXP+ 赛扬1G + VC6(主要工具)\Delphi6\C++Builder6;
一:函数调用约定;
函数调用约定是函数调用者和被调用的函数体之间关于参数传递、返回值传递、堆栈清除、寄存器使用的一种约定;
它是需要二进制级别兼容的强约定,函数调用者和函数体如果使用不同的调用约定,将可能造成程序执行错误,必须把它看作是函数声明的一部分;
二:常见的函数调用约定;
VC6中的函数调用约定;
调用约定 堆栈清除 参数传递
__cdecl 调用者 从右到左,通过堆栈传递
__stdcall 函数体 从右到左,通过堆栈传递
__fastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈
thiscall 函数体 this指针默认通过ECX传递,其它参数从右到左入栈
__cdecl是C\C++的默认调用约定; VC的调用约定中并没有thiscall这个关键字,它是类成员函数默认调用约定;
C\C++中的main(或wmain)函数的调用约定必须是__cdecl,不允许更改;
默认调用约定一般能够通过编译器设置进行更改,如果你的代码依赖于调用约定,请明确指出需要使用的调用约定;
Delphi6中的函数调用约定;
调用约定 堆栈清除 参数传递
register 函数体 从左到右,优先使用寄存器(EAX,EDX,ECX),然后使用堆栈
pascal 函数体 从左到右,通过堆栈传递
cdecl 调用者 从右到左,通过堆栈传递(与C\C++默认调用约定兼容)
stdcall 函数体 从右到左,通过堆栈传递(与VC中的__stdcall兼容)
safecall 函数体 从右到左,通过堆栈传递(同stdcall)
Delphi中的默认调用约定是register,它也是我认为最有效率的一种调用方式,而cdecl是我认为综合效率最差的一种调用方式;
VC中的__fastcall调用约定一般比register效率稍差一些;
C++Builder6中的函数调用约定;
调用约定 堆栈清除 参数传递
__fastcall 函数体 从左到右,优先使用寄存器(EAX,EDX,ECX),然后使用堆栈 (兼容Delphi的register)
(register与__fastcall等同)
__pascal 函数体 从左到右,通过堆栈传递
__cdecl 调用者 从右到左,通过堆栈传递(与C\C++默认调用约定兼容)
__stdcall 函数体 从右到左,通过堆栈传递(与VC中的__stdcall兼容)
__msfastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈(兼容VC的__fastcall)
常见的函数调用约定中,只有cdecl约定需要调用者来清除堆栈;
C\C++中的函数支持参数数目不定的参数列表,比如printf函数;由于函数体不知道调用者在堆栈中压入了多少参数, 所以函数体不能方便的知道应该怎样清除堆栈,那么最好的办法就是把清除堆栈的责任交给调用者; 这应该就是cdecl调用约定存在的原因吧;
VB一般使用的是stdcall调用约定;(ps:有更强的保证吗)
Windows的API中,一般使用的是stdcall约定;(ps: 有更强的保证吗)
建议在不同语言间的调用中(如DLL)最好采用stdcall调用约定,因为它在语言间兼容性支持最好;
三:函数返回值传递方式
其实,返回值的传递从处理上也可以想象为函数调用的一个out形参数; 函数返回值传递方式也是函数调用约定的一部分;
有返回值的函数返回时:一般int、指针等32bit数据值(包括32bit结构)通过eax传递,(bool,char通过al传递,short通过ax传递),特别的__int64等64bit结构(struct) 通过edx,eax两个寄存器来传递(同理:32bit整形在16bit环境中通过dx,ax传递); 其他大小的结构(struct)返回时把其地址通过eax返回;(所以返回值类型不是1,2,4,8byte时,效率可能比较差)
参数和返回值传递中,引用方式的类型可以看作与传递指针方式相同;
float\double(包括Delphi中的extended)都是通过浮点寄存器st(0)返回;
四:通过VC中的C++例子和产生出的汇编清单来对函数调用约定进行说明;
(ps:后面虽然列出了很多汇编,但是我做了很详细的注释,我希望那些对汇编感到“恐惧”的人
也能顺利的阅读; 并为那些想在VC中使用汇编的人提供一些帮助
A:
测试代码:
int x;
int __cdecl add(int a,int b) { return a+b; }//使用__cdecl调用约定
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}
; Debug模式编译后得到的汇编代码
PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01HDUP (?) ; x变量
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
EXTRN __chkesp:NEAR
; COMDAT _main
_TEXT SEGMENT
_main PROC NEAR ; COMDAT //main函数体
push ebp ; //保存ebp的值到堆栈,退出函数前用pop ebp恢复
mov ebp, esp ; //ebp指向当前堆栈; 函数中可以通过ebp来进行堆栈访问
sub esp, 64 ; //在堆栈中开辟64byte局部空间
; //说明:这三条汇编指令是很多函数体开始的惯用法;
; //用ebp指向堆栈(不会改变);并通过ebp来访问参数和局部变量;
push ebx ; //一般按照函数间调用的约定,函数中可以自由使用eax,ecx,edx;
push esi ; //其它寄存器如果需要使用则需要保存,用完时恢复;也就是寄存器的使用约定; 这也使函数调用约定的一部分;
push edi ; //即:在函数中调用了别的函数后,eax,ecx,edx很可能已经改变,
; //而其它寄存器(ebx,esi,edi,ebp)的值可以放心继续使用(esp除外)
lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd ; //前面开辟的(16*4)byte局部空间全部填充0xCC
; //注意: 0xCC是调试中断(__asm int 3)的指令码,所以可以想象,当
; //程序错误的跳转到这个区域进行执行时将产生调试中断
push 2 ; //代码: x=add(1,2);
push 1 ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z ; 调用add函数;call指令将把下一条指令的地址(返回地址)压入堆栈
add esp, 8 ; add函数调用完以后,调用者负责清理堆栈 (__cdecl调用约定!!!)
; 两个int型参数共使用了8byte空间的堆栈
mov DWORD PTR ?x@@3HA, eax ; 将add函数的返回值存入x变量中,可以看出add函数的返回值放在eax中
xor eax, eax ; //原代码:return 0; 执行eax清零,main函数的返回值0放在eax中
pop edi
pop esi
pop ebx ; //恢复edi,esi,ebx寄存器
add esp, 64 ; //恢复64byte局部空间
cmp ebp, esp
call __chkesp ; //到这里时应该ebp==esp, Debug版进行确认,如果不等,抛出异常等
mov esp, ebp
pop ebp ; //恢复ebp寄存器
ret 0
_main ENDP
_TEXT ENDS
;//下面是add函数的代码,就不用解释的像上面那么详细了
; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8 ;//参数a相对于堆栈偏移8
_b$ = 12 ;//参数b相对于堆栈偏移12
?add@@YAHHH@Z PROC NEAR ; add,COMDAT //add函数体
push ebp
mov ebp, esp
sub esp, 64 ; 00000040H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd
mov eax, DWORD PTR _a$[ebp] ;将参数a的值移动到eax
add eax, DWORD PTR _b$[ebp] ;将参数b的值累加到eax; 可以看出返回值通过eax返回
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0 ; 函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
; ret指令将取出call指令压入的返回地址,并跳转过去继续执行
?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END
; 再来看一下Release模式编译后得到的汇编代码
; 可以看出,这比Debug模式少了很多的汇编指令,速度当然可能更快了;不再做详细说明了,请对照上面的解释
PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01HDUP (?) ; x
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
; COMDAT _main
_TEXT SEGMENT
_main PROC NEAR ; COMDAT //main函数体
push 2
push 1 ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z ; //调用add函数;
mov DWORD PTR ?x@@3HA, eax ; x
add esp, 8 ; //调用者负责清理堆栈 (__cdecl调用约定!!!)
xor eax, eax
ret 0
_main ENDP
_TEXT ENDS
; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8
_b$ = 12
?add@@YAHHH@Z PROC NEAR ; add,COMDAT //add函数体
mov eax, DWORD PTR _b$[esp-4] ;将参数b的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ;将参数a的值移动到ecx
add eax, ecx ;将ecx的值累加到eax; 返回值通过eax传递
ret 0 ;函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END
下面的分析中将只给出Release模式编译后的汇编代码
B:
声明add函数为__stdcall调用约定
int x;
int __stdcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}
;来看产生的汇编代码:
; //main函数体
push 2
push 1 ; //从右到左入栈
call ?add@@YGHHH@Z ; add
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
ret 0
; //add函数体
mov eax, DWORD PTR _b$[esp-4]
mov ecx, DWORD PTR _a$[esp-4]
add eax, ecx
ret 8 ; //函数体负责清栈 ;两个int型参数共使用了8byte空间的堆栈
C:
声明add函数为__fastcall调用约定
int x;
int __fastcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}
;来看产生的汇编代码:
; //main函数体
mov edx, 2 ; b通过寄存器edx传递
mov ecx, 1 ; a通过寄存器ecx传递
call ?add@@YIHHH@Z ; add
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
ret 0
; //add函数体
lea eax, DWORD PTR [ecx+edx] ; //a,b参数值已经在ecx,edx中,该句将这两个值的和放到eax作为返回值;
ret 0 ; //这里应该函数体负责清栈 ;但因为两个参数已经通过寄存器传递
; //了,没有使用堆栈,所以ret 0;
D:
来看一下类的成员函数的调用:
struct T
{
int start0;
T():start0(1){}
int add(int a,int b); //类成员函数;只要不明确声明调用约定则默认使用thiscall调用约定;
};
int T::add(int a,int b) { return (*this).start0+a+b; }
int x;
int main(int argc, char* argv[])
{
T t;
x=t.add(1,2);
return 0;
}
来看产生的汇编代码:
; //main函数体
push ecx ; //保存ecx
push 2
push 1 ; //参数从右到左入栈
lea ecx, DWORD PTR _t$[esp+12] ; //t的地址保存到ecx
mov DWORD PTR _t$[esp+12], 1 ; //执行t::start0=1;
call ?add@T@@QAEHHH@Z ; //调用T::add函数,这时ecx中存放了t的的地址(this指针);
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
pop ecx
ret 0
; //T::add函数体
mov eax, DWORD PTR [ecx] ; //通过this指针(保存在ecx)将start0的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ; //把a的值移动到ecx; this的值将丢失,但函数体中已经不需要了
add eax, ecx ; //将a的值累加到eax
mov ecx, DWORD PTR _b$[esp-4] ; //把b的值移动到ecx;
add eax, ecx ; //将b的值累加到eax
ret 8 ; //函数体负责清栈 ;
五: 其他
1.在VC中实现一个函数体时可以使用__declspec(naked)声明,它告诉编译器,不要为函数体自动产生开始和结束码;
2.在VC6中,想得到汇编代码清单,设置方法为:
引用:[Project]->[Setting...]->[C++]->[Category:]->[ListingFiles]->[Listing file type:]->[Assembily ,...]
3.VC6中嵌入汇编代码的方式为:
__asm { <汇编语句s> }
或 __asm <一条汇编语句>
4.VC6中重新设定函数使用的默认调用约定的方法是:
引用:
在[Project]->[Setting...]->[C++]->[ProjectOptions:]中增加编译设置
比如:/Gd 代表__cdecl; /Gr 代表__fastcall; /Gz 代表__stdcall
参考资料:
MSDN:Calling Conventions;
Delphi6\C++Builder6帮助;
请用www.google.com和www.baidu.com搜索“调用约定”或“Calling Conventions”;
函数调用约定
1、_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。
3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
_fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。
2)名字修饰约定
1、修饰名(Decoration name)
“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。
a、C编译时函数名修饰约定规则:
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。
它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。
b、C++编译时函数名修饰约定规则:
__stdcall调用约定:
1、以“?”标识函数名的开始,后跟函数名;
2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
3、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
....
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。
VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.
CB在输出函数声明时使用4种修饰符号
//__cdecl
cb的默认值,它会在输出函数名前加_,并保留此函数名不变,参数按照从右到左的顺序依次传递给栈,也可以写成_cdecl和cdecl形式。
//__fastcall
她修饰的函数的参数将尽肯呢感地使用寄存器来处理,其函数名前加@,参数按照从左到右的顺序压栈;
//__pascal
它说明的函数名使用Pascal格式的命名约定。这时函数名全部大写。参数按照从左到右的顺序压栈;
//__stdcall
使用标准约定的函数名。函数名不会改变。使用__stdcall修饰时。参数按照由右到左的顺序压栈,也可以是_stdcall;
DLL中调用约定和名称修饰(一)
调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定。
在C++中,为了允许操作符重载和函数重载,C++编译器往往按照某种规则改写每一个入口点的符号名,以便允许同一个名字(具有不同的参数类型或者是不同的作用域)有多个用法,而不会打破现有的基于C的链接器。这项技术通常被称为名称改编(Name Mangling)或者名称修饰(Name Decoration)。许多C++编译器厂商选择了自己的名称修饰方案。
因此,为了使其它语言编写的模块(如Visual Basic应用程序、Pascal或Fortran的应用程序等)可以调用C/C++编写的DLL的函数,必须使用正确的调用约定来导出函数,并且不要让编译器对要导出的函数进行任何名称修饰。
1.调用约定(Calling Convention)
调用约定用来处理决定函数参数传送时入栈和出栈的顺序(由调用者还是被调用者把参数弹出栈),以及编译器用来识别函数名称的名称修饰约定等问题。在Microsoft VC++ 6.0中定义了下面几种调用约定,我们将结合汇编语言来一一分析它们:
1、__cdecl
__cdecl是C/C++和MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl。
下面将通过一个具体实例来分析__cdecl约定:
在VC++中新建一个Win32 Console工程,命名为cdecl。其代码如下:
int__cdecl Add(int a, int b); //函数声明
voidmain()
{
Add(1,2); //函数调用
}
int__cdecl Add(int a, int b) //函数实现
{
return (a + b);
}
函数调用处反汇编代码如下:
;Add(1,2);
push 2 ;参数从右到左入栈,先压入2
push 1 ;压入1
call @ILT+0(Add)(00401005) ;调用函数实现
add esp,8 ;由函数调用清栈
2、__stdcall
__stdcall调用约定用于调用Win32API函数。采用__stdcal约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。__stdcall可以写成_stdcall。
还是那个例子,将__cdecl约定换成__stdcall:
int__stdcall Add(int a, int b)
{
return(a + b);
}
函数调用处反汇编代码:
;Add(1,2);
push 2 ;参数从右到左入栈,先压入2
push 1 ;压入1
call @ILT+10(Add)(0040100f) ;调用函数实现
函数实现部分的反汇编代码:
;int __stdcall Add(int a, intb)
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
repstos dwordptr [edi]
;return(a + b);
mov eax,dwordptr [ebp+8]
add eax,dwordptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;清栈
3、__fastcall
__fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall。
依旧是相类似的例子,此时函数调用约定为__fastcall,函数参数个数增加2个:
int__fastcall Add(int a, double b, int c, int d)
{
return(a + b + c + d);
}
函数调用部分的汇编代码:
;Add(1,2, 3, 4);
push 4 ;后两个参数从右到左入栈,先压入4
mov edx,3 ;将int类型的3放入edx
push 40000000h ;压入double类型的2
push 0
mov ecx,1 ;将int类型的1放入ecx
call @ILT+0(Add)(00401005) ;调用函数实现
函数实现部分的反汇编代码:
; int __fastcall Add(int a,double b, int c, int d)
push ebp
mov ebp,esp
sub esp,48h
push ebx
push esi
push edi
push ecx
lea edi,[ebp-48h]
mov ecx,12h
mov eax,0CCCCCCCCh
repstos dwordptr [edi]
pop ecx
mov dwordptr [ebp-8],edx
mov dwordptr [ebp-4],ecx
;return(a + b + c + d);
fild dwordptr [ebp-4]
fadd qwordptr [ebp+8]
fiadd dwordptr [ebp-8]
fiadd dwordptr [ebp+10h]
call __ftol(004011b8)
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 0Ch ;清栈
关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...->C/C++->Code Generation项选择。它们对应的命令行参数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。
DLL中调用约定和名称修饰(二)
4、thiscall
thiscall调用约定是C++中的非静态类成员函数的默认调用约定。thiscall只能被编译器使用,没有相应的关键字,因此不能被程序员指定。采用thiscall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,只是另外通过ECX寄存器传送一个额外的参数:this指针。
这次的例子中将定义一个类,并在类中定义一个成员函数,代码如下:
class CSum
{
public:
int Add(int a, int b)
{
return (a + b);
}
};
void main()
{
CSum sum;
sum.Add(1, 2);
}
函数调用部分汇编代码:
;CSum sum;
;sum.Add(1, 2);
push 2 ;参数从右到左入栈,先压入2
push 1 ;压入1
lea ecx,[ebp-4] ;ecx存放了this指针
call @ILT+5(CSum::Add) (0040100a) ;调用函数实现
函数实现部分汇编代码:
;int Add(int a, int b)
push ebp
mov ebp,esp
sub esp,44h ;多用了一个4bytes的空间用于存放this指针
push ebx
push esi
push edi
push ecx
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0CCCCCCCCh
rep stos dwordptr [edi]
pop ecx
mov dwordptr [ebp-4],ecx
;return (a + b);
mov eax,dwordptr [ebp+8]
add eax,dwordptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;清栈
5、naked属性
采用上面所述的四种调用约定的函数在进入函数时,编译器会产生代码来保存ESI、EDI、EBX、EBP寄存器中的值,退出函数时则产生代码恢复这些寄存器的内容。对于定义了naked属性的函数,编译器不会自动产生这样的代码,需要你手工使用内嵌汇编来控制函数实现中的堆栈管理。由于naked属性并不是类型修饰符,故必须和__declspec共同使用。下面的这段代码定义了一个使用了naked属性的函数及其实现:
__declspec ( naked ) func()
{
int i;
int j;
_asm
{
push ebp
mov ebp, esp
sub esp,__LOCAL_SIZE
}
_asm
{
mov esp,ebp
pop ebp
ret
}
}
naked属性与本节关系不大,具体请参考MSDN。
6、WINAPI
还有一个值得一提的是WINAPI宏,它可以被翻译成适当的调用约定以供函数使用。该宏定义于windef.h之中。下面是在windef.h中的部分内容:
#define CDECL _cdecl
#define WINAPI CDECL
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define APIENTRY WINAPI
由此可见,WINAPI、CALLBACK、APIENTRY等宏的作用。
2.名称修饰(Name Decoration)
C或C++函数在内部(编译和链接)通过修饰名(Decoration Name)识别。函数的修饰名是编译器在编译函数定义或者原型时生成的字符串。编译器在创建.obj文件时对函数名称进行修饰。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出C++重载函数、构造函数、析构函数,又如在汇编代码里调用C或C++函数等。
在VC++中,函数修饰名由编译类型(C或C++)、函数名、类名、调用约定、返回类型、参数等多种因素共同决定。下面分C编译、C++编译(非类成员函数)和C++类及其成员函数编译三种情况说明:
1、C编译时函数名称修饰
当函数使用__cdecl调用约定时,编译器仅在原函数名前加上一个下划线前缀,格式为_functionname。例如:函数int __cdecl Add(int a, int b),输出后为:_Add。
当函数使用__stdcall调用约定时,编译器在原函数名前加上一个下划线前缀,后面加上一个@符号和函数参数的字节数,格式为_functionname@number。例如:函数int __stdcall Add(int a, intb),输出后为:_Add@8。
当函数是用__fastcall调用约定时,编译器在原函数名前加上一个@符号,后面是加一个@符号和函数参数的字节数,格式为@functionname@number。例如:函数int __fastcall Add(int a, intb),输出后为:@Add@8。
以上改变均不会改变原函数名中的字符大小写。
DLL中调用约定和名称修饰(三)
2、C++编译时函数(非类成员函数)名称修饰
当函数使用__cdecl调用约定时,编译器进行以下工作:
1.以?标识函数名的开始,后跟函数名;
2.函数名后面以@@YA标识开始,后跟返回值和参数表;
3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:
B:const
D:char
E:unsigned char
F:short
G:unsignedshort
H:int
I:unsigned int
J:long
K:unsigned long
M:float
N:double
_N:bool
PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0
代替,一个0代表一次重复)
PB:const指针
AA:引用(&)
AB:const引用
U:类或结构体
V:Interface(接口)
W4:enum
X:void
4、@@YA标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照5、6规则处理;
5、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);
6、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是与函数返回值所使用的类相同或者与前一个参数使用的类相同),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类没有出现过的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AA或PB/AB);
7、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。
当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG。
当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI。
3、C++编译类及其成员函数时名称修饰
对于导出的C++类,仅能使用__cdecl调用约定。在编译器编译过程中,编译器会对C++类进行处理。如:class__declspec(dllexport)
MyClass会被处理为class MyClass & MyClass::operator=(classMyClass const &)。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:
1.以?标识函数名的开始,后跟?4+类名;
2.类名后面跟@@QAE标识,对于导出类来说这是固定的;
3.@@QAE后面跟A***0@ABV0@,即引用类型标识符AA+V+0(重复的类的标识符)+@(不带加号)和const性质的引用AB+V+ 0(重复的类的标识符)+@(不带加号);
4.最后以@Z标识整个名字的结束。
对于导出的C++类中的成员函数(非构造函数和析构函数),可以使用不同的调用约定。当导出的C++类中的成员函数使用__cdecl调用约定时,编译器进行以下工作:
1.以?标识函数名的开始,后跟函数名+@+类名(不带加号);
2.之后以@@QAE标识开始,后跟返回值和参数表;
3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:
B:const
D:char
E:unsigned char
F:short
G:unsignedshort
H:int
I:unsigned int
J:long
K:unsigned long
M:float
N:double
_N:bool
PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0
代替,一个0代表一次重复)
PB:const指针
AA:引用(&)
AB:const引用
U:类或结构体
V:Interface(接口)
W4:enum
X:void
4、@@QAE标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照5、6规则处理;
5、当函数返回值为当前类或带有const性质的当前类的时候,返回值的命名为:?A或?B+V+1+@@(不带加号)。当函数返回值为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,返回值的命名为:PA/AA或PB/AB+V+1+@@(不带加号);
6、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);
7、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是当前要导出的类、与函数返回值所使用的类相同或者与前一个参数使用的类相同的类),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类不是当前要导出的类的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AA或PB/AB);
8、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。
当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG。
当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI。
4、C++编译导出数据时名称修饰
对于导出的数据,仅使用__cdecl调用约定。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:
1.以?标识数据的开始,后跟数据名;
2.数据名后面以@@3标识开始,后跟数据类型;
3.当数据类型与C++类无关的时候,数据类型以下列代号表示:
B:const
D:char
E:unsigned char
F:short
G:unsignedshort
H:int
I:unsigned int
J:long
K:unsigned long
M:float
N:double
_N:bool
PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0
代替,一个0代表一次重复)
PB:const指针
AA:引用(&)
AB:const引用
U:类或结构体
V:Interface(接口)
W4:enum
X:void
4.如果数据类型是某个类的时候,数据类型的命名为:V+类名+@@(不带加号)。当数据类型为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,数据类型的命名为:PA/AA或PB/AB+V+类名+@@(不带加号);
5.最后,如果数据类型是const性质,则修饰名以B结尾。如果数据类型是非const性质,则修饰名以A结尾。
=============================================================
粉颜色的地方是觉得不准确的地方,关于名称修饰还有很多东西,但基本上DLL之中也就用到这些,所以对其它未作分析。
http://www.allaboutprogram.com/index.php?option=content&task=view&id=29&Itemid=31
HouSisong@263.net 2004.11.07整理
文章来源于abp论坛中的一篇帖子:http://bbs.allaboutprogram.com/viewtopic.php?t=1245
(原文章写于2003.12.01,这里补充了函数返回值传递方式的说明)
前言:
文章讲述了几种主要程序语言中的函数调用约定;详细说明时主要以VC6中的函数调用约定为主,阐释方式主要是以C++程序编译后得到的汇编代码来进行说明;
我所使用的编译器和平台:WindowsXP+ 赛扬1G + VC6(主要工具)\Delphi6\C++Builder6;
一:函数调用约定;
函数调用约定是函数调用者和被调用的函数体之间关于参数传递、返回值传递、堆栈清除、寄存器使用的一种约定;
它是需要二进制级别兼容的强约定,函数调用者和函数体如果使用不同的调用约定,将可能造成程序执行错误,必须把它看作是函数声明的一部分;
二:常见的函数调用约定;
VC6中的函数调用约定;
调用约定 堆栈清除 参数传递
__cdecl 调用者 从右到左,通过堆栈传递
__stdcall 函数体 从右到左,通过堆栈传递
__fastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈
thiscall 函数体 this指针默认通过ECX传递,其它参数从右到左入栈
__cdecl是C\C++的默认调用约定; VC的调用约定中并没有thiscall这个关键字,它是类成员函数默认调用约定;
C\C++中的main(或wmain)函数的调用约定必须是__cdecl,不允许更改;
默认调用约定一般能够通过编译器设置进行更改,如果你的代码依赖于调用约定,请明确指出需要使用的调用约定;
Delphi6中的函数调用约定;
调用约定 堆栈清除 参数传递
register 函数体 从左到右,优先使用寄存器(EAX,EDX,ECX),然后使用堆栈
pascal 函数体 从左到右,通过堆栈传递
cdecl 调用者 从右到左,通过堆栈传递(与C\C++默认调用约定兼容)
stdcall 函数体 从右到左,通过堆栈传递(与VC中的__stdcall兼容)
safecall 函数体 从右到左,通过堆栈传递(同stdcall)
Delphi中的默认调用约定是register,它也是我认为最有效率的一种调用方式,而cdecl是我认为综合效率最差的一种调用方式;
VC中的__fastcall调用约定一般比register效率稍差一些;
C++Builder6中的函数调用约定;
调用约定 堆栈清除 参数传递
__fastcall 函数体 从左到右,优先使用寄存器(EAX,EDX,ECX),然后使用堆栈 (兼容Delphi的register)
(register与__fastcall等同)
__pascal 函数体 从左到右,通过堆栈传递
__cdecl 调用者 从右到左,通过堆栈传递(与C\C++默认调用约定兼容)
__stdcall 函数体 从右到左,通过堆栈传递(与VC中的__stdcall兼容)
__msfastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈(兼容VC的__fastcall)
常见的函数调用约定中,只有cdecl约定需要调用者来清除堆栈;
C\C++中的函数支持参数数目不定的参数列表,比如printf函数;由于函数体不知道调用者在堆栈中压入了多少参数, 所以函数体不能方便的知道应该怎样清除堆栈,那么最好的办法就是把清除堆栈的责任交给调用者; 这应该就是cdecl调用约定存在的原因吧;
VB一般使用的是stdcall调用约定;(ps:有更强的保证吗)
Windows的API中,一般使用的是stdcall约定;(ps: 有更强的保证吗)
建议在不同语言间的调用中(如DLL)最好采用stdcall调用约定,因为它在语言间兼容性支持最好;
三:函数返回值传递方式
其实,返回值的传递从处理上也可以想象为函数调用的一个out形参数; 函数返回值传递方式也是函数调用约定的一部分;
有返回值的函数返回时:一般int、指针等32bit数据值(包括32bit结构)通过eax传递,(bool,char通过al传递,short通过ax传递),特别的__int64等64bit结构(struct) 通过edx,eax两个寄存器来传递(同理:32bit整形在16bit环境中通过dx,ax传递); 其他大小的结构(struct)返回时把其地址通过eax返回;(所以返回值类型不是1,2,4,8byte时,效率可能比较差)
参数和返回值传递中,引用方式的类型可以看作与传递指针方式相同;
float\double(包括Delphi中的extended)都是通过浮点寄存器st(0)返回;
四:通过VC中的C++例子和产生出的汇编清单来对函数调用约定进行说明;
(ps:后面虽然列出了很多汇编,但是我做了很详细的注释,我希望那些对汇编感到“恐惧”的人
也能顺利的阅读; 并为那些想在VC中使用汇编的人提供一些帮助
A:
测试代码:
int x;
int __cdecl add(int a,int b) { return a+b; }//使用__cdecl调用约定
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}
; Debug模式编译后得到的汇编代码
PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01HDUP (?) ; x变量
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
EXTRN __chkesp:NEAR
; COMDAT _main
_TEXT SEGMENT
_main PROC NEAR ; COMDAT //main函数体
push ebp ; //保存ebp的值到堆栈,退出函数前用pop ebp恢复
mov ebp, esp ; //ebp指向当前堆栈; 函数中可以通过ebp来进行堆栈访问
sub esp, 64 ; //在堆栈中开辟64byte局部空间
; //说明:这三条汇编指令是很多函数体开始的惯用法;
; //用ebp指向堆栈(不会改变);并通过ebp来访问参数和局部变量;
push ebx ; //一般按照函数间调用的约定,函数中可以自由使用eax,ecx,edx;
push esi ; //其它寄存器如果需要使用则需要保存,用完时恢复;也就是寄存器的使用约定; 这也使函数调用约定的一部分;
push edi ; //即:在函数中调用了别的函数后,eax,ecx,edx很可能已经改变,
; //而其它寄存器(ebx,esi,edi,ebp)的值可以放心继续使用(esp除外)
lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd ; //前面开辟的(16*4)byte局部空间全部填充0xCC
; //注意: 0xCC是调试中断(__asm int 3)的指令码,所以可以想象,当
; //程序错误的跳转到这个区域进行执行时将产生调试中断
push 2 ; //代码: x=add(1,2);
push 1 ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z ; 调用add函数;call指令将把下一条指令的地址(返回地址)压入堆栈
add esp, 8 ; add函数调用完以后,调用者负责清理堆栈 (__cdecl调用约定!!!)
; 两个int型参数共使用了8byte空间的堆栈
mov DWORD PTR ?x@@3HA, eax ; 将add函数的返回值存入x变量中,可以看出add函数的返回值放在eax中
xor eax, eax ; //原代码:return 0; 执行eax清零,main函数的返回值0放在eax中
pop edi
pop esi
pop ebx ; //恢复edi,esi,ebx寄存器
add esp, 64 ; //恢复64byte局部空间
cmp ebp, esp
call __chkesp ; //到这里时应该ebp==esp, Debug版进行确认,如果不等,抛出异常等
mov esp, ebp
pop ebp ; //恢复ebp寄存器
ret 0
_main ENDP
_TEXT ENDS
;//下面是add函数的代码,就不用解释的像上面那么详细了
; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8 ;//参数a相对于堆栈偏移8
_b$ = 12 ;//参数b相对于堆栈偏移12
?add@@YAHHH@Z PROC NEAR ; add,COMDAT //add函数体
push ebp
mov ebp, esp
sub esp, 64 ; 00000040H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd
mov eax, DWORD PTR _a$[ebp] ;将参数a的值移动到eax
add eax, DWORD PTR _b$[ebp] ;将参数b的值累加到eax; 可以看出返回值通过eax返回
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0 ; 函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
; ret指令将取出call指令压入的返回地址,并跳转过去继续执行
?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END
; 再来看一下Release模式编译后得到的汇编代码
; 可以看出,这比Debug模式少了很多的汇编指令,速度当然可能更快了;不再做详细说明了,请对照上面的解释
PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01HDUP (?) ; x
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
; COMDAT _main
_TEXT SEGMENT
_main PROC NEAR ; COMDAT //main函数体
push 2
push 1 ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z ; //调用add函数;
mov DWORD PTR ?x@@3HA, eax ; x
add esp, 8 ; //调用者负责清理堆栈 (__cdecl调用约定!!!)
xor eax, eax
ret 0
_main ENDP
_TEXT ENDS
; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8
_b$ = 12
?add@@YAHHH@Z PROC NEAR ; add,COMDAT //add函数体
mov eax, DWORD PTR _b$[esp-4] ;将参数b的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ;将参数a的值移动到ecx
add eax, ecx ;将ecx的值累加到eax; 返回值通过eax传递
ret 0 ;函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END
下面的分析中将只给出Release模式编译后的汇编代码
B:
声明add函数为__stdcall调用约定
int x;
int __stdcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}
;来看产生的汇编代码:
; //main函数体
push 2
push 1 ; //从右到左入栈
call ?add@@YGHHH@Z ; add
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
ret 0
; //add函数体
mov eax, DWORD PTR _b$[esp-4]
mov ecx, DWORD PTR _a$[esp-4]
add eax, ecx
ret 8 ; //函数体负责清栈 ;两个int型参数共使用了8byte空间的堆栈
C:
声明add函数为__fastcall调用约定
int x;
int __fastcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}
;来看产生的汇编代码:
; //main函数体
mov edx, 2 ; b通过寄存器edx传递
mov ecx, 1 ; a通过寄存器ecx传递
call ?add@@YIHHH@Z ; add
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
ret 0
; //add函数体
lea eax, DWORD PTR [ecx+edx] ; //a,b参数值已经在ecx,edx中,该句将这两个值的和放到eax作为返回值;
ret 0 ; //这里应该函数体负责清栈 ;但因为两个参数已经通过寄存器传递
; //了,没有使用堆栈,所以ret 0;
D:
来看一下类的成员函数的调用:
struct T
{
int start0;
T():start0(1){}
int add(int a,int b); //类成员函数;只要不明确声明调用约定则默认使用thiscall调用约定;
};
int T::add(int a,int b) { return (*this).start0+a+b; }
int x;
int main(int argc, char* argv[])
{
T t;
x=t.add(1,2);
return 0;
}
来看产生的汇编代码:
; //main函数体
push ecx ; //保存ecx
push 2
push 1 ; //参数从右到左入栈
lea ecx, DWORD PTR _t$[esp+12] ; //t的地址保存到ecx
mov DWORD PTR _t$[esp+12], 1 ; //执行t::start0=1;
call ?add@T@@QAEHHH@Z ; //调用T::add函数,这时ecx中存放了t的的地址(this指针);
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
pop ecx
ret 0
; //T::add函数体
mov eax, DWORD PTR [ecx] ; //通过this指针(保存在ecx)将start0的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ; //把a的值移动到ecx; this的值将丢失,但函数体中已经不需要了
add eax, ecx ; //将a的值累加到eax
mov ecx, DWORD PTR _b$[esp-4] ; //把b的值移动到ecx;
add eax, ecx ; //将b的值累加到eax
ret 8 ; //函数体负责清栈 ;
五: 其他
1.在VC中实现一个函数体时可以使用__declspec(naked)声明,它告诉编译器,不要为函数体自动产生开始和结束码;
2.在VC6中,想得到汇编代码清单,设置方法为:
引用:[Project]->[Setting...]->[C++]->[Category:]->[ListingFiles]->[Listing file type:]->[Assembily ,...]
3.VC6中嵌入汇编代码的方式为:
__asm { <汇编语句s> }
或 __asm <一条汇编语句>
4.VC6中重新设定函数使用的默认调用约定的方法是:
引用:
在[Project]->[Setting...]->[C++]->[ProjectOptions:]中增加编译设置
比如:/Gd 代表__cdecl; /Gr 代表__fastcall; /Gz 代表__stdcall
参考资料:
MSDN:Calling Conventions;
Delphi6\C++Builder6帮助;
请用www.google.com和www.baidu.com搜索“调用约定”或“Calling Conventions”;
函数调用约定
1、_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。
3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
_fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。
2)名字修饰约定
1、修饰名(Decoration name)
“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。
a、C编译时函数名修饰约定规则:
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。
它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。
b、C++编译时函数名修饰约定规则:
__stdcall调用约定:
1、以“?”标识函数名的开始,后跟函数名;
2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
3、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
....
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。
VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.
CB在输出函数声明时使用4种修饰符号
//__cdecl
cb的默认值,它会在输出函数名前加_,并保留此函数名不变,参数按照从右到左的顺序依次传递给栈,也可以写成_cdecl和cdecl形式。
//__fastcall
她修饰的函数的参数将尽肯呢感地使用寄存器来处理,其函数名前加@,参数按照从左到右的顺序压栈;
//__pascal
它说明的函数名使用Pascal格式的命名约定。这时函数名全部大写。参数按照从左到右的顺序压栈;
//__stdcall
使用标准约定的函数名。函数名不会改变。使用__stdcall修饰时。参数按照由右到左的顺序压栈,也可以是_stdcall;
DLL中调用约定和名称修饰(一)
调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定。
在C++中,为了允许操作符重载和函数重载,C++编译器往往按照某种规则改写每一个入口点的符号名,以便允许同一个名字(具有不同的参数类型或者是不同的作用域)有多个用法,而不会打破现有的基于C的链接器。这项技术通常被称为名称改编(Name Mangling)或者名称修饰(Name Decoration)。许多C++编译器厂商选择了自己的名称修饰方案。
因此,为了使其它语言编写的模块(如Visual Basic应用程序、Pascal或Fortran的应用程序等)可以调用C/C++编写的DLL的函数,必须使用正确的调用约定来导出函数,并且不要让编译器对要导出的函数进行任何名称修饰。
1.调用约定(Calling Convention)
调用约定用来处理决定函数参数传送时入栈和出栈的顺序(由调用者还是被调用者把参数弹出栈),以及编译器用来识别函数名称的名称修饰约定等问题。在Microsoft VC++ 6.0中定义了下面几种调用约定,我们将结合汇编语言来一一分析它们:
1、__cdecl
__cdecl是C/C++和MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl。
下面将通过一个具体实例来分析__cdecl约定:
在VC++中新建一个Win32 Console工程,命名为cdecl。其代码如下:
int__cdecl Add(int a, int b); //函数声明
voidmain()
{
Add(1,2); //函数调用
}
int__cdecl Add(int a, int b) //函数实现
{
return (a + b);
}
函数调用处反汇编代码如下:
;Add(1,2);
push 2 ;参数从右到左入栈,先压入2
push 1 ;压入1
call @ILT+0(Add)(00401005) ;调用函数实现
add esp,8 ;由函数调用清栈
2、__stdcall
__stdcall调用约定用于调用Win32API函数。采用__stdcal约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。__stdcall可以写成_stdcall。
还是那个例子,将__cdecl约定换成__stdcall:
int__stdcall Add(int a, int b)
{
return(a + b);
}
函数调用处反汇编代码:
;Add(1,2);
push 2 ;参数从右到左入栈,先压入2
push 1 ;压入1
call @ILT+10(Add)(0040100f) ;调用函数实现
函数实现部分的反汇编代码:
;int __stdcall Add(int a, intb)
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
repstos dwordptr [edi]
;return(a + b);
mov eax,dwordptr [ebp+8]
add eax,dwordptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;清栈
3、__fastcall
__fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall。
依旧是相类似的例子,此时函数调用约定为__fastcall,函数参数个数增加2个:
int__fastcall Add(int a, double b, int c, int d)
{
return(a + b + c + d);
}
函数调用部分的汇编代码:
;Add(1,2, 3, 4);
push 4 ;后两个参数从右到左入栈,先压入4
mov edx,3 ;将int类型的3放入edx
push 40000000h ;压入double类型的2
push 0
mov ecx,1 ;将int类型的1放入ecx
call @ILT+0(Add)(00401005) ;调用函数实现
函数实现部分的反汇编代码:
; int __fastcall Add(int a,double b, int c, int d)
push ebp
mov ebp,esp
sub esp,48h
push ebx
push esi
push edi
push ecx
lea edi,[ebp-48h]
mov ecx,12h
mov eax,0CCCCCCCCh
repstos dwordptr [edi]
pop ecx
mov dwordptr [ebp-8],edx
mov dwordptr [ebp-4],ecx
;return(a + b + c + d);
fild dwordptr [ebp-4]
fadd qwordptr [ebp+8]
fiadd dwordptr [ebp-8]
fiadd dwordptr [ebp+10h]
call __ftol(004011b8)
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 0Ch ;清栈
关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...->C/C++->Code Generation项选择。它们对应的命令行参数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。
DLL中调用约定和名称修饰(二)
4、thiscall
thiscall调用约定是C++中的非静态类成员函数的默认调用约定。thiscall只能被编译器使用,没有相应的关键字,因此不能被程序员指定。采用thiscall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,只是另外通过ECX寄存器传送一个额外的参数:this指针。
这次的例子中将定义一个类,并在类中定义一个成员函数,代码如下:
class CSum
{
public:
int Add(int a, int b)
{
return (a + b);
}
};
void main()
{
CSum sum;
sum.Add(1, 2);
}
函数调用部分汇编代码:
;CSum sum;
;sum.Add(1, 2);
push 2 ;参数从右到左入栈,先压入2
push 1 ;压入1
lea ecx,[ebp-4] ;ecx存放了this指针
call @ILT+5(CSum::Add) (0040100a) ;调用函数实现
函数实现部分汇编代码:
;int Add(int a, int b)
push ebp
mov ebp,esp
sub esp,44h ;多用了一个4bytes的空间用于存放this指针
push ebx
push esi
push edi
push ecx
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0CCCCCCCCh
rep stos dwordptr [edi]
pop ecx
mov dwordptr [ebp-4],ecx
;return (a + b);
mov eax,dwordptr [ebp+8]
add eax,dwordptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;清栈
5、naked属性
采用上面所述的四种调用约定的函数在进入函数时,编译器会产生代码来保存ESI、EDI、EBX、EBP寄存器中的值,退出函数时则产生代码恢复这些寄存器的内容。对于定义了naked属性的函数,编译器不会自动产生这样的代码,需要你手工使用内嵌汇编来控制函数实现中的堆栈管理。由于naked属性并不是类型修饰符,故必须和__declspec共同使用。下面的这段代码定义了一个使用了naked属性的函数及其实现:
__declspec ( naked ) func()
{
int i;
int j;
_asm
{
push ebp
mov ebp, esp
sub esp,__LOCAL_SIZE
}
_asm
{
mov esp,ebp
pop ebp
ret
}
}
naked属性与本节关系不大,具体请参考MSDN。
6、WINAPI
还有一个值得一提的是WINAPI宏,它可以被翻译成适当的调用约定以供函数使用。该宏定义于windef.h之中。下面是在windef.h中的部分内容:
#define CDECL _cdecl
#define WINAPI CDECL
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define APIENTRY WINAPI
由此可见,WINAPI、CALLBACK、APIENTRY等宏的作用。
2.名称修饰(Name Decoration)
C或C++函数在内部(编译和链接)通过修饰名(Decoration Name)识别。函数的修饰名是编译器在编译函数定义或者原型时生成的字符串。编译器在创建.obj文件时对函数名称进行修饰。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出C++重载函数、构造函数、析构函数,又如在汇编代码里调用C或C++函数等。
在VC++中,函数修饰名由编译类型(C或C++)、函数名、类名、调用约定、返回类型、参数等多种因素共同决定。下面分C编译、C++编译(非类成员函数)和C++类及其成员函数编译三种情况说明:
1、C编译时函数名称修饰
当函数使用__cdecl调用约定时,编译器仅在原函数名前加上一个下划线前缀,格式为_functionname。例如:函数int __cdecl Add(int a, int b),输出后为:_Add。
当函数使用__stdcall调用约定时,编译器在原函数名前加上一个下划线前缀,后面加上一个@符号和函数参数的字节数,格式为_functionname@number。例如:函数int __stdcall Add(int a, intb),输出后为:_Add@8。
当函数是用__fastcall调用约定时,编译器在原函数名前加上一个@符号,后面是加一个@符号和函数参数的字节数,格式为@functionname@number。例如:函数int __fastcall Add(int a, intb),输出后为:@Add@8。
以上改变均不会改变原函数名中的字符大小写。
DLL中调用约定和名称修饰(三)
2、C++编译时函数(非类成员函数)名称修饰
当函数使用__cdecl调用约定时,编译器进行以下工作:
1.以?标识函数名的开始,后跟函数名;
2.函数名后面以@@YA标识开始,后跟返回值和参数表;
3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:
B:const
D:char
E:unsigned char
F:short
G:unsignedshort
H:int
I:unsigned int
J:long
K:unsigned long
M:float
N:double
_N:bool
PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0
代替,一个0代表一次重复)
PB:const指针
AA:引用(&)
AB:const引用
U:类或结构体
V:Interface(接口)
W4:enum
X:void
4、@@YA标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照5、6规则处理;
5、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);
6、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是与函数返回值所使用的类相同或者与前一个参数使用的类相同),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类没有出现过的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AA或PB/AB);
7、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。
当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG。
当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI。
3、C++编译类及其成员函数时名称修饰
对于导出的C++类,仅能使用__cdecl调用约定。在编译器编译过程中,编译器会对C++类进行处理。如:class__declspec(dllexport)
MyClass会被处理为class MyClass & MyClass::operator=(classMyClass const &)。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:
1.以?标识函数名的开始,后跟?4+类名;
2.类名后面跟@@QAE标识,对于导出类来说这是固定的;
3.@@QAE后面跟A***0@ABV0@,即引用类型标识符AA+V+0(重复的类的标识符)+@(不带加号)和const性质的引用AB+V+ 0(重复的类的标识符)+@(不带加号);
4.最后以@Z标识整个名字的结束。
对于导出的C++类中的成员函数(非构造函数和析构函数),可以使用不同的调用约定。当导出的C++类中的成员函数使用__cdecl调用约定时,编译器进行以下工作:
1.以?标识函数名的开始,后跟函数名+@+类名(不带加号);
2.之后以@@QAE标识开始,后跟返回值和参数表;
3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:
B:const
D:char
E:unsigned char
F:short
G:unsignedshort
H:int
I:unsigned int
J:long
K:unsigned long
M:float
N:double
_N:bool
PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0
代替,一个0代表一次重复)
PB:const指针
AA:引用(&)
AB:const引用
U:类或结构体
V:Interface(接口)
W4:enum
X:void
4、@@QAE标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照5、6规则处理;
5、当函数返回值为当前类或带有const性质的当前类的时候,返回值的命名为:?A或?B+V+1+@@(不带加号)。当函数返回值为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,返回值的命名为:PA/AA或PB/AB+V+1+@@(不带加号);
6、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);
7、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是当前要导出的类、与函数返回值所使用的类相同或者与前一个参数使用的类相同的类),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类不是当前要导出的类的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AA或PB/AB);
8、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。
当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG。
当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI。
4、C++编译导出数据时名称修饰
对于导出的数据,仅使用__cdecl调用约定。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:
1.以?标识数据的开始,后跟数据名;
2.数据名后面以@@3标识开始,后跟数据类型;
3.当数据类型与C++类无关的时候,数据类型以下列代号表示:
B:const
D:char
E:unsigned char
F:short
G:unsignedshort
H:int
I:unsigned int
J:long
K:unsigned long
M:float
N:double
_N:bool
PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0
代替,一个0代表一次重复)
PB:const指针
AA:引用(&)
AB:const引用
U:类或结构体
V:Interface(接口)
W4:enum
X:void
4.如果数据类型是某个类的时候,数据类型的命名为:V+类名+@@(不带加号)。当数据类型为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,数据类型的命名为:PA/AA或PB/AB+V+类名+@@(不带加号);
5.最后,如果数据类型是const性质,则修饰名以B结尾。如果数据类型是非const性质,则修饰名以A结尾。
=============================================================
粉颜色的地方是觉得不准确的地方,关于名称修饰还有很多东西,但基本上DLL之中也就用到这些,所以对其它未作分析。
相关文章推荐
- java类的初始化顺序
- 关于s:iterator 和s:if 的结合使用
- 【面试题二十】顺时针打印矩阵
- @selector 如何调用在另一个类中的静态函数?
- oracle:rman恢复----通过时间set until time
- 一个imap的例子 - 参考,代码仅参考,复制后不可用
- Javascript 使用 "大杂烩"
- android之fragment
- 我的第一个App开发过程
- 菜根谭#17
- FFMPEG中关于ts流的时长估计的实现(转)
- SharePoint 2013 Farm 安装指南——构建一个双层SharePoint Farm
- tomcat对于context.xml的配置报错问题
- SQL数据库显示恢复挂起的解决方法
- BeagleBone Black USB一线通(3)
- C#中的try与finally
- Linux学习笔记
- 【仿乐享微信源码免费分享】99%的人都不知道的微信功能!
- C#特性学习与使用(为枚举定义Description)
- 哈希表的C语言实现