解析VC++ Name Mangling 机制

摘要

在C++中,Name Mangling 是为了支持重载而加入的一项技术,目前C++ Name Mangling 并没有统一的标准,也没有较完整的中文文档化资料,所以本篇文章在VS2005环境中,解析C++ Name Mangling 的技术细节,以及怎样将VC Name Mangling后的名称还原为可读的形式。

Name Mangling 简介

Name Mangling 是一种在编译过程中,将函数、变量的名称重新改编的机制。在 C++重载、namespace等操作符下,函数可以有同样的名字,编译器为了区分各个不同地方的函数,将各个函数通过编译器内定的算法,将函数改成唯一的名称。
Name Mangling翻译成中文意思为:名字修饰、名字改编等,由于对这个翻译没有统一的约定,所以本文中采用英文表示。
在写VC++程序时,我们有时会遇到类似于“error LNK2019: unresolved external symbol “void __cdecl MyFun (void)” (?MyFun@@YAXXZ) referenced in function _wmain”的连接错误,此语句中“?MyFun@@YAXXZ”是VC Name Mangling后的结果。本文主要讨论Name Mangling 后名称还原为可读的方式。
本文首先讨论 VC 环境中,C/C++ 语言的 Name Mangling 算法机制。然后讨论手动将 C++ 语言 Name Mangling 后的字符串转换为函数的定义式,最后编码实现还原。

  1. VC环境中Name Mangling
    在VC中,微软采用了自己独特的Name Mangling技术,当然微软也为此Name Mangling技术申请了专利。
    想要查看 VC 将函数名称 Name Mangling 的结果,只需将函数声明,不实现,然后调用之即可。
    下面先讨论VC环境中C语言的Name Mangling技术,然后再讨论C++。

  2. VC环境中C 语言的 Name Mangling
    在VC中,也可以采用C语言编译器,只需要如下设置:Project ->Property ->Configuration Properties -> C/C++ -> Advanced Compile As,将其设为“(/TC)”即可
    在 C 语言中,函数可以有如下声明方式(其中 __CALLTYPE 可以为 __cdecl、__stdcall、__fastcall等)

1、void __CALLTYPE fun();
2、int __CALLTYPE fun(int); ;

