MinGW 与 MSVC 生成 DLL 各种情况的折腾笔记

时间:2021-04-21 01:56:47

本博文由CSDN博主zuishikonghuan所作,版权归zuishikonghuan所有,转载请注明出处:http://blog.csdn.net/zuishikonghuan/article/details/51918076

写这篇博客,主要是刚折腾 MinGW,相关内容网上的资料不全,而且错误很多

其实之前我根本没把这个当回事,我就想 MinGW 跟 Linux 上的 GNU 编译器不会有差别,但是事实却不是这样。。。

提示:所有代码均使用 __stdcall

安装 MSVC 和 MinGW

MSVC:安装 Visual Studio,之后即可在开始菜单中找到“Visual Studio开发人员命令提示”,启动后会自动配制环境变量,不多说了(之前我写过提取 MSVC 编译器的博客)

MinGW:这真是一个悲伤的故事,官方的下载工具总是失败,看起来需要搭*,其实,有一种更简单的方法。。

http://www.mingw.org/wiki/InstallationHOWTOforMinGW 里面下载各个组件,然后自己解压到一起就行。注意上面的页面中有的组件的连接已经失效了(但放心并不多),所以只能在 MinGW 的 Sourceforge 上一点点找了。

MSYS 环境就不用了,这个下来不好用,版本很老,不知道官方为什么不更新,其实,只需要安装一个 msysgit,MSYS 环境就有了,版本也是最新的,不过 msysgit 在 AWS 上,还是需要搭*才能下载。

嘿嘿,写一个超简单的脚本

#!bash
export PATH="/c/Users/abc/Downloads/MinGW/MinGW/bin:$PATH"
bash

把 /c/Users/abc/Downloads/MinGW/MinGW/bin 换成你的 MinGW/bin 目录即可,双击打开一个可以用 MinGW GCC、G++ 的 Bash 终端。

MinGW 调用 MinGW 生成的 DLL

自家调用自家的,也会出现问题,别不信,比如这儿有 dll.cpp 和 dlluse.cpp

#include <Windows.h>

BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved
) {
return TRUE;
}

extern "C" __declspec(dllexport) void WINAPI showMessage() {
MessageBoxA(0, "I am showMessage", 0, 0);
}

extern "C" __declspec(dllexport) void WINAPI showMessage2() {
MessageBoxA(0, "showMessage2", 0, 0);
}
#include <Windows.h>

extern "C" void WINAPI showMessage();
extern "C" void WINAPI showMessage2();

int main() {
showMessage();
}

如果我们这样编译:

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--out-implib,lib1.a

$ g++ -mwindows -static dlluse.cpp -l1 -L.

$ ./a.exe

这样是没有问题的,但是,问题出现在了 –kill-at 选项上

我们先用微软的 dumpbin 工具来看一下导出表:

> dumpbin /exports 1.dll
Microsoft (R) COFF/PE Dumper Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file 1.dll

File Type: DLL

Section contains the following exports for 1.dll

//....

ordinal hint RVA name

1 0 000012BB showMessage2@0
2 1 0000128C showMessage@0

Summary

//....

每个函数后面都出现了一个“@n”,如果我们不希望 DLL 的导出函数中还要带着一个 “@n” 标记(就像Windows的DLL一样都是没有的),在 MSVC 中我们可以通过 DEF 导出,MInGW 则提供了 –kill-at 选项。

你可能会问为什么非的不要这个呢,如果带上这个参数占用的堆栈大小,那么如果我们想动态调用 DLL 中的函数,就得自己计算这个大小,这是不能忍受的,OK,我们使用 –kill-at 来编译试试:

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--kill-at,--out-implib,lib1.a

来看看是否生成了不带“@n”的导出函数

> dumpbin /exports 1.dll
Microsoft (R) COFF/PE Dumper Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

//....

ordinal hint RVA name

1 0 0000128C showMessage
2 1 000012BB showMessage2

//....

果然如此,那么我们尝试编译 dlluse.cpp,但看见结果的那一刻,真是吓死宝宝了。

