C++编译器的函数名修饰规则

原创
小哥 2年前 (2023-05-22) 阅读数 2 #大杂烩

转自:http://mxdxm.iteye.com/blog/510486

函数名称修改(Decorated Name)方式

函数名称修改(Decorated Name编译器在编译过程中创建的字符串,用于指示函数的定义或原型。LINK程序或其他工具有时需要指定函数的名称修改,以找到函数的正确位置。在大多数情况下,程序员不需要知道函数的名称修改,LINK程序或其他工具将自动区分它们。当然,在某些情况下,有必要指定函数的名称修改,例如在C++在程序中,为了LINK如果程序或其他工具可以匹配正确的函数名称,则需要为重载函数和某些特殊函数(如构造函数和析构函数)指定名称修饰。需要指定函数名称修改的另一种情况是调用C或C++的功能。如果函数名称、调用约定、返回值类型或函数参数发生任何更改,则原始名称修改不再有效,必须指定新的名称修改。C和C++程序的功能在内部使用不同的名称修饰方法,下面将分别介绍这两种方法。

  1. C在编译器中修改函数名称的规则

对于__stdcall调用约定,编译器和链接器会在输出函数名称前添加下划线前缀,在函数名后添加下划线前缀”@“符号的字节数及其参数,例如_functionname@number。__cdecl调用约定仅在输出函数名称前面加上下划线,例如_functionname。__fastcall调用约定在输出函数名称前面加上”@“符号,后跟一个”@“符号的字节数及其参数,例如@functionname@number

  1. C++在编译器中修改函数名称的规则

C++函数名称的修改规则有些复杂,但信息更全面。通过分析修改名称,不仅可以知道函数的调用方法、返回值类型、参数数量,甚至参数类型。不管__cdecl,__fastcall还是__stdcall调用方法和函数修饰均基于”?“开始,后跟函数的名称,后跟参数表的起始标识符和根据参数类型代码拼写的参数表。大约__stdcall参数表的起始标识符为”@@YG”,对于__cdecl方法是”@@YA”,对于__fastcall方法是”@@YI"。参数表的拼写代码如下所示:
X--void
D--char
E--unsigned char
F--short
H--int
I--unsigned int
J--long
K--unsigned long(DWORD)
M--float
N--double
_N--bool
U--struct
....
指针的工作方式有些独特,使用PA表示指针,使用PB表示const指向类型的指针。以下代码指示指针类型。如果相同类型的指针连续出现”0“替换,一个”0“代表重复。U表示结构的类型,通常后跟结构的类型名称,使用”@@“表示结构类型名称的结尾。函数的返回值不被视为特殊值,它的描述方式与函数参数相同,紧跟在参数表的开始标志之后。也就是说,函数参数表中的第一项实际上表示函数的返回值类型。参数表后”@Z“标识整个名称的结尾。如果函数没有参数,则”Z“身份识别结束。下面是两个示例,如果有以下函数声明:

int Function1 (char *var1,unsigned long);
它的函数修饰符名称是”?Function1@@YGHPADK@Z“对于函数声明:
void Function2();
函数修饰符名称为”?Function2@@YGXXZ” 。

对于C++的类成员函数(其调用方法是thiscall)函数名称修改和非成员C++功能略有不同,首先,通过插入”@“字符指导类名称;其次,参数表的起始标识符不同,public(public)成员函数的标识为”@@QAE”,保护(protected)成员函数的标识为”@@IAE”,私有(private)成员函数的标识为”@@AAE“如果函数声明使用const关键字,对应的标识符应为”@@QBE”,“@@IBE”和“@@ABE"。如果参数类型是对类实例的引用,请使用”AAV1”,对于const键入,然后使用”ABV1"。以下是课程CTest为例说明C++修改成员函数名称的规则:
class CTest
{
......
private:
void Function(int);
protected:
void CopyInfo(const CTest &src);
public:
long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);
long InsightClass(DWORD dwClass) const;
......
};

