创建DLL时出现未解决的外部符号

6
我的一个朋友在创建DLL时遇到了许多错误。Visual Studio抱怨有未解决的外部符号。我主要是Unix用户,所以可能会犯错。在Unix上,当您创建静态库(存档)时,它并没有做太多工作,只是将不同的对象文件连接到一个存档文件中。我希望动态对象也是以同样的方式创建的,但显然,会发生额外的链接阶段。
第一个问题:为什么dll需要链接阶段?
在这种情况下,DLL确实包含未定义的符号,因为我们期望DLL在EXE文件中找到这些符号。这与典型的DLL行为完全相反,其中EXE使用DLL中定义的符号。为了明确起见,我希望在DLL加载到内存时立即找到这些符号。
第二个问题:如何使DLL使用在EXE文件中定义的符号?
编辑:我重新表述了问题,因为我认为我没有清楚地陈述问题。

1
请参阅 MSDN 上关于相互导入的文章。 - Jerry Coffin
2个回答

12
您在Luchian Grigore的回答评论中描述了问题的起源:“我希望DLL从exe文件中导入一些符号”。您在问题的文本中还写道,您想要“DLL在EXE文件中查找这些符号。 我们期望DLL能够在EXE文件中找到这些符号。”
大多数情况下,是否从exe导出函数或数据都是一个设计问题。通常只从DLL中创建导出。如果EXE需要向DLL提供一些信息,则通过“参数”提供信息。例如,在EXE中调用某个由DLL实现和导出的函数MyFunc时,您将获得context指针作为MyFunc的附加参数,该指针可以直接或间接地使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文件包含OBJ文件,其格式与程序可执行文件(PE)不同。在编译期间,不同的公共部分将被放置在OBJ文件中。非常重要的是,程序可执行文件(EXE或DLL)不仅包含代码,还有许多附加信息在PE的头部分。其中最重要的是:
  • 导出目录
  • 导入目录
  • 导入地址表目录
  • 基址重定位目录
  • 资源目录
你可以使用DumpBin.exe实用程序(只需启动“Visual Studio命令提示符(2010)”即可轻松使用它)。要查看有关标头的信息,可以使用DumpBin.exe /headers my.exe。要查看导出目录的内容,可以使用DumpBin.exe /exports my.exe等等。
如果你编译一个导出一些函数或数据的DLL,那么将会额外创建LIB文件,这被称为“导入库”。如果你在使用DLL中的某些函数或数据的EXE项目中使用LIB,链接器将解析外部引用,并在EXE的导入目录中放置关于应该在加载时解析的函数的信息。因此,导入库仅包含用于填充EXE中的Import Directory和Import Address Table Directory的模板。通常情况下,可以以同样的方式从EXE导出一些数据或函数,创建LIB,在DLL项目中使用LIB,并以此实现从EXE向DLL导入一些信息。我制作了一个演示过程的演示项目。请仔细阅读我的答案末尾的编译说明,如果你想从项目中删除所有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.exe和ExportFromExe.lib。在更大的项目中,可以使用链接器的Undefined Symbol Only (/FORCE:UNRESOLVED)选项。然后我们可以构建MyDll项目,它将创建MyDll.dll和MyDll.lib。现在您可以从EXE中删除CREATE_IMPORT_LIBRARY_ONLY,并将$(OutDir)MyDll.lib包括在链接器设置中(“Input”部分中的“Additional Dependencies”)。下一次构建EXE项目将产生最终解决方案。
我使用了一些小技巧来移除C-Runtime并将EXE和DLL的大小减小到2.5或3 KB。因此,您可以使用DumpBin.exe/all开关来检查包括原始二进制数据在内的完整信息。
由于EXE以ERRORLEVEL形式返回结果,您可以在命令提示符中测试应用程序:
echo %ERRORLEVEL%
0

ExportFromExe.exe

echo %ERRORLEVEL%
18

+1。感谢您花时间了解我遇到的问题,并用如此多的细节和示例进行回答。您肯定会得到回应分数。 - qdii
@qdii:不客气!导出时有很多选项可供选择。理解问题的重要方面太多了,但我不想在这里写一本书 :-)。我只是试图简短地阐述一些背景信息,并展示一个工作示例中的可能性。很高兴听到我能帮助你理解和解决问题。 - Oleg
一个很好的回答,可惜只有4个赞。现在它有第五个赞了。 :) - Jules
@Jules:很高兴你觉得我的回答有趣。 - Oleg

3

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

因为这是创建二进制文件的常规步骤。符号必须以某种方式被解析,不是吗?

第二个问题:如何做到这一点?

您可以将与dll同时生成的lib文件添加到项目的属性 -> 配置属性 -> 链接器 -> 输入 -> 附加依赖项中。

注意:

如果您没有这样做,那么为了能够被导出到lib中,符号必须使用_declspec(dllexport)进行声明。当您包含头文件时,您告诉编译器这些符号将使用_declspec(dllimport)进行导入。


我认为他在问如何从DLL中调用exe中的函数,而不是如何从exe中调用DLL函数。 - tinman
@tinman:是的,没错。我想让DLL从exe文件中导入一些符号。 - qdii
@qdii 如果你将你的 DLL 静态链接到另一个库中,这并不意味着它成为了一个静态库。 - Luchian Grigore
1
@qdii 这其实是一样的东西。一个.exe 和一个.dll 之间并没有太大的区别,除了入口点和可以直接运行这个exe。原理是一样的。 - Luchian Grigore
@LuchianGrigore:我并不是想将动态库链接到静态库。我甚至不知道这是什么意思。我只是想指出你所说的:“每次创建二进制文件时都有一个链接阶段”是错误的。当创建一个存档文件(library.a)时,没有链接阶段,它仍然是一个二进制文件。我本来以为DLL文件也是一样的,但我无法理解为什么会不同。 - qdii
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接