动态库函数名称

75 阅读8分钟

动态库函数名称

  • 名称修饰(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是三种函数调用协议,函数调用协议会影响函数参数的入栈方式、栈内数据的清除方式、编译器函数名的修饰规则等。

  1. 调用协议常用场合

    1. __stdcall:Windows API默认的函数调用协议。

    2. __cdecl:C/C++默认的函数调用协议。

    3. __fastcall:适用于对性能要求较高的场合。

  2. 函数参数入栈方式

    1. __stdcall:函数参数由右向左入栈。

    2. __cdecl:函数参数由右向左入栈。

    3. __fastcall:从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈。

    4. 问题一:__fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。

  3. 栈内数据清除方式

    1. __stdcall:函数调用结束后由被调用函数清除栈内数据。

    2. __cdecl:函数调用结束后由函数调用者清除栈内数据。

    3. __fastcall:函数调用结束后由被调用函数清除栈内数据。

    4. 问题一:不同编译器设定的栈结构不尽相同,跨开发平台时由函数调用者清除栈内数据不可行。

    5. 问题二:某些函数的参数是可变的,如printf函数,这样的函数只能由函数调用者清除栈内数据。

    6. 问题三:由调用者清除栈内数据时,每次调用都包含清除栈内数据的代码,故可执行文件较大。

  4. C语言编译器函数名称修饰规则

    1. __stdcall:编译后,函数名被修饰为“_functionname@number”。

    2. __cdecl:编译后,函数名被修饰为“_functionname”。

    3. __fastcall:编译后,函数名给修饰为“@functionname@nmuber”。

    4. 注:“functionname”为函数名,“number”为参数字节数。

    5. 注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

  5. C++语言编译器函数名称修饰规则

    1. __stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。

    2. __cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。

    3. __fastcall:编译后,函数名被修饰为“?functionname@@YI******@Z”。

    4. 注:“******”为函数返回值类型和参数类型表。

    5. 注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

    6. C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。