对于成员函数Function,它的函数修饰符名称是”?Function@CTest@@AAEXH@Z“、字符串”@@AAE“表示这是一个私人功能。成员函数CopyInfo类只有一个参数CTest的const引用参数,其函数修饰符名称为”?CopyInfo@CTest@@IAEXABV1@@Z”。DrawText它是一个相对复杂的函数声明,不仅有字符串参数,还有结构参数和HDC处理参数,需要注意的是HDC实际上,这是一个HDC__指向结构类型的指针,此参数的表示形式为”PAUHDC__@@“功能齐全的装饰命名”?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z”。InsightClass它是一个共享实体const函数,其成员函数标识符为”@@QBE“完整的修饰符名称是”?InsightClass@CTest@@QBEJK@Z”。

无论是C如何修改函数名称C++函数名的修改不会改变输出函数名中字符的大小写,这与PASCAL调用约定不同,PASCAL约定输出的函数名称未修改,全部大写。

3.查看函数的名称修饰

有两种方法可以检查程序中函数的名称修饰:使用编译的输出列表或使用Dumpbin工具。用/FAc,/FAs或/FAcs命令行参数可以允许编译器输出函数或变量名称的列表。用dumpbin.exe /SYMBOLS也可以获取命令obj文件或lib文件中的函数或变量名称列表。此外,您还可以使用 undname.exe 将修饰名称转换为未修饰的形式。

函数调用约定与名称修改规则不匹配导致的常见问题
如果在函数调用期间发生堆栈异常,则十分之九是由函数调用约定中的不匹配引起的。例如,动态链接库a有以下导出功能:long MakeFun(long lFun);
生成动态库时使用的函数调用约定是__stdcall所以编译的a.dll中函数MakeFun的调用约定是_stdcall,也就是函数调用时参数从右向左推送,函数返回时自己还原堆栈。现在某个程序模块b要引用a中的MakeFun,b和a一样使用C++按方法编译,只需b模块的函数调用方法是__cdecl,由于b包含了a在提供的头文件中MakeFun函数声明,所以MakeFun在b由模块中的其他人调用MakeFun的功能被认为是__cdecl调用方法,b在模块中调用这些函数后MakeFun当然,我们需要帮助恢复堆栈,但是MakeFun我已经在最后自己恢复了堆栈,b模块中函数的过度使用会导致堆栈指针错误,进而触发堆栈异常。宏观现象是函数调用没有问题(因为参数传递的顺序是一样的),MakeFun我也完成了我自己的函数,但函数返回并显示错误。解决方案也非常简单,只要两个模块在编译时设置了相同的函数调用约定即可。
了解了函数调用约定和名称修改规则之后,我们来看看C++在程序中使用C在语言编译库中频繁出现LNK 2001错误非常简单。以上示例中的两个模块为例,两个模块都是使用__stdcall调用约定,但是a.dll使用C语法编译语言(C语言风格),所以a.dll的载入库a.lib中MakeFun函数的名称修改为”_MakeFun@4”。b包含了a在提供的头文件中MakeFun函数声明,但由于b采用的是C++语言编译,所以MakeFun在b在模块中,根据C++命名的名称修改规则是”?MakeFun@@YGJJ@Z“编译过程是和平的,当链接程序时c++的链接器a.lib中去找“?MakeFun@@YGJJ@Z”,但是a.lib中只有“_MakeFun@4“,不”?MakeFun@@YGJJ@Z“然后链接器报告:

error LNK2001: unresolved external symbol ?MakeFun@@YGJJ@Z

解决方案和简单性是使b模块知道这个函数是C编译语言,extern "C"这是可以实现的。收养C用于语言编译的库应考虑使用此库的程序可能是C++程序(使用C++编译器),因此在设计头文件时应注意这一点。头文件通常应声明如下:

ifdef _cplusplus

extern "C" {

endif

long MakeFun(long lFun);

ifdef _cplusplus

}

endif

这样C++编译器知道MakeFun的修改名称是”_MakeFun@4“不会有链接错误。

很多人不明白为什么我用的编译器都是VC编译器还将生成”error LNK2001“错误?实际上VC编译器将根据源文件的扩展名选择编译方法。如果文件的扩展名是”.C“编译器将采用C如果扩展名是”.cpp“编译器将使用C++编译程序的最佳方法是使用extern "C"。

1.__stdcall