$ g++ -mwindows -static dlluse.cpp -l1 -L.
C:\Users\abc\AppData\Local\Temp\cc3M0ER9.o:dlluse.cpp:(.text+0xc): undefined reference to `showMessage@0'
collect2.exe: error: ld returned 1 exit status

我明明有-l1 -L.,你告诉我“未定义的引用”(相当于MSVC的“无法解析的外部符号”),难不成是闹鬼了不成。。

于是吓得我赶紧 Google,但网上愣是没有一个人知道如何在 MinGW 中用导入库导入不带at的函数。

但是也有人(包括 MinGW 官方的手册)提供了一个方法,就是直接把 dll 输入进去:

$ g++ -mwindows -static dlluse.cpp 1.dll
Warning: resolving _showMessage@0 by linking to _showMessage
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups

出现了一个警告,如果想屏蔽这个警告,按照提示添加“–enable-stdcall-fixup”参数即可:

$ g++ -mwindows -static dlluse.cpp 1.dll -Wl,--enable-stdcall-fixup

这样虽然能解决问题,但是总是觉得怪怪的,毕竟把一个 PE 文件当作目标文件或源文件输入进去总是觉得不好,Linux 上的 GNU 编译器绝对不能直接把 so 库传进去的,MinGW 你是要闹哪样?

我就是想用导入库来调用不带at的DLL函数,MinGW你做不到吗。。

在接下来的时候,我一直在考虑这个问题,最终茶饭不思(咳咳,扯远了),不过最终还是让我找到了方法

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--output-def,1.def

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--kill-at

$ dlltool --kill-at -d 1.def --dllname 1.dll -l lib1.a

$ g++ -mwindows -static dlluse.cpp -l1 -L.

$ ./a.exe

解释:第一次编译,是按照有 at 的编译,生成 1.dll 和一个模块定义文件 1.def,第二次再编译,按照没有at的生成,第三步,使用第一步生成的 def 文件生成导入库,一定要添加 –kill-at 选项。

生成的 1.def 内容如下

EXPORTS
showMessage2@0 @1
showMessage@0 @2

后面的序号部分不是必要的,要的就是“@n”

总之我之前是很怀疑这样究竟行不行的,但事实告诉我这样可以,用 dumpbin 查 exports 和 imports 都是没有at的!

虽然解决了,但是心里并不高兴,在 MinGW 中为了达到这个目的,需要编译(连接)两次同一个代码,让我这个强迫症浑身不舒服。

MSVC 调用 MinGW 生成的 DLL

先来说说带 at 的怎么处理

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--output-def,1.def

然后就是在 MSVC 中编译 dlluse 了

> lib /machine:i386 /def:1.def
Microsoft (R) Library Manager Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

正在创建库 1.lib 和对象 1.exp

> cl /MT /c dlluse.cpp
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.00.23918
版权所有(C) Microsoft Corporation。保留所有权利。

dlluse.cpp

> link dlluse.obj 1.lib
Microsoft (R) Incremental Linker Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

> dlluse.exe

首先利用 MinGW 生成的 def 文件创建了导入库,然后编译 dlluse,连接,运行

这样是没有问题的。但是莫名其妙的问题在不带 at 的DLL中出现了:

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--output-def,1.def,--kill-at

$ cat 1.def
EXPORTS
showMessage = showMessage@0 @1
showMessage2 = showMessage2@0 @2

> lib /machine:i386 /def:1.def
Microsoft (R) Library Manager Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

正在创建库 1.lib 和对象 1.exp

#刚才已经cl编译过了,现在直接连接就行
> link dlluse.obj 1.lib
Microsoft (R) Incremental Linker Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

dlluse.obj : error LNK2019: 无法解析的外部符号 _showMessage@0,该符号在函数 _main 中被引用
dlluse.exe : fatal error LNK1120: 1 个无法解析的外部命令

这个问题又是闹得我茶饭不思啊(笑),这个问题真是麻烦,MSVC 的 link 是支持 def 文件中的“=”的,但 lib 不支持,于是我就想还是用带 at 的版本的 def,用不带 at 的DLL文件不就行了,就像上面编译两次的那个是一个道理

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--output-def,1.def

$ g++ -mwindows -static -shared -o 1.dll dll.cpp -Wl,--kill-at

#标准输出略
> lib /machine:i386 /def:1.def

> cl /MT /c dlluse.cpp

> link dlluse.obj 1.lib

> dlluse.exe

整个过程一气呵成,没有任何问题,正当我满心欢喜的查看成果的时候,惊呆了,弹出的是“showMessage2”而不是 “I am showMessage”。

这又是闹哪样啊,不过内心也在庆幸自己导出了两个函数,不然这个问题可能真发现不了。

不多说,dumpbin 走起

> dumpbin /exports 1.dll
Microsoft (R) COFF/PE Dumper Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file 1.dll

File Type: DLL

//....

ordinal hint RVA name

1 0 0000128C showMessage
2 1 000012BB showMessage2
//....

> dumpbin /exports 1.lib
Microsoft (R) COFF/PE Dumper Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file 1.lib

File Type: LIBRARY

Exports

ordinal name

1 _showMessage2@0
2 _showMessage@0

//....

> dumpbin /imports dlluse.exe
Microsoft (R) COFF/PE Dumper Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file dlluse.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

1.dll
40D000 Import Address Table
412264 Import Name Table
0 time date stamp
0 Index of first forwarder reference

Ordinal 2
//....

MSVC 按照序号导入,但是为神马 DLL 里的序号和导入库里的不一样啊,我下意识就感觉是 def 出问题了,用vim 1.def看看def文件有没有问题,果然如此:

EXPORTS
showMessage2@0 @1
showMessage@0 @2

看来是用 MinGW 编译两次产生的后遗症

于是我就想,既然这样我就把def中的序号都删除了吧

$ sed -i 's/ @.*//g' 1.def

$ cat 1.def
EXPORTS
showMessage2@0
showMessage@0

这下连接是没有问题了,正当我满心欢喜运行之时,又是一盆冷水下来,csrss告诉我:无法定位程序输入点 showMessage@0 于动态链接库 1.dll 上。

可恶,在导出函数不带 at 的情况下,如果 def 中不带 at,就会导致代码中生成的弱符号“showMessage@0”找不到对应的强符号,如果def中带 at,link是没有问题的,但是却只能在 PE 文件的导入表中使用“showMessage@0”而不能用“showMessage”

只能说微软的 lib.exe 功能太弱,生成的导入库很多功能比不上 link 生成DLL时生成的。

如果def中不带at,那么生成的导入库就不能与代码正常连接,如果带at,显然可以与代码连接,但却显然无法从DLL的不带at中找到相应的导出函数,归根到底,是 MSVC 的 lib.exe 不支持别名(即“=”后的内容被忽略),现在我们已经走入了死胡同。

但我突然灵光一闪(笑),刚刚的那个情况,不就说明了可以在 def 中使用带at的名字,而连接到正确的不带at的函数吗,那个不是通过别名来实现的(这一点和 MinGW 不同),而是靠的函数序号,之所以不行,是因为序号和DLL中导出的序号不一致而已

想明白了这一点,问题就简单了,我们可以这样:

$ pexports -h dlluse.cpp -o 1.dll > 1.def

$ cat 1.def
LIBRARY 1.dll
EXPORTS
showMessage@0 @1
showMessage2@0 @2

> lib /machine:i386 /def:1.def

> dumpbin /exports 1.lib
Microsoft (R) COFF/PE Dumper Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file 1.lib

File Type: LIBRARY

Exports

ordinal name

2 _showMessage2@0
1 _showMessage@0

//....

其中的-h dlluse.cpp不能少,我们需要有一个包含函数声明的头文件,这样 pexports 才能正确计算at后面的堆栈字节数。

这样序号就对了,之后在 link 就行了!

MinGW 调用 MSVC 生成的 DLL

这个就非常简单了,我们先用 MSVC 编译这个 DLL(用 MSVC 编译器的估计大家都用 DEF 导出,也就是不带 at 的):

> cl /MT /c dll.cpp
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.00.23918
版权所有(C) Microsoft Corporation。保留所有权利。

dll.cpp

> link /dll dll.obj user32.lib /def:dll.def
Microsoft (R) Incremental Linker Version 14.00.23918.0
Copyright (C) Microsoft Corporation. All rights reserved.

正在创建库 dll.lib 和对象 dll.exp

最简单的办法就是上面说的 MinGW 的“个性”:

$ g++ -mwindows -static dlluse.cpp dll.dll -Wl,--enable-stdcall-fixup

如果不想这样,可以这样:

$ pexports -h dlluse.cpp dll.dll > dll.def

$ dlltool --kill-at -d dll.def --dllname dll.dll -l libdll.a

$ g++ -mwindows -static dlluse.cpp -ldll -L.

直接用原来的def生成导入库再使用会出现连接错误的,原因就不说了,因为道理和上面的情况是一个道理。

总算写完了,现在思路清晰多了(笑)