【问题标题】:Unresolved external symbol when creating a DLL创建 DLL 时无法解析的外部符号
【发布时间】:2012-04-14 14:08:24
【问题描述】:

我的一个朋友在创建 DLL 时遇到了一堆错误。 Visual Studio 抱怨未解析的外部符号。我主要是 Unix 用户,所以我可能会误会。在 Unix 上,当您创建一个静态库(存档)时,它所做的只是将不同的目标文件连接到一个存档文件中。我希望以相同的方式创建动态对象,但显然会发生额外的链接阶段。

第一个问题:为什么dll会有链接阶段?

在这种情况下,DLL 确实包含未定义的符号,因为我们希望 DLL 在 EXE 文件中找到这些符号。这与典型的 DLL 行为完全相反,EXE 使用 DLL 中定义的符号。为了清楚起见,我希望当 DLL 加载到内存中时可以找到这些符号。

第二个问题:如何让 DLL 使用 EXE 文件中定义的符号?

编辑: 我重新提出了问题,因为我认为我没有足够清楚地说明问题。

【问题讨论】:

  • 参见MSDN关于相互导入的文章。

标签: c++ visual-studio dll linker


【解决方案1】:

您在对 Luchian Grigore 的回答的评论中将问题的根源描述为:“我希望 DLL 从 exe 文件中导入一些符号”。您在问题文本中还写道,您希望“DLL 在 EXE 文件中找到这些符号。我们希望 DLL 在 EXE 文件中找到这些符号。”

是否从 exe 中导出函数或数据主要是设计问题。通常,仅从 DLL 创建导出。如果EXE 需要向DLL 提供一些信息,它通过参数 提供信息。例如,您在 EXE 中调用某个函数 MyFunc 在 DLL 中实现和导出。作为MyFunc的附加参数,你会得到context指针,它可以直接或间接获取DLL所需的所有EXE信息。

在极少数情况下,您可以从 EXE 中导出数据或函数。例如,您使用DumpBin.exe 实用程序(只需启动“Visual Studio 命令提示符(2010)”即可使用它)来验证 Outlook.exe 是否导出

DumpBin.exe /exports "C:\Program Files\Microsoft Office\Office14\OUTLOOK.EXE"

File Type: EXECUTABLE IMAGE

  Section contains the following exports for outlook.exe

    00000000 characteristics
    4E79B6C8 time date stamp Wed Sep 21 12:04:56 2011
        0.00 version
           1 ordinal base
          66 number of functions
          66 number of names

    ordinal hint RVA      name

          1    0 00B58A88 CleanupAddressComponents
          2    1 00B58A88 CleanupNameComponents
          3    2 00228DC4 DllCanUnloadNow
          4    3 004848F8 DllGetClassObject
          ...
         65   40 0038EF30 UpdateContactTracker
         66   41 00902788 dwIsLoggingEnabled

我可以解释你如何在没有长时间讨论的情况下实现这个场景,你何时以及是否真的应该这样做。

首先,LIB 文件包含另一种格式为Program Executable (PE) 的OBJ 文件。在编译期间,不同的公共部分将被放置在 OBJ 文件中。非常重要的是,可执行程序(EXE 或 DLL)不仅包含来自代码,而且在 PE 的标头部分中还有许多附加信息。最重要的是

  • 导出目录
  • 导入目录
  • 导入地址表目录
  • 基本重定位目录
  • 资源目录

您可以使用DumpBin.exe 实用程序(只需启动“Visual Studio 命令提示符(2010)”即可轻松使用它)。要查看有关标头的信息,您可以使用DumpBin.exe /headers my.exe。要查看导出目录的包含,您可以使用DumpBin.exe /exports my.exe 等。

如果您编译导出某些函数或数据的 DLL,则会另外创建 LIB 文件。它是如此命名的导入库。如果您在 EXE 项目中使用 LIB,它使用 DLL 中的某些函数或数据,则链接器将解析外部引用并放置在 EXE 的导入目录中有关应在加载时解析的函数的信息时间。

所以导入库只包含EXE中填写导入目录和导入地址表目录的模板。

一般来说,可以通过同样的方式从EXE中导出函数的一些数据,创建LIB,在DLL项目中使用LIB,并以同样的方式实现从EXE中导入DLL中的一些信息。

我制作了the demo project,它演示了方法。 如果您想从项目中删除所有 LIB 并自行创建,请仔细阅读我回答末尾的编译说明ExportFromExe.c的代码(EXE):

//#define CREATE_IMPORT_LIBRARY_ONLY
#include <Windows.h>

EXTERN_C __declspec(dllexport) int someData = 0;
EXTERN_C __declspec(dllexport) int __stdcall myFunc (int x);
EXTERN_C __declspec(dllexport) int __stdcall MyFunc();