以“?“确定函数名称的开头,后跟函数名称; 函数名称后跟”@@YG“确定参数表的开头,然后是参数表;
参数表由代码表示: 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“代表重复;
参数表中的第一项是函数的返回值类型,后跟参数的数据类型,指针在它所指的数据类型之前标识;
参数表后”@Z“标识整个名称的结尾。如果函数没有参数,则”Z“身份识别结束。 它的格式是”?functionname@@YG****@Z”或“?functionname@@YGXZ”, 例如 int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z” void Test2() -----“?Test2@@YGXXZ”
2 __cdecl调用约定: 规则同上 _stdcall 调用约定,只有参数表的起始标识符由”@@YG”变为“@@YA”。

3 __fastcall调用约定: 规则同上_stdcall调用约定,只有参数表的起始标识符由”@@YG”变为“@@YI”。

VC++函数缺少的声明是"__cedcl",只能是C/C++调用。

CB输出函数声明时使用4物种修饰符符号 :

__cdecl cb 将在输出函数名称之前添加默认值 "_"并保持函数名称不变。参数按从右到左的顺序传递给堆栈,也可以写为_cdecl和cdecl形式。
__fastcall 修改后的函数的参数将尽可能使用寄存器进行处理,函数名称前缀为@参数按从左到右的顺序堆叠;
__pascal 它描述的函数名称使用 Pascal 格式的命名约定。此时,函数名称全部大写。参数按从左到右的顺序堆叠;
__stdcall 使用标准约定函数名称。函数名称不会更改。用 __stdcall 修改时。参数按从右到左的顺序堆叠,也可以_stdcall;

C语言函数调用约定

在C在语言中,假设我们有这样的函数:

int function(int a,int b)

呼叫时,只需使用 result = function(1,2) 这样,您就可以使用此功能。但是,当高级编程语言被编译成计算机可以识别的机器代码时,就会出现问题:CPU在计算机中,计算机无法知道函数调用需要多少参数和哪些参数,并且没有硬件来存储这些参数。也就是说,计算机不知道如何将参数传递给这个函数,传递参数的工作必须由函数调用者和函数本身协调。为此,计算机提供了一种称为堆栈的数据结构来支持参数传输。

堆栈是一种顺序输入先出的数据结构,在堆栈顶部具有存储区域和指针。堆栈指针的顶部指向堆栈中的第一个可用数据项(称为堆栈顶部)。用户可以将数据添加到堆栈顶部的堆栈中,这称为堆栈推送 (Push)按下堆栈后,堆栈顶部会自动更改为新添加的数据项的位置,堆栈顶部的指针也会相应修改。用户还可以删除堆栈的顶部,这称为弹出堆栈 (pop)堆栈弹出后,堆栈顶部的元素将成为堆栈顶部,堆栈顶部的指针也会相应修改。

调用函数时,调用方依次按下堆栈上的参数,然后调用该函数。调用函数后,在堆栈中获取数据并进行计算。函数计算完成后,调用方或函数本身修改堆栈以恢复其原始状态。

在参数传输中必须明确解决两个重要问题:

当有多个参数时,参数按什么顺序推送到堆栈上
谁将在函数调用后将堆栈还原到其原始状态
在高级编程语言中,函数调用约定用于说明这两个问题。常见的调用约定包括:

stdcall
cdecl
fastcall
thiscall
naked call

stdcall调用约定
stdcall通常称为pascal调用约定,因为pascal它是一种常见的早期教学计算机编程语言,具有严格的语法和使用函数调用约定。stdcall。在Microsoft C++系列的C/C++在编译器中,通常使用PASCAL宏声明这个调用约定,类似的宏也有WINAPI和CALLBACK。

stdcall调用约定声明的语法是(以前面的函数为例:

int __stdcall function(int a,int b)

stdcall调用约定的意思是:1)参数从右到左推到堆栈上,2)函数自修改栈 3)函数名称前面自动带有下划线,后跟@符号,后跟参数的大小

以上述函数为例,参数b首先推送到堆栈上,然后是参数a、函数调用function(1,2)将调用转换为汇编语言将导致:

push 2      // 第二个参数被推送到堆栈上
push 1      // 第一个参数被推送到堆栈上
call function // 调用参数并注意自动cs:eip入栈

对于函数本身,它可以翻译为:

