如何静态地 link 到由序号导出的 DLL 函数?
How to statically link to a DLL function that is exported by an ordinal?
比如说,如果一个 DLL 有一个函数:
int TestFunc01(int v)
{
WCHAR buff[256];
::StringCchPrintf(buff, _countof(buff), L"You passed a value of %d", v);
return ::MessageBox(NULL, buff, L"Test Dll Message", MB_OK | MB_ICONINFORMATION);
}
仅按序数值导出(以下为.def
文件):
LIBRARY DllName
EXPORTS
TestFunc01 @1 NONAME
所以现在当我想从另一个模块静态 link 到该函数时,如果该函数是按名称导出的,我会执行以下操作:
extern "C" __declspec(dllimport) int TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
但是我如何仅通过其序数值静态地link它?
PS。我正在使用 Visual Studio C++ 编译器 & linker.
您可以使用 lib.exe 工具从您编写的 .DEF 文件创建 .lib 文件,您不必实际提供任何目标文件。
您可以编写一个 .def 来匹配您要导出的序数,但还要提供名称,然后使用 lib.exe 创建一个 .lib。
在代码中,您可以将其声明为:
extern "C" __declspec(dllimport) ret_type funcName(arg_type);
重要的是调用约定与函数实际使用的任何内容相匹配,但名称必须与您创建的 .lib 使用的内容相匹配,即使该名称不遵循该调用类型的装饰法则。
But how do I statically link to it only by its ordinal value?
完全相同,当函数按名称导出时 - 没有任何区别。在这两种情况下你都需要两件事——正确的函数声明:
extern "C" __declspec(dllimport) int /*calling convention*/ TestFunc01(int v);
和 lib 文件,包含在链接器输入中。
当您将 somename.def 文件包含到 visual studio 项目时,它会自动向链接器添加 /def:"somename.def"
选项(否则您需要手动添加此选项选项)。生成的 lib 文件将包含 __imp_*TestFunc01*
符号 - 在 * 位置将根据 c 或 c++ 符号和调用约定进行不同的修饰,如果 x86。
从另一方面来说,当你调用函数时,带有 __declspec(dllimport)
属性。编译器 (CL) 将生成 call [__imp_*TestFunc01*]
- 所以引用 __imp_*TestFunc01*
符号(再次 * 在实际装饰中)。链接器将搜索 __imp_*TestFunc01*
符号并在 lib 文件中找到它。
NONAME
选项对于这个过程无关紧要 - 这只会影响它的形成方式 IAT/INT 此函数在 PE(按名称或序号导入)
请注意,如果我们仅通过 link.exe /lib /def:somename.def
将生成的 lib 文件与 def 文件分开 - 链接器将没有正确的导出函数声明(def 文件仅包含名称而不包含调用约定和 c 或 c++ 名称) - 所以它总是被视为 extern "C"
和 __cdecl
的符号
在具体情况下可见,在 dll 函数中实现为 int TestFunc01(int v)
- 所以没有 extern "C"
- 因为 lib 文件中的结果将是 __imp_?TestFunc01@@YAHH@Z
这样的符号(我假设 __cdecl 和 x86),但在与 extern "C"
一起使用的另一个模块函数中 - 所以链接器将搜索 __imp__TestFunc01
当然找不到,因为它不存在于 lib 文件中。因为这样,当我们 export/import 一些符号时 - 它必须 等于 为两个模块声明。最好在单独的 .h 文件中使用显式调用约定
声明它
这不是要与 竞争。 David Heffernan 在评论中提出了一个很好的观点,关于 dllimport
的使用。所以我想做几个测试,看看当我使用 __declspec(dllimport)
和不使用时有什么区别:
1。 x86 发布版本
DLL 中的函数声明:
extern "C" int __cdecl TestFunc01(int v)
{
WCHAR buff[256];
::StringCchPrintf(buff, _countof(buff), L"You passed a value of %d", v);
return ::MessageBox(NULL, buff, L"Test Dll Message", MB_OK | MB_ICONINFORMATION);
}
然后从另一个模块导入并调用它:
和__declspec(dllimport)
extern "C" __declspec(dllimport) int __cdecl TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
call
指令从导入地址读取函数地址Table(IAT):
它给出了导入函数的位置:
没有__declspec(dllimport)
extern "C" int __cdecl TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
在这种情况下,它是相对 call
到单个 jmp
指令:
依次从 IAT 读取函数地址:
并跳转到它:
2。 x64 发布版本
对于 64 位,我们必须将调用约定更改为 __fastcall
。其余保持不变:
和__declspec(dllimport)
extern "C" __declspec(dllimport) int __fastcall TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
call
指令再次从 IAT 读取函数地址(在 x64 的情况下它使用相对地址):
这给了它函数地址:
没有__declspec(dllimport)
extern "C" int __fastcall TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
又是一个亲戚 call
到单身 jmp
:
依次从 IAT 读取函数地址:
并跳转到它:
结论
因此,如您所见,不使用 dllimport
从技术上讲会导致额外的 jmp
。我不确定跳转的目的是什么,但肯定不会使您的代码 运行 更快。可能是代码维护,可能是 hot-patching 更新的方式,也可能是调试功能。因此,如果有人能阐明这次跳跃的目的,我将很高兴听到。
比如说,如果一个 DLL 有一个函数:
int TestFunc01(int v)
{
WCHAR buff[256];
::StringCchPrintf(buff, _countof(buff), L"You passed a value of %d", v);
return ::MessageBox(NULL, buff, L"Test Dll Message", MB_OK | MB_ICONINFORMATION);
}
仅按序数值导出(以下为.def
文件):
LIBRARY DllName
EXPORTS
TestFunc01 @1 NONAME
所以现在当我想从另一个模块静态 link 到该函数时,如果该函数是按名称导出的,我会执行以下操作:
extern "C" __declspec(dllimport) int TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
但是我如何仅通过其序数值静态地link它?
PS。我正在使用 Visual Studio C++ 编译器 & linker.
您可以使用 lib.exe 工具从您编写的 .DEF 文件创建 .lib 文件,您不必实际提供任何目标文件。
您可以编写一个 .def 来匹配您要导出的序数,但还要提供名称,然后使用 lib.exe 创建一个 .lib。
在代码中,您可以将其声明为: extern "C" __declspec(dllimport) ret_type funcName(arg_type);
重要的是调用约定与函数实际使用的任何内容相匹配,但名称必须与您创建的 .lib 使用的内容相匹配,即使该名称不遵循该调用类型的装饰法则。
But how do I statically link to it only by its ordinal value?
完全相同,当函数按名称导出时 - 没有任何区别。在这两种情况下你都需要两件事——正确的函数声明:
extern "C" __declspec(dllimport) int /*calling convention*/ TestFunc01(int v);
和 lib 文件,包含在链接器输入中。
当您将 somename.def 文件包含到 visual studio 项目时,它会自动向链接器添加 /def:"somename.def"
选项(否则您需要手动添加此选项选项)。生成的 lib 文件将包含 __imp_*TestFunc01*
符号 - 在 * 位置将根据 c 或 c++ 符号和调用约定进行不同的修饰,如果 x86。
从另一方面来说,当你调用函数时,带有 __declspec(dllimport)
属性。编译器 (CL) 将生成 call [__imp_*TestFunc01*]
- 所以引用 __imp_*TestFunc01*
符号(再次 * 在实际装饰中)。链接器将搜索 __imp_*TestFunc01*
符号并在 lib 文件中找到它。
NONAME
选项对于这个过程无关紧要 - 这只会影响它的形成方式 IAT/INT 此函数在 PE(按名称或序号导入)
请注意,如果我们仅通过 link.exe /lib /def:somename.def
将生成的 lib 文件与 def 文件分开 - 链接器将没有正确的导出函数声明(def 文件仅包含名称而不包含调用约定和 c 或 c++ 名称) - 所以它总是被视为 extern "C"
和 __cdecl
在具体情况下可见,在 dll 函数中实现为 int TestFunc01(int v)
- 所以没有 extern "C"
- 因为 lib 文件中的结果将是 __imp_?TestFunc01@@YAHH@Z
这样的符号(我假设 __cdecl 和 x86),但在与 extern "C"
一起使用的另一个模块函数中 - 所以链接器将搜索 __imp__TestFunc01
当然找不到,因为它不存在于 lib 文件中。因为这样,当我们 export/import 一些符号时 - 它必须 等于 为两个模块声明。最好在单独的 .h 文件中使用显式调用约定
这不是要与 dllimport
的使用。所以我想做几个测试,看看当我使用 __declspec(dllimport)
和不使用时有什么区别:
1。 x86 发布版本
DLL 中的函数声明:
extern "C" int __cdecl TestFunc01(int v)
{
WCHAR buff[256];
::StringCchPrintf(buff, _countof(buff), L"You passed a value of %d", v);
return ::MessageBox(NULL, buff, L"Test Dll Message", MB_OK | MB_ICONINFORMATION);
}
然后从另一个模块导入并调用它:
和__declspec(dllimport)
extern "C" __declspec(dllimport) int __cdecl TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
call
指令从导入地址读取函数地址Table(IAT):
它给出了导入函数的位置:
没有__declspec(dllimport)
extern "C" int __cdecl TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
在这种情况下,它是相对 call
到单个 jmp
指令:
依次从 IAT 读取函数地址:
并跳转到它:
2。 x64 发布版本
对于 64 位,我们必须将调用约定更改为 __fastcall
。其余保持不变:
和__declspec(dllimport)
extern "C" __declspec(dllimport) int __fastcall TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
call
指令再次从 IAT 读取函数地址(在 x64 的情况下它使用相对地址):
这给了它函数地址:
没有__declspec(dllimport)
extern "C" int __fastcall TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译后的机器码:
又是一个亲戚 call
到单身 jmp
:
依次从 IAT 读取函数地址:
并跳转到它:
结论
因此,如您所见,不使用 dllimport
从技术上讲会导致额外的 jmp
。我不确定跳转的目的是什么,但肯定不会使您的代码 运行 更快。可能是代码维护,可能是 hot-patching 更新的方式,也可能是调试功能。因此,如果有人能阐明这次跳跃的目的,我将很高兴听到。