int __stdcall myFunc (int x)
{
    return x + 10;
}

#ifndef _DEBUG
int mainCRTStartup()
#else
int main()
#endif
{
    someData = 5;
#ifndef CREATE_IMPORT_LIBRARY_ONLY
    return MyFunc();
#endif
}

MyDll.c(DLL)的代码:

#include <Windows.h>

EXTERN_C __declspec(dllexport) int myData = 3;
EXTERN_C __declspec(dllimport) int someData;
EXTERN_C __declspec(dllimport) int __stdcall myFunc (int x);

#ifndef _DEBUG
EXTERN_C BOOL WINAPI _DllMainCRTStartup (HINSTANCE hinstDLL, DWORD fdwReason,
                                         LPVOID lpvReserved)
#else
BOOL WINAPI DllMain (HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
#endif
{
    if (fdwReason == DLL_PROCESS_ATTACH)
        DisableThreadLibraryCalls(hinstDLL);

    return TRUE;
    UNREFERENCED_PARAMETER (lpvReserved);
}

EXTERN_C __declspec(dllexport) int WINAPI MyFunc()
{
    return someData + myFunc(myData);
}

为了能够在第一时间成功创建项目,我们必须解决以下问题:“谁先做:先有鸡还是先有蛋?”因为 EXE 项目依赖于 MyDll.lib 而 DLL 项目依赖于 ExportFromExe.lib。对于 EXE 的第一次编译,我们可以从 EXE 项目的链接器设置中临时删除 $(OutDir)MyDll.lib 并定义 CREATE_IMPORT_LIBRARY_ONLY。结果,我们将创建ExportFromExe.exeExportFromExe.lib。在更大的项目中,可以改用链接器的Undefined Symbol Only (/FORCE:UNRESOLVED) 选项。然后我们可以构建MyDll 项目,该项目创建MyDll.dllMyDll.lib。现在您可以从 EXE 中删除 CREATE_IMPORT_LIBRARY_ONLY 并将 $(OutDir)MyDll.lib 作为链接器设置(设置的“输入”部分中的“附加依赖项”)。 EXE项目的下一个构建产生最终的解决方案。

我使用了一些小技巧来删除 C-Runtime 并将 EXE 和 DLL 的大小减少到 2.5 或 3 KB。因此,您可以使用DumpBin.exe/all 开关来检查来自EXE 和DLL 包括RAW 二进制数据的完整信息。

因为 EXE 返回结果为 ERRORLEVEL,所以您可以在推荐提示符下测试应用程序:

echo %ERRORLEVEL%
0

ExportFromExe.exe

echo %ERRORLEVEL%
18

【讨论】:

  • +1。感谢您花时间了解我遇到的问题,并用如此多的细节和示例来回答它。您一定会得到响应点。
  • @qdii:不客气!出口有很多选择。有太多方面对于理解这个问题很重要,但我不想在这里写一本书:-)。我只是试图保持简短并解释一些背景信息,并在工作示例中仅显示一种可能性。我很高兴听到我可以帮助您理解并解决您的问题。
  • 一个很好的答案,很遗憾看到它只有 4 个赞成票。所以现在它有五分之一。 :)
  • @Jules:很高兴你觉得我的回答很有趣。
【解决方案2】:

第一个问题:为什么 dll 有一个链接阶段?

因为事情就是这样。每次您要创建二进制文件时,都会有一个链接阶段。符号需要以某种方式解析,对吧?

第二个问题:我该怎么做?

您将与dll 一起生成的lib 文件添加到项目中的Additional Dependencies - 属性 -> 配置属性 -> 链接器 -> 输入

注意:

如果您还没有这样做,为了导出到lib,符号必须用_declspec(dllexport) 声明。当您包含标头时,您告诉编译器这些符号将使用_declspec(dllimport) 导入。

【讨论】:

  • 我想他是在问如何从 DLL 中调用 exe 中的函数,而不是如何从 exe 中调用 DLL 函数。
  • @LuchianGrigore 但是创建静态库文件时没有链接阶段。目标文件只是连接在一起。然而它是一个二进制文件。
  • @tinman:是的,完全正确。我希望 DLL 从 exe 文件中导入一些符号。
  • @qdii 如果您将您的 dll 静态链接到另一个库,则不会使其成为静态库。
  • @qdii 是一样的。除了入口点和可以运行的exe之外,exe和dll之间确实没有太大区别。同样的原则也适用。
猜你喜欢
  • 2012-09-14
  • 1970-01-01
  • 1970-01-01
  • 2012-09-18
  • 2018-07-01
  • 2018-01-16
  • 2018-02-21
  • 1970-01-01
  • 2013-07-17
相关资源
最近更新 更多