push ebp     // 保存ebp寄存器,将用于存储堆栈的顶部指针,可以在函数退出时恢复
mov ebp,esp // 保存堆栈指针
mov eax,[ebp + 8H] // 堆栈中ebp在指向位置之前按顺序保存 ebp,cs:eip,a,b,ebp + 8指向 a
add eax,[ebp + 0CH] // 堆栈中ebp + 1 2处保存了b
mov esp,ebp        // 恢复esp
pop ebp
ret 8
在编译时,此函数的名称被翻译为_function@8

请注意,不同的编译器会插入自己的汇编代码以提供编译通用性,但整体代码是相同的。
在函数开头保留esp到ebp在函数末尾恢复是编译器常用的方法。

从函数调用的角度来看,2和1依次被push进入堆栈,而在函数中,相对ebp(堆栈指针进入函数时的偏移量访问参数。
函数结束后,ret 8 表示清理8一堆字节,函数恢复了栈本身。

cdecl调用约定
cdecl 调用约定,也称为C呼叫约定,是的C语言的默认调用约定为:

int function (int a ,int b) //没有装饰C调用约定
int __cdecl function(int a,int b) //明确指出C调用约定

cdecl调用约定参数栈的顺序与 stdcall它是一样的,参数首先从左到右推送到堆栈上。
区别在于函数本身不清理堆栈,调用方负责清理堆栈。
由于这一变化,C 调用约定允许对函数进行无限数量的参数,这也是C语言的一大特征。
对于上一个function函数, 使用cdecl之后的汇编代码变为:

调用处
push 1
push 2
call functionadd
esp,8   // 注意:在这里,调用方正在恢复堆栈

调用函数_function处
push ebp      // 保存ebp寄存器,将用于存储堆栈的顶部指针,可以在函数退出时恢复
mov ebp,esp // 保存堆栈指针
mov eax,[ebp + 8H] // 堆栈中ebp在指向位置之前按顺序保存 ebp, cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] // 堆栈中ebp + 12处保存了b
mov esp,ebp         // 恢复esp
pop ebp
ret         // 请注意,此处未修改堆栈

MSDN在中文中,此修饰符会自动在函数名称前添加前导下划线,因此函数名称在符号表中记录为_function但是在编译过程中我似乎没有看到这种变化。
由于参数按从右到左的顺序堆叠,因此初始参数位于最靠近堆栈顶部的位置。因此,当使用无限数量的参数时,可以确定堆栈中第一个参数的位置。只要可以根据前者与后者的后续显式参数确定不确定数量的参数,就可以使用不定参数,例如CRT中的sprintf函数,定义为:

int sprintf(char buffer,const char format,...)

因为所有不确定的参数都可以通过 format 当然,所以使用无限数量的参数没有问题。

fastcall调用约定
fastcall调用约定和stdcall同样,这意味着:

第一和第二功能DWORD参数(或更小的尺寸)通过ecx和edx通过,其他参数按从右到左的顺序堆叠
被调用的函数清理堆栈
修改函数名称的规则与stdcall
声明语法为:int fastcall function(int a,int b)

为了说明此调用约定,请定义以下类和用法代码:
class A
{
public:
int function1(int a,int b);
int function2(int a,...);
};

int A::function1 (int a,int b)
{
return a+b;
}

int A::function2(int a,...)
{
va_list ap;

va_start(ap,a);

int i;
int result = 0;

for(i = 0 ; i < a ; i ++)
{
result += va_arg(ap,int);
}

return result;
}

void callee()
{
A a;
a.function1 (1,2);
a.function2(3,1,2,3);
}

// 以下汇编代码来自原始文章。我觉得有问题,所以我应该自己拆开看看

//函数function1调用0401C1D
push        200401C1F
push        100401C21
lea         ecx,[ebp-8]00401C24
call function1
// 注意,这里this未推送到堆栈上
//函数function2调用00401C29
push        300401C2B
push        200401C2D
push        100401C2F
push        300401C31
lea         eax,[ebp-8]
这里引入this指针00401C34
push        eax00401C35
call   function200401C3A
add         esp,14h

我修改和分析了以下代码:

上面的C++代码,必须包括 stdarg.h 提供动态参数头文件

int A::function1 (int a,int b)     //
{
004113A0 push        ebp
004113A1 mov         ebp,esp
004113A3 sub         esp,0CCh
004113A9 push        ebx
004113AA push        esi
004113AB push        edi
004113AC push        ecx
004113AD lea         edi,[ebp-0CCh]
004113B3 mov         ecx,33h
004113B8 mov         eax,0CCCCCCCCh
004113BD rep stos    dword ptr es:[edi]
004113BF pop         ecx
004113C0 mov         dword ptr [ebp-8],ecx
return a+b;
004113C3 mov         eax,dword ptr [a]
004113C6 add         eax,dword ptr [b]
}

004113C9 pop         edi
004113CA pop         esi
004113CB pop         ebx
004113CC mov         esp,ebp
004113CE pop         ebp
004113CF ret         8

void callee()
{
00411460 push        ebp
00411461 mov         ebp,esp
00411463 sub         esp,0CCh
00411469 push        ebx
0041146A push        esi
0041146B push        edi
0041146C lea         edi,[ebp-0CCh]
00411472 mov         ecx,33h
00411477 mov         eax,0CCCCCCCCh
0041147C rep stos    dword ptr es:[edi]
A a;
a.function1 (1,2);
0041147E push        2                // 参数 2 入栈
00411480 push        1                 // 参数 1 入栈
00411482 lea         ecx,[a]           // this 指针 ----> ECX
00411485 call        A::function1 (411050h)
a.function2(3,1,2,3);
0041148A push        3
0041148C push        2
0041148E push        1
00411490 push        3
00411492 lea         eax,[a]          // 这里 this 指针已被推到堆栈上,相比之下 callee 对 function1 的调用,

00411495 push        eax             // 对 this 处理方式不同
00411496 call        A::function2 (411122h)     // 调用方在此处未自行恢复堆栈

// 由于上面的堆叠顺序,可以看出在 function 2中 当保存ebp 后(打开stack frame后)堆栈的状态如下.
ebp               // 保存的 EBP 的值, 且 此时ebp指向该处
RetAddr       // 返回地址
this指针       // 入栈的 this 指针
参数 3          // 以下是堆叠的参数, 从右向左推送
参数 1
参数 2
参数 3

0041149B add         esp,14h                            // 调用方在此处自行恢复堆栈

//.............以下汇编代码是 检查堆栈和还原 callee 堆栈操作,不再写入
}

可以看出,对于固定数量的参数,类似于stdcall如果不是定时,则相似cdecl

naked call 调用约定
这是一般程序员不推荐的罕见调用约定。编译器不会向这种类型的函数添加初始化和清理代码,更具体地说,你不能使用return返

返回值只能使用插入程序集返回。这通常用于实模式驱动程序设计,假设定义了求和加法器,可以定义为:

__declspec(naked) int add(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret
}

请注意,此函数未显式定义return返回值,通过修改返回eax寄存器实现和退出函数串联ret必须明确插入说明。

上面的代码被翻译成汇编后,它变成了:

mov eax,[ebp+8]
add eax,[ebp+12]
ret 8

请注意,此修改与__stdcall及cdecl它与cdecl结合使用的代码stdcall组合代码变为:

__declspec(naked) int __stdcall function(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret 8        //注意以下几点8
}
至于调用的这种类型的函数,它与普通函数不同cdecl及stdcall一致地调用函数。

函数调用约定导致的常见问题
如果定义和使用约定不一致,则可能导致堆栈损坏和严重问题。以下是两个常见问题:
函数原型声明和函数体定义不一致
DLL导入函数时声明了不同的函数约定
以后者为例,我们假设我们dll函数声明为:

__declspec(dllexport) int func(int a,int b);//注意,没有stdcall用cdecl
使用的代码是:

typedef int (*WINAPI DLLFUNC)func(int a,int b);
hLib = LoadLibrary(...);
DLLFUNC func = (DLLFUNC)GetProcAddress(...) //此处修改了调用约定
result = func(1,2);       //导致错误
由于来电者不理解WINAPI这种装饰的错误添加必然导致堆栈被破坏,
MFC编译时插入checkesp该函数将告诉您堆栈已被销毁。

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除