如何静态地 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* 符号 - 在 * 位置将根据 cc++ 符号和调用约定进行不同的修饰,如果 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 这样的符号(我假设 __cdeclx86),但在与 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 更新的方式,也可能是调试功能。因此,如果有人能阐明这次跳跃的目的,我将很高兴听到。