假设此处 CALLTYPE 为 __cdecl(即:#define __CALLTYPE __cdecl),Name Mangling 结果

1、_fun
2、_fun

看看其他调用方式,如:__stdcall(#define __CALLTYPE __stdcall),结果如下:

1、_fun@0
2、_fun@4

__fastcall 的结果(#define __CALLTYPE __ fastcall):
1、@fun@0
2、@fun@4

由此,我们可以得出结论,从被 Name Mangling 后的字符串推断出原来的函数名。
1、__cdecl
在此声明方式下,仅仅在函数名前加一个下划线,至于函数返回值、参数,完全没有处理。
2、__stdcall
在此声明方式下,在函数名前加一个下划线,然后紧跟“@”符号,最后是函数参数大小总和(注意:此总和包含了字节填充)。
3、__fastcall
在此声明方式下,跟 __stdcall 唯一不同的是,函数前面的下划线变为了“@”符号。

由上面 5 个实例函数,我们大概可以看到 VC 环境中,C 语言 Name Mangling 技术了,但也可以发现,从 Name Mangling 后的字符串,并不能得出函数原来的定义式。不同的定义式,Name Mangling 后的名称可以相同,由此也可以知道,C 语言不支持函数重载。

Name mangling或者Decorated Name是指程序设计语言中具有存储性质的对象的名字被编译器改写,以适合编译器、链接器(linker)、汇编器(assembler)使用[1]。 所谓的具有存储性质的对象,即lvalue对象,是指要实际占用内存空间、有内存地址的那些实体对象,例如:变量(variables)、函数、函数指针 等。C++中的纯虚函数作为特例也属于这一范畴。而数据类型(data type)就不属于具有存储性质的对象。
对于支持重载(overload)的程序设计语言,name mangling是必需的因而具有特别重要意义。C++允许函数重载
int foo(int i);
void foo(char c);
C++也允许变量名称在不同限定(qualifier)下同名:
std::cout;
::cout;

因此C++的编译器必须给这些函数及变量不同的名字以供程序编译链接、载入时的内部辨别使用。
C++的编译器可大略分为Windows平台上的Microsoft Visual C++与类Linux平台上的GNU GCC/g++两大类,分别成了各自操作系统环境下的业界工业标准。例如,Windows平台上的Intel C++ Compiler(ICC)与Digital Mars C++,都与Visual C++保持了二进制兼容(Application Binary Interface, ABI)。而Linux平台上的Intel C++ Compiler(ICC)与HP aC++,都与GCC 3.x/4.x做到二进制兼容。GCC是开源产品,它的内部实现机制是公开的;而Visual C++不公开它内部实现细节,因此在name mangling上并无详尽的正式文档,Visual C++ name mangling的细节属于hacker行为。
一般情况下,编程者不需要知道C/C++函数的修饰名字。但是,如果在汇编源程序或者内联汇编中引用了C/C++函数,就必须使用其正确的修饰名字。

C语言的name mangling

C语言并 不支持重载,因此C程序中禁止函数与函数同名,也就没有必要做name mangling。但C语言的函数调用协议(calling conventions)五花八门,用某一种调用协议编译的静态库或动态库的函数,如果用另外的调用协议去调用将会导致错误甚至系统崩溃。因此C语言编译 器对函数的名字做少量的修饰,用于区别该函数支持哪种调用协议,这可以给编译、链接、特别是库函数的载入提供额外的检查信息。

目前,C语言常用的调用协议有三种:cdecl, stdcall与fastcall,其它众多调用协议如__pascal, __fortran, __syscall, __far等基本上算是过时了(obsolote),因此无需考虑。cdecl的函数被改名为_name;stdcall的函数被改名为_name@X;fastcall的函数被改名为@name@X。其中X是函数形参所占用的字节长度(包括那些用寄存器传递的参数, 如fastcall协议)[5]. 例如:
int __cdecl foo(int i); // mangled name is _foo;
int __stdcall bar(int j); // mangled name is _bar@4
int __fastcall qux(int i) ; // mangled name is @qux@4
注意在64位Windows平台上,由于ABI有正式标准可依,只存在一种子程序调用协议,所以该平台上的C语言的函数不在名字前加上下划线 (leading underscore). 这会导致一些老的程序(legacy program), 例如使用’alias’去链接C语言的函数的Fortran程序不能正常工作。

C++语言name mangling概述

C++语言由于含有大量复杂的语言特性,如classes, templates, namespaces, operator overloading等,这使得对象的名字在不同使用上下文中具有不同的意义。所以C++的name mangling非常复杂。
这些名字被mangled的对象,实际上都是具有全局属性的存储对象(storage object),其名字将绑定到所占用内存空间的地址,都有lvalue。存储对象具体可分为数据变量(data variable)与函数(function)。为什么不考虑函数的非静态局部变量、类的非静态数据成员呢?因为编译器把函数的非静态局部变量翻译为[sp]+固定的偏移量;把类的非静态数据成员翻译为this+固定的偏移量。
下文使用了巴科斯-瑙尔范式(BNF)来表述一些name mangling的语法定义。方括号[]表示该项出现0次或1次,除非在方括号后用上下角标给出该项出现的上下限。下文使用类,一般包含了class、struct、union等复合数据类型。
基本结构

数据对象的name mangling

这里所说的数据对象,包括全局变量(global variables)、类的静态数据成员变量(static member variables of classes)。
<数据对象的name mangling> ::= ?@[@]∞0@

用于表示数据对象的类别,其编码为:
编码 含义
0 Private static member
1 Protected static member
2 Public static member
3 global variable
4 static local variable
是数据对象的访问属性的编码表示,一般常用的值有:A表示default对象、B表示const对象、C表示volatile对象、D表示const volatile对象。详见小节:。需要注意的是对于指针、数组、引用类型的对象,是对所指向的基类型的内存空间的访问属性。
例如:
int alpha; // mangled as ?alpha@@3HA 其中3表示全局变量,H表示整形,A表示非const非volatile
char beta[6] = “Hello”; // mangled as ?beta@@3PADA
//其中P表示为指针(或一维数组)且指针自身为非const非volatile,
//D表示char类型,两次出现的A都表示基类型为非const非volatile
class myC{
static int s_v; // mangled as ?s_v@myC@@0HA 其中0表示私有静态数据成员
};

函数的name mangling

函数需要分配内存空间以容纳函数的代码,函数的名字实际上都是lvalue,即指向一块可执行内存空间的起始地址。而函数模板的实例化 (function template instantiation),也是lvalue,需要分配内存空间存储实例化后的代码,其name mangling在模板实例化的名字编码中详述。
<全局函数的name mangling> ::= ?@ [@]∞0 @
[] ∞1
<成员函数的name mangling> ::= ?@ [@]∞0 @ []
[] ∞1

其中,给出了函数是near或far、是否为静态函数、是类成员函数还是全局函数、是否为虚函数、类成员函数的访问级别等基本信息。需要注意,far属性仅适用于Windows 16位环境,32位或64位环境下使用扁平(flat)内存地址模型,函数只能具有near属性。
类成员函数的是指是否为只读成员函数(constant member function). 如果不是const,则编码为A;如果是const,则编码为B;如果是类的静态成员函数,则省略该项,因为静态成员函数没有this指针,无法修改类对象的数据。
是指函数的调用协议。详见调用协议的编码。常见的调用协议的编码为:__cdecl是A, __pascal是C, __thiscall是E, __stdcall是G, __fastcall是I。在Windows 64位环境中唯一允许的调用协议的编码是A。
[]是指函数的返回值的是否有const或volatile属性:
::= ?
如果函数的返回值不具有const或volatile性质,那么该项在编码中被省略;但是如果函数的返回类型是class、struct或union等复合数据类型,此项在编码中是必需的,不能省略。
是函数的返回值的数据类型,详见类型的编码表示。
是函数的形参列表(parameter list)的数据类型的编码。按照形参从左到右顺序给每个参数的数据类型编码,详见类型的编码表示。参数类型列表的编码为:
X (即函数没有参数,或者说参数为void,该编码也是列表的结束标志)
type1 type2 … typeN @ (正常N个形参. 以@作为列表的结束标志)
type1 type2 … Z (形参表最后一项为…,即ellipsis,其编码Z也标志着列表的结束)
是函数抛出异常的说明,即异常规范(exception specification)。截至Visual C++ 2010,仍是接受但没有实现异常规范[6]。因此这一项编码仍然保持为字符Z。
举例:
void Function1 (int a, int * b); /*mangled as ?Function1@@YAXHPAH@Z
其中 Y: 全局函数
A: cdecl调用协议
X: 返回类型为void
H: 第一个形参类型为int
PAH:第二个形参类型为整形指针
@: 形参表结束标志
Z: 缺省的异常规范 */
int Class1::MemberFunction(int a, int * b); /* mangled in 32-bit mode as
?MemberFunction@Class1@@QAEHHPAH@Z
其中 Q: 类的public function
A: 成员函数不是const member function
E: thiscall调用协议
H: 返回值为整形
H: 第一个形参类型为整形
PAH: 第二个形参为整形指针 */

C++语言name mangling细节

名字的编码
C++程序中,需要考虑的具有全局存储属性的变量名字及函数的名字。这些名字受namespace、class等作用域(scope)的限定。因此,带完整限定信息的名字定义为:
::= []∞0 @
::= |
::= | |
| | |
其中,是组成名字标识符的ASCII码串,规定必须以@作为结尾后缀。是指构造函数、析构函数、运算符函数(operator function)、虚表(vtable)等内部数据结构等,详见特殊名字的编码。
与就是指C++程序中的名字空间与类。是指实例化后的函数模板或类模板,详见模板实例化的名字编码。是对一个函数内部用花括号{ … }给出的不同的作用域(scope)的编号表示,详见编号名字空间。是对一个mangled name的ASCII码串中重复出现的类型或名字的简写表示方法,详见重复出现的名字与类型的简写表示。是对静态局部变量所在的函数名的表示方法,详见嵌套的名字。
数的编码
Visual C++的name mangling,有时会用到数(number),例如多维数组的维数等。数的编码使用一套独特的方法:
1-10 编码为 0-9,这节省了最常用的数的编码长度;
大于10的数编码为十六进制,原来的十六进制数字0-F用A-P代替,不使用前缀0或0x,使用后缀@作为结束标志;
0编码为A@;
负数编码为前缀?后跟相应的绝对值(absolute value)的编码。
例如,8编码为7。29110编码为BCD@。-1510编码为?P@。
特殊名字的编码
特殊名字(special names)是指类的构造函数、析构函数、运算符函数、类的内部数据结构等的名字,表示为前缀?后跟编码。已知的编码:
编码字符 不带下划线()的含义 前置下划线()的含义 前置双下划线(__)的含义
0 Constructor operator /=
1 Destructor operator %=
2 operator new operator >>=
3 operator delete operator <<=
4 operator = operator &=
5 operator >> operator |=
6 operator << operator ^=
7 operator ! ‘vftable’
8 operator == ‘vbtable’
9 operator != ‘vcall’
A operator[] ‘typeof’ ‘managed vector constructor iterator’
B operator returntype ‘local static guard’ ‘managed vector destructor iterator’
C operator -> ‘string’(Unknown) ‘eh vector copy constructor iterator’
D operator * ‘vbase destructor’ ‘eh vector vbase copy constructor iterator’
E operator ++ ‘vector deleting destructor’
F operator – ‘default constructor closure’
G operator - ‘scalar deleting destructor’
H operator + ‘vector constructor iterator’
I operator & ‘vector destructor iterator’
J operator ->* ‘vector vbase constructor iterator’
K operator / ‘virtual displacement map’
L operator % ‘eh vector constructor iterator’
M operator < ‘eh vector destructor iterator’
N operator <= ‘eh vector vbase constructor iterator’
O operator > ‘copy constructor closure’
P operator >= ‘udt returning’ (prefix)
Q operator , Unknown
R operator () RTTI-related code (see below)
S operator ~ ‘local vftable’
T operator ^ ‘local vftable constructor closure’
U operator | operator new[]
V operator && operator delete[]
W operator ||
X operator *= ‘placement delete closure’
Y operator += ‘placement delete[] closure’
Z operator -=
虚表的mangled name是::= ??_7[ ]∞0 @6B@,
前缀_P用在?_PX之中. 其含义未知。
下表是RTTI相关的编码,都是在_R后跟一个数字. 有些编码还后跟参数.
编码 含义 尾部参数
_R0 type ‘RTTI Type Descriptor’ Data type type.
_R1 ‘RTTI Base Class Descriptor at (a,b,c,d)’ Four encoded numbers: a, b, c and d.
_R2 ‘RTTI Base Class Array’ None.
_R3 ‘RTTI Class Hierarchy Descriptor’ None.
_R4 ‘RTTI Complete Object Locator’ None.
模板实例化的名字编码
函数模板实例化后,就是一个具体的函数。类模板实例化后,就是一个具体的类数据类型。
<类模板实例化的名字> ::= ? <><><>::=? <函数模板的名字> <模板实参的编码>
<函数模板实例化的名字manging> ::= ?$ <函数模板的名字> <模板实参的编码> <函数的类型信息>

模板的名字以前缀? ? <函数模板的名字> <函数模板实参的编码>可以代替,? <><>(templateargument)(typename)(nontype)(constant)?xanonymoustypetemplateparameterx(templateparameterx) 0a 整数值a
2aba×10bk+1,whereka Da anonymous type template parameter a (‘template-parametera’)
Fab2tuplea,b(unknown) Gabc 3-tuple {a,b,c} (unknown)
Hx(unknown) Ixy (unknown)
Jxyz(unknown) Qa anonymous non-type template parameter a (‘non-type-template-parametera’)
上表中,用a, b, c表示有符号整数,而x, y, z表示无符号整数. 这些有符号整数或无符号整数的编码格式,详见数的编码。上表中,实数值的编码表示$2ab,a、b都是有符号整数,但计算实数的值时,实际上规范到以10为基数的科学计数法的表示形式。
例如:
template class one{
int i;
};

one one1; // mangled as ?one1@@3V? one@H@@A//3V@A@//one? one@H@,其中one@是模板的名字,
//H是模板实参int,其后的@是模板实参表结束标志

class Ce{};
one another; /* mangled as ?another@@3V?$one@VCe@@@@A */
//注意,倒数第1个@表示整个(模板实例)类的结束;
// 倒数第2个@表示模板实参表的结束;
// 倒数第3个@表示类Ce的限定情况的结束(此处限定为空,即Ce的作用域是全局);
// 倒数第4个@表示类名码串的结束
编号名字空间
编号名字空间(numbered namespace)用于指出函数静态局部变量包含在函数的哪个内部作用域中。之所以需要引入编号名字空间,是为了区分函数内部的不同作用域,从而可以区 分包含在不同作用域中但同名的变量,详见下例。其编码格式为前缀?后跟一个无符号数,无符号数的编码参见数的编码小节。
特例情况是, 以?A开始的编号名字空间, 是(‘anonymous namespace’).
例如:
int func()
{
static int i; // mangled as ?i@?1??func@@YAHXZ@4HA 内部表示为func'::2’::i
// ?1表示第2号名字空间;?func@@YAHXZ@是函数的mangled名字;4表示静态局部变量
{
static int i; // mangled as ?i@?2??func@@YAHXZ@4HA 内部表示为func'::3’::i
// ?3表示第3号名字空间
}
return 0;
}
重复出现的名字与类型的简写
在对一个名字做mangling时,用简写方法表示非首次出现的同一个名字或同一个类型。整个简写过程需要对ASCII码串做3次扫描处理:
对函数或函数指针的形参表中的重复出现的参数数据类型的编码简写;
把第一步获得结果中重复出现的名字的编码简写;
把第二步获得的结果中的模板实例化(模板名字+模板实参表)内部重复出现的名字的编码简写。
重复出现的类型的简写

这适用于函数与函数指针的形参列表。只有编码超过一个字符的类型参与简写,包括指针、函数指针、引用、数组、bool、__int64、 class、实例化的模板类、union、struct、enum等数据类型。形参表中前10种多字符编码的类型按照出现次序依次编号为 0,1,…,9。用单个字符编码的类型不参加编号。对不是该数据类型首次出现的形参,用该类型的单个数字的编号代替该数据类型的多个字符的编码来简写 表示。排在前十名之后的多字符编码的数据类型,不再简写。函数的返回值类型不参与此编号及简写。
如果函数的返回类型或者形参是函数指针型,那么函数指针型的形参也参与类型排序编号与简写,但函数指针型的返回值类型不参与类型排序编号与简写。在对类型排序编号时,先编号函数指针型内部的形参的数据类型,再编号函数指针型本身。例如,假如函数的第一个形参是void (__cdecl*)(class alpha, class beta),那么class alpha编号为0,class beta编号为1, 最后整个函数指针编号为2.
例如:
bool ExampleFunction (int*a, int b, int c, int*d, bool e, bool f, bool*g);
// mangled as ?ExampleFunction@@YA_NPAHHH0_N1PA_N@Z
// 其中,_N为返回类型bool,不参与类型简写,不参与编号排序;
// 类型的排序编号:int*为0,bool为1,bool*为2。
// int的编码为单字符H,因此不参与编号
// 第3个形参不简写,仍为H;第四个形参简写为0,第五个形参简写为1 */
//

typedef int* (FP)(int); /* 该函数指针类型编码为 P6APAHPAH@Z */
//

FP funcfp (int , FP) / mangled as ?funcfp@@YAP6APAHPAH@Z0P6APAH0@Z@Z */
// 其中P6A为函数指针类型的编码前缀
// 其中共出现了5次int* (编码为PAH)
// 第1次为funcfp的返回值类型FP的返回值类型,不参与排序编号与简写;
// 第2次为funcfp的返回值类型FP的形参,参与排序编号,编号为0,
// 因为是该类型int*的首次作为形参出现,不简写,仍编码为PAH
// 第3次为funcfp的第一个形参,简写为0;
// 第4次为funcfp的第二个形参的返回值类型,不参与排序编号与简写;
// 第5次为funcfp的第二个形参的参数,简写为0
{return 0;}
重复出现的名字的简写

在对码串完成重复出现的类型的简写后,再对结果中所有不同的名字排序编号,从0编号到9。排在前10个之后的名字不再编号、简写。这里的名字是指函数、class、struct、union、enum、实例化的带实参的模板等等的以@作为结尾后缀的名字。例如,在alpha@?1beta@@(即beta::’2’::alpha)中, 0指代alpha@, 1指代beta@,?1是编号名字空间‘2’的编码.特殊名字、编号名字空间的名字都不参加此轮名字的排序编号与简写。 例如:
class C1{
class C2{};
};

union C2{};

void func(C2,C1::C2) /* mangled as ?func@@YAXTC2@@V1C1@@@Z */
//其中,func的形参表是TC2@@V1C1@@@
//TC2@@是第一个形参union C2;
//V1C1@@是第二个形参C1::C2,其中V表示这是class,
//首个1表示是编号为1的名字(即C2@)重复出现,随后的C1@表示C2的限定域为C1
//随后的@表示限定域的嵌套结束。注意,该字串中编号为“0”的名字是func@
{}
对于实例化模板,模板名字后跟模板实参作为一个整体视作一个名字,参加此轮排序编号与简写。而模板实参表中的参数序列,单独处理它的编号及简写。例如:
template class tc{
public: void __stdcall func(tc){};
};

int main(int argc, char *argv[])
{
tc ins;
ins.func(ins); /* tc::func(tc) mangled as ?func@? tc@H@@QAEXV1@@Z///? tc@H@表示实例化的类模板tc,里面的H表示模板实例化的实参是整型
//Q表示public的成员函数;A表示非只读成员函数;E表示thiscall
//X表示函数返回类型void;
//V1@表示形参为一个class,class的名字为”1”号名字,本例中即tc的编码?$tc@H@;
//注意,本例中”0”号名字是函数名,即func@
//最后一个@表示函数的形参表结束;Z表示缺省的exception specification
return 0;
}
模板实例化时重复出现的实参的简写

模板实例化的名字编码,基本上就是用模板的名字与模板实参作为一个整体,当作或使用。因此模板实例化的名字在参与完成重复出现的类型简写与重复出现的名字简写两步处理之后,再单独处理模板实例化的模板实参表,对其内部重复出现的名字的编号与简化。其方法与重复出现的名字简写的处理相同。
例1:
template

数据类型的编码表示

这里所说的类型,包括数据类型、函数指针的类型、函数模板、类模板等不需要分配内存空间的一些概念属性。类型是数据对象与函数这两类实体的属性。
编码 不带下划线()的含义 前置下划线()的含义
? 用于表示模板
$ 用于表示模板 __w64 (prefix)
0-9 Back reference即用于重复出现的类型或名字的简写
A Type modifier (reference)
B Type modifier (volatile reference)
C signed char
D char __int8
E unsigned char unsigned __int8
F short __int16
G unsigned short unsigned __int16
H int __int32
I unsigned int unsigned __int32
J long __int64
K unsigned long unsigned __int64
L __int128
M float unsigned __int128
N double bool
O long double Array
P Type modifier (pointer)
Q Type modifier (const pointer)
R Type modifier (volatile pointer)
S Type modifier (const volatile pointer)
T union
U struct
V class
W enum wchar_t
X void, Complex Type (coclass) Complex Type (coclass)
Y Complex Type (cointerface) Complex Type (cointerface)
Z … (ellipsis)
对于简单的数据类型,其编码往往就是一个字母。如int类型编码为X。对各种衍生的数据类型(如指针)、复合的数据类型(如类)、函数指针、模板等,在下文中分述。
X表示void 仅当用于表示函数的返回类型、形参表的终止或指针的基类型, 否则该编码表示cointerface. 代码Z (表示ellipsis)仅用于表示不定长度的形参列表(varargs).
指针、引用、数组的类型编码
<指针类型的编码> ::=
<引用类型的编码> ::=
<一维数组类型的编码> ::= <指针类型的编码>
<多维数组类型的编码> ::=

其中作为前缀,用于区分各种情况的指针、引用、数组。指针自身是const还是volatile等访问属性,由确定。共有八种情况:
none const volatile const volatile
Pointer P Q R S
Reference A B
none ?, $$C
表示所指向的基类型(Referred type)是否具有const或volatile等访问属性,详见小节:。
表示指针或引用的基类型(Referred type),或数组的成员类型(element type)。其编码详见数据类型的编码表示。
表示多维数组的基础维度信息,其格式为:Y<数组总的维数-1><第2维的长度>…<最后的第N维的长度>。注意,这里使用的数字,要用Visual C++ name mangling特有的数字编码方法,详见数的编码。 可见,C++语言的数组是对连续存储数据的内存的直接随机访问(random access)的手段;因此一维数组视作指针,数组访问是否越界,完全由编程者负责;而对多维数组,必须知道除了第一维之外其它各维的长度,才能做到直接 随机访问,所以多维数组作为函数形参时,必须已知其除了第一维之外其它各维的长度(以及总的维数),这些信息都被编入了数组的mangled name中。
对于函数指针类型的编码,其为函数调用接口信息,包括使用的调用协议、返回值类型、形参类型、允许抛出的异常等,详见函数指针类型的编码。类成员指针的类型编码,详见类成员指针的类型编码。类成员函数指针的类型编码,详见类成员函数指针的类型编码。
2003年x86-64位处理器问世后,第一批64位Windows平台的C++编译器曾经使用_O作为数组类型的前缀修饰符(type modifier). 但不久就改回了32位Windows平台C++编译器使用的P前缀.
需注意的是,全局数组类型被编码为P(指针型),同时作为函数形参的数组类型被编码为Q(常量指针). 这与其本来含义恰恰相反——全局数组型的变量名字表示某块内存地址,该名字不能再改为指向其它内存地址;而作为函数形参的数组型变量的名字所表示的内存地 址是可以修改的。但数组类型这种编码方法已经被各种C++编译器广泛接受。显然,这是为了与老的代码保持向后兼容。例如:
int ia[10]; //ia是数组类型的非函数形参的变量
//
int main(int argc, char argv[]) //argv是数组类型的形参, 其类型为 (char )[]
{
int j=*(ia++); //编译错误!ia是只读的lvalue,不能完成地址的++操作
char c= (argv++); //编译正确!argv是可以修改的lvalue
return 0;
}
例如:
typedef int * p1; // coded as PAH 其中P表示default访问属性的指针,
//A表示对基类型的default访问属性,H表示基类型为int
typedef const int * p2; //coded as PBH 其中B表示基类型的const访问属性
typedef volatile int *p3; //coded as PCH 其中C表示基类型的volatile访问属性
typedef volatile const int *p4; //coded as PDH 其中D表示基类型的const volatile访问属性
typedef int * const p5; //coded as QAH 其中Q表示是const pointer
typedef int * volatile p6; //coded as RAH 其中R表示是volatile pointer
typedef int * const volatile p7; //coded as SAH 其中S表示是const volatile pointer
typedef volatile int * const p6; //coded as QCH 例如这是一个外部IO设备输入数据的内存地址
typedef int &r1; // coded as AAH 其中第一个A表示引用类型(reference type)
typedef int[8] a1; // global array coded as PAH
typedef int[10][8] a2; //global array coded as PAY07H 其中Y标志多维数组,0是(维数-1)即1的编码
// 7是第二维长度8的编码
typedef int[4][16][5] a3; //global array coded as PAY1BA@4H 其中1为(维数-1)即2的编码
//BA@是第二维长度16的编码(16进制的10),4是第3维长度5的编码
int[7][6] // 作为函数形参时,该数据类型编码为 QAY05H
函数指针类型的编码
函数指针的类型信息,包括函数返回类型,函数形参类型,调用协议等。以前缀P6开始。各项具体定义可参见函数的类型信息编码:
::=
[][]∞1

一般地,取值为P,意为指针;取值为6,意为指针的基类型为near属性的非成员函数。
例如:
typedef const int (__stdcall FP) (int i); / coded as P6G?BHH@Z */
// 其中?B表示为const
类成员指针的类型编码
指向类成员的指针,其编码为
=

其中各项的定义详见指针、引用、数组的类型编码。注意,是指基类型的属性,常用的值为:Q for default, R for const; S for volatile; T for const volatile。
例如:
class C1{
int i;
};

typedef int C1::*p; // coded as PQC1@@H
类成员指针变量的mangled name
类成员指针(pointer to member)的名字mangling的最末尾处对基类型访问属性的编码不同于普通的指针,要在最后加上所指向类的带完整限定信息的名字:
::= ?

有的文献称[7],类成员指针、类成员函数指针的name mangling都必须以Q1@作为结尾,以替代。从下述几例可以看出,这种说法是错误的。Q是对所指向的成员类型使用default访问属性,这是最常见的情况。1是该指针所指向类的名字简写,因为在此位置之前该类的名字必然已经出现在该数据类型的编码中,所以此处名字的简写是必然的。但不一定总是简写作1
下例中,成员指针变量的mangled name以S12@结尾:
class outer{
public:
class cde{
public: volatile int i;
};
};

volatile int outer::cde::* p; // mangled as ?p@@3PScde@outer@@HS12@
// 其中两个S都是表示基类型为volatile属性
// 1是cde@的简写
// 2是outer@的简写
例2:
class C1{
public: int i;
};

C1 const *pi; // ?pi@@3PBVC1@@B 一个简单的指针变量。作为对比

typedef int C1::* TP; // coded as PQC1@@H

void func(TP){
static TP ppp=0; // func'::2’::ppp mangled as ?ppp@?1??func@@YAXPQC1@@H@Z@4PQ2@HQ2@
//其中,?ppp@?1??func@@YAXPQC1@@H@Z@表示带作用域限定信息的名字func'::2’::ppp
//4表示静态局部变量;PQ2@H表示成员指针类型,其中的“2”是C1@的简写。
//注意ppp@编号为0,func@编号为1
//最后三个字符Q2@表示对基类型“2”(C1@的简写)的访问属性为Q(缺省属性,即非const非volatile)
}
类成员函数指针的类型编码
类成员函数的指针(pointer to member function),遵从指针类型编码的一般规则。但与函数指针类型的编码相比,多了一项,表示所指的函数是否为只读成员函数(constant member function)。
::=

[] []
[]∞1

一般地,取值为P,意为指针;取值为8,意为指针的基类型为near属性的类成员函数。其它各项取值参见函数的name mangling。
例如:
class C1{
public: void foo(int) const
{};
};

typedef void (C1::TP)(int) const; / coded as P8C1@@BEXH@Z
其中B表示const member function; E表示thiscall */
类成员函数的指针变量的mangled name
类成员函数指针(pointer to member function)的名字mangling,对基类型访问属性的编码不同于普通的指针,要在最后加上所指向类的带完整限定信息的名字。
::= ?

上述定义中,取值一般是Q
例如:
class xyz{
public: void foo(int) {};
};

void (xyz::pfunc)(int) ; / mangled as ?pfunc@@3P8xyz@@AEXH@ZQ1@ */
// 其中Q表示对基类型的访问属性为default;1表示被简写的编号为‘1’的名字,即‘xyz@’;
// 注意,编号为‘0’的名字是‘pfunc@’
复合类型(union, struct, class, coclass, cointerface)的编码
<复合类型的编码> ::= <复合类型的种类><复合类型的带限定的名字>
其中复合类型的种类作为前缀,union编码为T, struct编码为U, class编码为V, coclass编码为X, cointerface编码为Y。复合类型的带限定的名字,是指按照名字所在的名字空间、所属的类,逐级列出限定情况(qualifier),详见名字的编码。
经常可以看到复合类型的编码以@@两个字符结尾,这是因为第一个@表示复合类型名字的字串结束,第二个@表示限定情况的结束(即作用域为全局,限定情况为空)。
编写代码时,经常要用到类的前向声明(forward declaration),即提前声明这个名字是个类,但类的成员尚未给出。例如:
class myClassName; //mangled type name is VmyClassName@@
class myClassName::embedClassName; //mangled type name is VembedClassName@VmyClassName@@
枚举类型(enum)的编码
<枚举类型的编码> ::= W <枚举实际使用的数据类型>
<枚举成员的编码> ::= W <枚举实际使用的数据类型> @

其中,W为枚举类型前缀词。为枚举类型的带限定的名字,是指按照名字所在的名字空间、所属的类,逐级列出限定情况(qualifier),详见名字的编码。枚举实际使用的数据类型, 编码如下:
编码 对应的实际数据类型
0 char
1 unsigned char
2 short
3 unsigned short
4 int (generally normal “enum”)
5 unsigned int
6 long
7 unsigned long
例如:
enum namex:unsigned char {Sunday, Monday}; // enum-type coded as W4namex@@
看起来Visual C++已经把所有枚举类型用int型实现,因此枚举的基类型(The underlying type of the enumeration identifiers)的编码总是为4

用于普通的数据对象,表示其是否具有const、volatile等访问属性;用于指针、数组、引用类型,则表明对基类型的访问属性,而指针自身是否为const、volatile等属性,则专由编码表示。
用于函数指针时,表示该指针所指向的基类型是函数。但与指向数据对象的普通指针不同——函数指针指向的基类型(即函数)也有自己的内存空间,只是这块内存空间必定是只读的、可执行的,因此函数指针所指向的基类型内存空间不存在const、volatile等访问属性。
的取值情况:
Variable Function
none const volatile const volatile
none A B, J C, G, K D, H, L 6, 7
__based() M N O P _A, _B
Member Q, U, Y R, V, Z S, W, 0 T, X, 1 8, 9
__based() Member 2 3 4 5 _C, _D
可以有0个或多个前缀:
Prefix Meaning
E type __ptr64
F __unaligned type
I type __restrict
__based()属性的变量

指针变量的__based()属性是Microsoft的C++语言扩展. 这一属性编码为:
0 (意味着__based(void))
2 (意味着__based())
5 (意味着没有__based())
例如:
int *pBased; // mangled name: ?pBased@@3PAHA
int __based(pBased) * pBasedPtr; // 需要注意Visual C++编译器把这个指针变量的声明解释为:
// (int __based(pBased) * __based(pBased) pBasedPtr)
// 因此其mangled name: ?pBasedPtr@@3PM2pBased@@HM21@
// 其中PM2pBased@@表示这是基于<::pBased>的指针;HM21表示是基于“1”的整型指针,
// “1”是重复出现的名字的编号简写,这里就是指pBased@
//
int __based(void) *pbc; // mangled name: ?pbc@@3PM0HM0 其中的0表示这是__based(void).
// 编译器把该变量声明解释为(int __based(void) * __based(void) pbc)

函数的类型信息编码

函数的类型信息,是指调用函数时必须考虑的ABI(Application Binary Interface),包括调用协议、返回类型、函数形参表、函数抛出异常的说明(exception specification)等,参见函数的name mangling。

给出了函数是near或far(但far属性仅适用于Windows 16位环境,32位或64位环境下只能函数具有near属性)、是否为静态函数、是否为虚函数、类成员函数的访问级别等信息:
near far static near static far virtual near virtual far thunk near thunk far
private: A B C D E F G H
protected: I J K L M N O P
public: Q R S T U V W X
not member Y Z
上表中的thunk函数[8],是指在多继承时,由编译器生成的包装函数(warpper function),用于多态调用实际已被子类对应函数覆盖(overrided)的父类虚函数,并把指向父类的this指针调整到指向子类的起始地址。
调用协议的编码

Code    Exported?   Calling Convention
A   No  __cdecl
B   Yes __cdecl
C   No  __pascal __fortran
D   Yes __pascal
E   No  __thiscall
F   Yes __thiscall
G   No  __stdcall
H   Yes __stdcall
I   No  __fastcall
J   Yes __fastcall
K   No  none
L   Yes none
M   No  __clrcall

64位编程时,唯一可用的调用协议的编码是A
查看Visual C++的函数的修饰后的名字
有多种方法,可以方便地查看一个函数在编译后的修饰名字[9]:
直接用工具软件(如微软开发环境提供的dumpbin)查看obj、exe等二进制文件。使用dumplib查看.obj或.lib文件时,使用”/SYMBOLS”命令行选项。[10]
编译时使用”/FA[c|s|u]”编译选项,生成带有丰富注释信息的汇编源程序,其文件扩展名是.cod或者.asm,可以查看每个C/C++函数的修饰名字[11]。
在源程序中使用微软提供的预定义宏(Microsoft-Specific Predefined Macros)—— __FUNCDNAME__,例如:

  void exampleFunction()
  {
      printf("Function name: %s\n", __FUNCTION__);
      printf("Decorated function name: %s\n", __FUNCDNAME__);
      printf("Function signature: %s\n", __FUNCSIG__);

      // 输出为:  
      // -------------------------------------------------
     // Function name: exampleFunction
     // Decorated function name: ?exampleFunction@@YAXXZ
     // Function signature: void __cdecl exampleFunction(void)

  }

由修饰名字反查其未修饰时的原名
使用微软Visual C++中的解析修饰名字的工具软件undname.exe。例如:
C:>undname.exe ??$name9@V0class1@@@@YAXVname9@class1@@@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- “??$name9@V0class1@@@@YAXVname9@class1@@@Z”
is :- “void __cdecl name9(class class1::name9)”
使用Windows提供的系统调用UnDecorateSymbolName()[12]把 修饰名字翻译为未修饰名字。UnDecorateSymbolName在DbgHelp.h或imagehlp.h中声明,在DbgHelp.dll中实 现,需要使用导入库DbgHelp.lib。Windows SDK中包含了DbgHelp.h与DbgHelp.lib。示例程序:

//UnDecorate.cpp
#include <windows.h> //如果不包含此头文件,编译DbgHelp.h时会产生大量语法错误
#include <DbgHelp.h>
#include <tchar.h>
#include <iostream>
#pragma comment(lib,"dbghelp.lib") //告诉链接器使用这个输入库

int _tmain(int argc, _TCHAR* argv[])  
{  
        TCHAR szUndecorateName[256];  
        memset(szUndecorateName,0,256);  
        if (2==argc)  
        {  
                ::UnDecorateSymbolName(argv[1],szUndecorateName,256,0);  
                std::cout<<szUndecorateName<<std::endl;  
        }  
        return 0;  
}

编译后,执行上述程序:
C:>UnDecorate.exe ?apiname@@YA_NEEPAD@Z

bool __cdecl apiname(unsigned char,unsigned char,char *)

C++修饰名字的用途

DLL输出的C++函数
在Windows平台上,使用dllexport关键字直接输出C++函数时,DLL的用户看到的是修饰后的函数名字. 如果不希望使用复杂的C++修饰后的函数名,替代办法是在DLL的.def文件中定义输出函数的别名,或者把函数声明为extern “C”.

在汇编源程序或者内联汇编中引用C/C++函数

在汇编源程序或者内联汇编中引用了C/C++函数,就必须引用该函数的修饰名字。

VC环境中C++ 语言中的 Name Mangling
在 C++ 语言中,函数需要支持重载,新增命名空间函数调用、类函数调用、运算符重载、模板函数等等,所以情况也比 C 语言复杂很多。
下面我们列举一些函数例子进行分析,函数可以有如下声明方式(其中 __CALLTYPE 可以为 __cdecl、__stdcall、__fastcall等):
1、void __CALLTYPE fun();
2、int __CALLTYPE fun();
3、int __CALLTYPE fun(int);
4、double __CALLTYPE fun(int, double);
5、int* __CALLTYPE fun(int*, char*);
6、class ABCD
{
public:
int __CALLTYPE fun();
};

7、template

int fun(typename T);
我们仍先假设此处 CALLTYPE 为 __cdecl(即:#define __CALLTYPE __cdecl),在 VC 中,Name Mangling 结果如下:

1、?fun@@YAXXZ
2、?fun@@YAHXZ
3、?fun@@YAHH@Z
4、?fun@@YANHN@Z
5、?fun@@YAPAHPAHPAD@Z
6、?fun@ABCD@@QAAHXZ
7、??$fun@H@@YAHH@Z

由此可见,C++ 的 Name Mangling 技术比 C 语言的复杂很多。
我们挑选第一条分析一下,“?”表示一个函数的开始,用以区别于 C 语言的“_”,fun 为函数名称,“@@YA”表示函数调用约定为 __cdecl,“X”表示函数的参数为空,“XZ”为结束标识。
将上述名称还原为可读方式并不复杂,但要记住这些规则,考虑到所有组合方式却是一件比较复杂的事情,下面我们来看看一个比较复杂的函数调用,声明函数如下:
int fun(const CString&, const std::vector&);
Name Mangling 后的结果为:
?fun@@YAHABV? CStringT@WV? StrTraitATL@_WV? ChTraitsCRT@W@ATL@@@ATL@@@ATL@@ABV? vector@NV?$allocator@N@std@@@std@@@Z
如此长的一串,用人脑来直接分析显然不符合实际,好在 Windows 提供了 API 函数用于解析字符串,具体解析办法,下面一节将详细解释。

将Name Mangling 后的名称还原为可读的形式

在 Windows 的DbgHelp.dll 导出函数中,UnDecorateSymbolName 是用于解析 Name Mangling 字符串的,具体函数的细节可以查看 MSDN。如下为实例代码:
void UnDecorateName()
{
char szDecorateName[1024] = {0};
char szUnDecorateName[2048] = {0};
printf(“Please Input Decorated Name: “);
scanf(“%s”, szDecorateName);

if (UnDecorateSymbolName(szDecorateName, szUnDecorateName, sizeof(szUnDecorateName), UNDNAME_COMPLETE) == 0)
{
printf(“UnDecorateSymbolName Failed. GetLastError() = %d”, GetLastError());
getchar();
return;
}

printf("The UnDecorated Name Is: %s/r/n", szUnDecorateName);
getchar();
return;

}

在 Xp 中当我们输入如上的:?fun@@YAPAHPAHPAD@Z
程序得出的结果为:int * __cdecl fun(int ,char )
注意:在 Xp 中,带有模板的 Name Mangling 字符串无法直接还原,如需还原,可以在 Vista、Win7 中运行此程序。


  1. c++的一个重要的特性就是重载,一个类可以拥有多个同名函数。那么编译器是如何来区别重载函数的那?答案就是Name Mangling(或者叫做Name Decorate).
  2. Name Mangling就一个对函数进行哈希的算法,所以有的中文翻译为:函数签名。一个VC++函数经过函数签名后可能是这个结果:?Test2@@YAHXZ 。
  3. Name Mangling不是C++特有的,只要是支持重载的高级语言就会有。对于VC++来说,有一个WIN32 API可以解析这些经过编码后的函数签名。
  4. 这篇文章是研究如何Mangling的:VC++ Name Mangling
#include <Windows.h>
#include <Dbghelp.h>

#pragma comment(lib, "dbghelp.lib")

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,int nCmdShow)
{
    wchar_t * lpdecorate_name = L"?Test2@@YAHXZ";
    wchar_t readable[MAX_PATH]= {0};
    //
    if (::UnDecorateSymbolNameW(lpdecorate_name, readable,
        MAX_PATH, UNDNAME_COMPLETE))
    {
        ::MessageBox(NULL, readable, L"Readable Name", MB_OK);
    }
    return 0;
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LNK2000错误是链接错误,表示编译器在链接阶段无法解析某个外部符号的定义。 LNK2000错误通常有以下几种可能的原因: 1. 缺少定义:编译器找不到某个符号的定义。这可能是因为你在代码中使用了某个函数、变量或类,但没有提供相应的定义。请检查你的代码,确保所有使用的符号都有正确的定义。 2. 定义重复:编译器找到了多个相同符号的定义。这可能是因为你在多个源文件中重复定义了同一个符号。请确认你的代码中没有重复定义的情况。 3. 链接错误:编译器无法找到符号的定义,可能是因为你未正确链接相关的库文件。请检查你的项目配置,确保正确地链接了需要的库文件。 4. 符号修饰问题:在一些情况下,C++编译器会对函数名进行修饰(name mangling),导致符号名称与预期不符。如果你在代码中使用了某个外部库的函数,可能需要使用 extern "C" 来告知编译器使用 C 链接规约来解析函数名。 对于LNK2000错误,你可以尝试以下几个步骤来解决问题: 1. 检查代码中是否缺少定义或重复定义了某个符号。 2. 确认项目配置是否正确链接了需要的库文件。 3. 如果使用了外部库函数,请检查是否需要使用 extern "C" 来修饰函数名。 4. 检查代码是否存在循环引用或者定义顺序不正确的情况。 如果你可以提供更多的错误信息和相关代码,我可以给出更具体的建议。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值