2024-02-19 | 研究与探索 | UNLOCK

MSVC 动态链接符号导出

本文的知乎链接:https://zhuanlan.zhihu.com/p/622426221

似乎很难找到资料把这个问题讲清楚。

基本知识

动态链接库可以用一些名字(就是 字符串)把自己的 变量 和 函数 导出供外部使用,实现细节是 导出表。除了通过名字外,也可以通过 序数(ordinal)来导出,导出的 对象 总有一个序数,但可以没有名字,这也是系统 dll 隐藏内部 API 的办法之一。如果没有手动指定, link.exe 会自动分配序数。

用户程序手动加载动态链接库一般用 LoadLibrary 来加载,GetProcAdress 来获得导出的 对象地址,GetProcAdress 的第二个参数可以传 名字字符串 或者 序数(序数需要强制成相应类型)。

link.exe 在生成 .dll 时也可以生成一个配套的 .lib 导入库,里面有相应的符号用来供链接器链接。动态库中对象的地址在运行时才能确定,所以 .lib 导入库 里的符号实际上是指针,符号名 也不直接是相应的对象名经过 修饰(mangle)的结果而是要加上 __imp_ 前缀(下面简称 imp 符号/imp 指针),运行时这个指针会被设定为相应导出对象的地址。

同时为了方便使用导出函数,.lib 导入库 里也有与修饰后 函数名 一致的符号,但其代表的地址的内容只有

1
jmp [__imp_xxx]

跳转到 imp 指针指向的地址。导出的变量则没有这种机制,必须在编译时生成正确的 imp 符号,也就是如下一节所述使用 __declspec(dllimport) 修饰符。

符号生成

通过以下方法可以在生成的 .lib 导入库中自动生成 imp 符号:

  • 源代码中使用 __declspec(dllexport),此时,imp 符号在生成的 .obj 文件中既已存在
  • 源代码中使用 #pragma comment(linker, "/export:xxx"),此时,imp 符号在生成的 .obj 文件中既已存在
  • 链接时 link.exe 参数中通过 /exports 指定导出名
  • 链接时 link.exe 参数中通过 /def 指定 def 文件,在 def 文件中指定导出符号

其中 def 文件最灵活,支持符号重命名,设置序数,不导出名称,名称重定向。

__declspec(dllimport) 的作用是,编译时直接使用对应的 imp 符号 间接使用相应对象。例如以下 C++ 代码

1
extern "C" void __declspec(dllimport) __stdcall test_func(int x);

当调用 test_func() 时生成的汇编代码是 call [__imp__test_func@4] 。而去掉 __declspec(dllimport) 后生成的汇编代码就是常规的 call _test_func@4。两种代码链接 .lib 导出库都能编译,但使用了 __declspec(dllimport) 产生的代码少一层间接跳转,效率稍微高一些。

MinGW 能不用 .lib 导出库,直接链接 .dll 动态库,但这需要 .dll 动态库中存在相应的导出名称,且只能为普通调用自动生成 imp 符号,不能自动生成调用 __declspec(dllimport) 修饰的函数产生的 imp 符号。

其它语言

C# 支持在源代码中指定 dll 名,导出名或序数:

1
2
[DllImport("test.dll", EntryPoint="#123")]
static extern int test_fun(int i);

Rust 也有类似机制:

1
2
3
4
5
6
7
#[link(name = "exporter", kind = "raw-dylib")]
extern "stdcall" {
#[link_name = "actual_symbol_name"]
fn name_in_rust();
#[link_ordinal(15)]
fn imported_function_stdcall(i: i32);
}

都比 C/C++ 的方式要好用……不知道 C++20 加上 module 之后有没有更好用的方式。

参考链接