【问题标题】:How to statically link to a DLL function that is exported by an ordinal?如何静态链接到由序号导出的 DLL 函数?
【发布时间】:2018-06-11 08:44:03
【问题描述】:

假设一个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

所以现在当我想从另一个模块静态链接到该函数时,如果该函数是按其名称导出的,我将执行以下操作:

extern "C" __declspec(dllimport) int TestFunc01(int v);

int _tmain(int argc, _TCHAR* argv[])
{
    TestFunc01(123);
}

但我如何仅通过其序数值静态链接到它?

PS。我正在使用 Visual Studio C++ 编译器和链接器。

【问题讨论】:

  • 当您构建导出某些功能的项目时 - lib 文件总是会自动创建。您只需要将这个 lib 文件包含到另一个项目的链接器输入中,您想要在其中导入此函数。函数将按名称或序号导出无关紧要 - extern "C" __declspec(dllimport) int TestFunc01(int v); + lib 文件将起作用。因为您没有明确指定调用约定 - 两个项目必须具有相同的
  • @RbMm:当然,如果我从.def 文件中删除NONAME,它将按照您的描述工作。但这不是重点。我问的是使用序号函数号静态链接到该函数。否则,如果我尝试使用上面 DLL 中的 .lib 文件编译第二个模块,它将编译正常,但链接器将发出 TestFunc01 作为未声明的函数错误并且不会链接。
  • 不,与NONAME 一起工作也很完美
  • @RbMm: :) 好吧,我正坐在一个 VS 2013 链接器前,它给了我这个错误。
  • 这个完全不依赖于VS。查看你的 lib 文件 - 这里的TestFunc01 是哪种形式?哪个是未解析的符号(确切名称)报告的链接器?

标签: c++ visual-studio winapi dll dllimport


【解决方案1】:

您可以使用 lib.exe 工具从您编写的 .DEF 文件创建一个 .lib 文件,您不必实际提供任何目标文件。

您将编写一个与要导出的序数匹配的 .def,同时提供名称,然后使用 lib.exe 创建一个 .lib。

在代码中,您可以将其声明为: extern "C" __declspec(dllimport) ret_type funcName(arg_type);

重要的是调用约定与函数实际使用的任何内容相匹配,但名称必须与您创建的 .lib 使用的名称匹配,即使该名称不遵循该调用类型的正常修饰。

【讨论】:

  • 您实际上不必提供任何目标文件 - 不,这是错误的。在这种情况下,lib 文件不能包含 x86 平台上符号的 正确 名称。因为它根本没有它的定义。任何符号都将被视为extern "C" 和__cdecl。所以这不适用于 c++ 符号,也不适用于 x86 上的 __cdecl 函数。说如果 TestFunc01 声明为 __sdtcall - 具有正常的 lib 文件构建(包括对象)__imp__TestFunc01@4 符号将在 lib 中,但只有 def 文件 - __imp__TestFunc01
【解决方案2】:

但我如何仅通过其序数值静态链接到它?

绝对相同的方式,当按名称导出函数时 - 没有任何区别。在这两种情况下,您都需要两件事 - 正确声明函数:

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 文件中。因为这个,当我们导出/导入一些符号时 - 它必须为两个模块声明equal。最好在具有显式调用约定的单独 .h 文件中声明它

【讨论】:

  • 感谢您的帮助。是的,你是对的。我的错。只是出于好奇。你在这个论坛上对我很有帮助。你怎么这么快就知道所有的答案? :)
  • 这里肯定不需要dllimport。我们有一个定义函数的 lib 文件。
  • @DavidHeffernan - 定义函数的 lib 文件 - 不,未定义 lib 文件。这里是链接器搜索符号的地方。但这不是编译器的定义。当我们使用或不使用__declspec(dllimport) 时也存在严重的不同 - 如果我们使用它 - 编译器生成调用 - call [__imp_*TestFunc01*] 和链接器将搜索 __imp_*TestFunc01* 符号(这也是隐式的 selectany 类型)。如果我们不使用dllimport - 编译器生成调用call *TestFunc01* 和链接器将搜索*TestFunc01* 符号。
  • @c00000fd - 如果函数 X 没有用 __declspec(dllimport) 标记 - 编译器假定这是普通函数并生成 call X。稍后,在链接时 - 链接器搜索 X 符号 - 如果在导入库中找到它。实现 body-stub X: jmp [__imp_X]。如果我们用dllimport 标记函数 - 编译器只生成call [__imp_X] 而不是call X。和链接器搜索__imp_X 符号。但是现代的CL 有/GL 选项(链接时间代码生成)。使用它(和/LTCG 链接器选项),即使您调用未标有dllimport 链接器的导入函数删除jmp 存根
  • @c00000fd - 另请阅读blogs.msdn.microsoft.com/russellk/2005/03/20/lnk4217 - 这是旧文章,在/GL 和/LTCG (Link-time Code Generation) 之前。所以当你使用这个选项时——代码会改变——你永远不会看到call X; X: jmp [__imp_X]
【解决方案3】:

这不是为了和actual answer竞争。 David Heffernan 在那里的 cmets 中提出了一个很好的观点,关于 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指令从导入地址表(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。我不确定跳转的目的是什么,但肯定不会让你的代码运行得更快。也许这是一个代码维护的事情,也许是一种为更新进行热补丁的方法,也许是一个调试功能。因此,如果有人能阐明这次跳跃的目的,我会很高兴听到的。

【讨论】:

  • 就性能而言,调用它不太可能产生太大影响,除非您进行了测量,否则做出太多假设是愚蠢的。
  • 这意味着有一个地址可以执行重定位修复,而不是每次调用函数时都有一个。
  • 我不确定你是否会通过搬迁赢得任何东西。以下是每种情况所需重定位次数的细目:x86 with dllimport = 2, x86 without dllimport = 2, x64 with dllimport = 1, x64 without dllimport = 1。所以肯定还有别的。 @RbMm 你知道吗?
  • 您不需要每次调用 DllImport 重新定位吗?
  • 对跳跃的目的有所了解 - 没有任何目的。但是如果你没有用dlimport 标记函数,编译器就无法知道这是导入的api。他假设这是通常的功能并生成call X(相对调用)。只有在链接时链接器才发现这是导入的api。但他已经无法改变这个call X(他只能修复相对偏移量 - 4 个字节)。所以链接器并生成存根X: jmp [__imp_X]。可以通过 2 种方式更改它 - 让编译器知道这是导入的 api - 结果编译器生成间接 call [__imp_X]
猜你喜欢
  • 1970-01-01
  • 2022-01-02
  • 2010-09-30
  • 2015-08-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-20
  • 1970-01-01
相关资源
最近更新 更多