动态库函数名称
- 名称修饰(Name Mangling)
- 符号导出(Symbol Exporting)
- 调用约定(Calling Convention)
名称修饰(Name Mangling)
动态库中的函数名称修饰(Name Mangling)是编译器为了解决函数重载、命名空间和类成员函数等问题而对函数名进行的一种转换。这种转换使得每个符号在链接时具有唯一性,但也会带来一些跨语言调用的复杂性。下面详细介绍C++中的名称修饰机制以及如何处理这些问题。
C语言
无名称修饰:C语言不进行名称修饰,函数名直接映射到其对应的符号。
示例:
void foo(int a) {
// 函数实现
}
编译后,foo 函数的符号通常是 _foo 或 foo,具体取决于编译器。
C++语言
名称修饰:C++编译器会对函数名进行修饰(mangling),以支持函数重载、命名空间和类成员函数等功能。
示例:
void foo(int a) {
// 函数实现
}
void foo(double b) {
// 函数实现
}
编译后,这两个同名但参数类型不同的函数会被修饰为类似 _Z3fooi 和 _Z3food 的形式(实际格式可能因编译器不同而有所差异),以便在链接时能够正确识别和区分。
名称修饰的细节
不同的编译器有不同的名称修饰规则。以下是一些常见编译器的名称修饰示例:
GCC/Clang
- 函数 void foo(int) 可能被修饰为 _Z3fooi。
- 函数 void foo(double) 可能被修饰为 _Z3food。
- 类成员函数 class MyClass { void foo(int); }; 可能被修饰为 _ZN7MyClass3fooEi。
MSVC
- 函数 void foo(int) 可能被修饰为 ?foo@@YAXH@Z。
- 函数 void foo(double) 可能被修饰为 ?foo@@YAXN@Z。
- 类成员函数 class MyClass { void foo(int); }; 可能被修饰为 ?foo@MyClass@@QAEXH@Z。
extern "C" 避免名称修饰
为了避免名称修饰带来的问题,特别是在需要跨语言调用或导出函数时,可以使用 extern "C" 链接规范来声明函数。这告诉编译器不要对这些函数名进行修饰,从而保持与C语言兼容的链接方式。
使用 extern "C"
在C++代码中声明C语言风格的函数
extern "C" {
void foo(int a); // 声明一个C语言风格的函数
}
// 定义一个C语言风格的函数
extern "C" void foo(int a) {
// 函数实现
}
在头文件中声明
为了确保在C和C++代码中都能正确使用该函数,通常会在头文件中使用条件编译:
#ifndef EXAMPLE_H
#define EXAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
void foo(int a);
#ifdef __cplusplus
}
#endif
#endif // EXAMPLE_H
注意事项
- 函数重载:extern "C" 禁用了名称修饰,因此不能用于重载函数。
- C++ 特性:extern "C" 函数不能使用 C++ 特性(如类、模板、命名空间等) 。
- 变量:extern "C" 也可以用于变量,但通常不推荐。
动态库函数名称的查找
可以使用以下工具查看动态库中的函数名称:
工具 1:nm
nm -D libmylib.so
工具 2:objdump
objdump -T libmylib.so
工具 3:readelf
readelf -s libmylib.so
工具 4:c++filt(解析 C++ 修饰名称)
nm -D libmylib.so | c++filt
符号导出(Symbol Exporting)
动态库符号导出(Symbol Exporting)是指在编译和链接过程中,明确指定哪些函数、变量或类可以从动态库中被外部程序使用。不同的操作系统和编译器有不同的机制来实现这一点。
Windows (PE格式)
在Windows平台上,通常使用 __declspec(dllexport) 和 __declspec(dllimport) 来控制符号的导出和导入。
头文件 (example.h)
为了同时支持导出和导入符号,通常会使用条件编译:
#ifndef EXAMPLE_H
#define EXAMPLE_H
#ifdef EXAMPLE_EXPORTS
#define EXAMPLE_API __declspec(dllexport)
#else
#define EXAMPLE_API __declspec(dllimport)
#endif
// 声明一个C语言风格的函数
extern "C" {
EXAMPLE_API void foo(int a);
}
#endif // EXAMPLE_H
源文件 (example.cpp)
在定义符号时,确保定义了 EXAMPLE_EXPORTS 宏,以便正确导出符号:
#include "example.h"
#include <stdio.h>
extern "C" {
EXAMPLE_API void foo(int a) {
printf("foo: %d\n", a);
}
}
编译命令
在编译生成动态库时,需要定义 EXAMPLE_EXPORTS 宏:
cl /LD example.cpp /Feexample.dll /D EXAMPLE_EXPORTS
这将生成 example.dll 动态库和 example.lib 导入库。
Linux/Unix (ELF格式)
在Linux和Unix系统上,通常使用 attribute((visibility("default"))) 来显式导出符号,并结合 -fvisibility=hidden 编译选项来隐藏默认符号。
头文件 (example.h)
同样,为了支持跨语言调用,可以使用 extern "C" 来避免名称修饰:
#ifndef EXAMPLE_H
#define EXAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
__attribute__((visibility("default"))) void foo(int a);
#ifdef __cplusplus
}
#endif
#endif // EXAMPLE_H
源文件 (example.cpp)
在定义符号时,使用 attribute((visibility("default"))) 来导出符号:
#include "example.h"
#include <iostream>
extern "C" {
__attribute__((visibility("default"))) void foo(int a) {
std::cout << "foo: " << a << std::endl;
}
}
编译命令
在编译生成共享库时,使用 -fvisibility=hidden 选项来隐藏默认符号,并显式导出所需的符号:
g++ -shared -o libexample.so -fPIC -fvisibility=hidden example.cpp
符号版本控制:在某些情况下,你可能希望对符号进行版本控制。在Linux上,可以通过 .symver 指令来实现:
__asm__(".symver original_foo, foo@VERSION_1");
调用约定(Calling Convention)
调用约定(Calling Convention)定义了函数调用过程中的一些规则,包括参数如何传递、堆栈由谁清理、寄存器的使用等。不同的编程语言和平台可能有不同的调用约定。在动态库开发中,选择合适的调用约定非常重要,尤其是在跨语言调用时。
1. __cdecl
描述:这是C和C++语言中的默认调用约定。
特点:
调用者负责清理堆栈。
支持可变参数函数(如 printf)。
适用场景:适用于大多数情况,尤其是当你需要支持可变参数函数时。
2. __stdcall
描述:这是Windows API函数使用的标准调用约定。
特点:
被调用者负责清理堆栈。
不支持可变参数函数。
适用场景:适用于已知参数数量的函数,尤其适合Windows API函数。
3. __fastcall
描述:尽可能使用寄存器传递参数,以提高性能。
特点:
前几个参数通过寄存器传递(通常是ECX和EDX),其余参数通过堆栈传递。
被调用者负责清理堆栈。
适用场景:适用于对性能要求较高的函数,但需要注意寄存器使用限制。
4. __thiscall
描述:这是C++类成员函数的默认调用约定。
特点:
第一个参数(即this指针)通过ECX寄存器传递,其余参数通过堆栈传递。
被调用者负责清理堆栈。
适用场景:仅用于C++类成员函数。
5. __vectorcall
描述:一种高效的调用约定,主要用于支持SIMD指令集的函数。
特点:
使用更多的寄存器来传递参数,从而减少堆栈操作。
支持浮点数和SIMD数据类型的高效传递。
适用场景:适用于需要高效处理SIMD数据类型的函数。
区别
__stdcall、__cdecl和__fastcall是三种函数调用协议,函数调用协议会影响函数参数的入栈方式、栈内数据的清除方式、编译器函数名的修饰规则等。
-
调用协议常用场合
-
__stdcall:Windows API默认的函数调用协议。
-
__cdecl:C/C++默认的函数调用协议。
-
__fastcall:适用于对性能要求较高的场合。
-
-
函数参数入栈方式
-
__stdcall:函数参数由右向左入栈。
-
__cdecl:函数参数由右向左入栈。
-
__fastcall:从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈。
-
问题一:__fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。
-
-
栈内数据清除方式
-
__stdcall:函数调用结束后由被调用函数清除栈内数据。
-
__cdecl:函数调用结束后由函数调用者清除栈内数据。
-
__fastcall:函数调用结束后由被调用函数清除栈内数据。
-
问题一:不同编译器设定的栈结构不尽相同,跨开发平台时由函数调用者清除栈内数据不可行。
-
问题二:某些函数的参数是可变的,如printf函数,这样的函数只能由函数调用者清除栈内数据。
-
问题三:由调用者清除栈内数据时,每次调用都包含清除栈内数据的代码,故可执行文件较大。
-
-
C语言编译器函数名称修饰规则
-
__stdcall:编译后,函数名被修饰为“_functionname@number”。
-
__cdecl:编译后,函数名被修饰为“_functionname”。
-
__fastcall:编译后,函数名给修饰为“@functionname@nmuber”。
-
注:“functionname”为函数名,“number”为参数字节数。
-
注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。
-
-
C++语言编译器函数名称修饰规则
-
__stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。
-
__cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。
-
__fastcall:编译后,函数名被修饰为“?functionname@@YI******@Z”。
-
注:“******”为函数返回值类型和参数类型表。
-
注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。
-
C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。
-