Chinaunix

标题: 心得:MinGW 链接 stdcall 的 DLL [打印本页]

作者: starwing83    时间: 2013-12-22 22:28
标题: 心得:MinGW 链接 stdcall 的 DLL

    以前一直用MinGW,导出的DLL一直都没有出问题,但是这次居然出了很严重的问题,
主要是遇到了以前从来没有遇到过的 stdcall 调用约定。

    假设一个函数叫做 Function,根据调用约定的常识,如果是cdecl(C的调用约定)
,则在内部的链接名字就是_Function,而导出到DLL的名字却会根据导出的方式而变,无
论如何,真正在DLL里面的导出符号都是没有下划线前缀的。

    然而,导出就意味着导入,如果我们通过__declspec(dllimport)期望从动态库导入
文件,那么事情会很不一样了,在这种情况下,期待被导入的函数并不叫_Function甚至
Function,他会期望一个叫做__imp_Function的名字。

    通常这并不是什么大问题,因为 MinGW 不仅可以从 DLL 直接链接,而且在产生 DLL
的时候,也可以再产生一个.a的标准格式的导入库(VC则是至始至终都会产生这个导出库
的)。在导出库里,则会有__imp_Function这么一个静态的小函数,这个函数存在的目的
就是动态地去调用DLL里面真正导出了的函数。因为这么一个原因,如果我们在声明函数
时带上了 __declspec(dllexport) ,则我们就失去了直接链接DLL的能力了,则必须链接
ld帮我们生成的.a导入库了。

    而ld里面跟这个有关的参数就那么几个,我们是通过gcc把参数传递给ld的,因此我
们需要在参数前加入-Wl,前缀,并且用,作为参数的分隔符:

    -Wl,--output-def=<def文件名>  : 产生一个跟导出有关的def文件。
    -Wl,--out-implib=<.a文件名>   :产生一个用于提供__imp_符号的.a导入库。

    另外,如果在产生DLL文件的时候,并没有产生.a文件,也是有办法的。首先,可以
通过一个额外的工具 pexports 通过 DLL 得到 .def 文件,然后可以通过 MinGW 附带的
dlltool工具将.def 转换为.a文件。

    通常,对于C调用标准的DLL文件,MinGW是可以直接链接到它的,完全不需要
__declspec(dllexport) 这样的修饰,再加上上面的了解,基本上对DLL的使用就很清晰
了。

    然而,如果一遇上__stdcall约定的函数,那么整个局面就复杂起来了。

    首先,其实 __stdcall 约定才是Windows下面DLL的标准调用约定。如果采用 def 文
件的导出方式的话,那么__stdcall函数和普通函数其实没有任何区别,其导出的符号名
依然是Function,坏就坏在,__stdcall函数本身会清理调用它时传递的参数,因此,一
些编译器就必须得知道,这个函数调用以后,到底会把栈指针减去多少,从而预先把栈指
针加上去。很不幸的,GCC就是这么一种编译器……

    根据从这里(http://www.willus.com/mingw/yongweiwu_stdcall.html)得到的一张
表,可以看出,对于__stdcall函数,各种不同的编译器有完全不同的命名方法,而且就
算是相同的编译器,如果导出的方式不同(dllexport?或者def文件?)其最终导出的符
号也不同。

    但是,总的来说,落实到DLL上面去,也就是两种符号而已:Function,和
Function@n,这里的n是一个数字,代表Function执行完以后,会弹多少栈元素。

    对于MinGW,要和对方进行交互,我们需要考虑下面的四种情况:

    1. 如何导出 Function 名字
    2. 如何导出 Function@n 名字
    3. 如何链接到 Function 名字
    4. 如何链接到 Function@n 名字

    这里所有的讨论都没有涉及到之前提到的导出库,对于导出库的问题,我们稍后来解
释。

    导出 Function 名字还是很简单的:只要你能提供一个 def 文件,那么就可以将
stdcall函数导出为Function名字了,不过这里还有问题:gcc会给你一个警告,说会将
导出的Function链接到Function@n,提供-Wl,--enable-stdcall-fixup则会去掉这个警告
,默认执行转换,当然如果disable,则就会因为找不到需要导出的符号而链接失败了。

    导出 Function@n 的名字,一方面也可以通过 def 文件,但是这样你就得手工去算
函数会使用多少栈空间了,也许有工具可以做到这一点,但是我暂时还不知道怎么办(可
能跟nm啊objdump啊什么的有关,希望知道的童鞋能告知一下),但是,最简单的方式还
是采用__declspec(dllexport)的方法了。

    那么,如果在这种情况下,依然想通过def做点事儿,怎么办呢?上面的网站给出了
一些方法来得到def文件,并修改后重新链接,但是反正最简单的方法还是需要
__declspec(dllexport)的,这里我们就不讨论了。

    那么,现在就是链接问题了:如何链接到有@n后缀和没有@n后缀的函数去呢?

    首先,如果导出的DLL里面有@n后缀,那么事情就非常简单了,只需要直接链接即可
,但是如果DLL里面没有@n后缀,那么你直接的链接肯定是会链接失败的,这里发现一个
方法,就是依然采用-Wl,--enable-stdcall-fixup,这样可以链接到没有@n后缀的函数去


    但是,我这里遇到了更麻烦的问题……

    首先,上面的所有讨论,都基于一个特点:我们直接链接的是DLL,而且我们没有采
用任何__dllspec(dllexport)修饰,这意味着,我们不打算链接到__imp_这样的符号上去
,但是,如果DLL上面的某个符号没有导出名字,怎么办?

    我最近就遇到了这样的问题,主要是Google的ANGLE项目,即利用DirectX来实现
OpenGL ES 2.0 接口的一个库。它的libglesv2.dll这个dll里面,有六个符号是没有导出
的,只有位置,没有名字,因此,如果直接链接这个dll,链接是一定会出错的,因为没
有名字嘛。

    那么,链接我们产生的libglesv2.dll.a行不行呢?不行。因为为了保证DLL导出的符
号的顺序,这个dll的符号导出全部采用了def的方式,而def里面没有@n后缀!

    在这种情况下,我们产生的libglesv2.dll.a里面的符号,也是没有后缀的,即使是
__imp_符号,也没有@n的后缀,完全无法链接。

    我最后找到了Mozilla的网站,看样子火狐用的也是这个库来提供WebGL支持,他们的
解决方法是,直接在def文件后面加两行:

  1.     ; GCC has problems with linking to undecored stdcall functions,
  2.     ; so we explicitly add aliases for APIs used by EGL
  3.     glGetProcAddress@4=glGetProcAddress
  4.     glBindTexImage@4=glBindTexImage
  5.    
复制代码
给DLL里面的某些符号起了别名,这样才导入成功。感觉这个办法治标不治本啊,万
一隐藏符号太多,岂不是必须一个一个的这么去加么= =而且这样,本质上是把隐藏符号
给变得不隐藏了呗,最终还是可以在DLL文件的导出表里面看到这两个加了@的符号。

    所以,虽然问题解决了,但是我还是留存了两个疑问,记在这里,如果有牛人知道答
案的可以告知一下,或者以后我找到了答案,我再回来补完。

    1. MinGW如何链接.a导入库里面没有@n后缀的名字?
    2. 如何让MinGW即使是在有def文件的情况下,也始终在.a导入库里面产生@n和不带
    @n的名字呢?已经试过了--add-stdcall-alias,没效果,最理想的的状态是这样的
    ,对于每个符号,在.a里面有四个符号导出:Function, Function@n,
    __imp_Function, __imp_Function@n,这样就不愁找不到符号的问题了。

作者: webdna    时间: 2013-12-22 22:36

想不到linux也用dll
作者: starwing83    时间: 2013-12-22 22:38
我现在在混Windows啊……没办法,电脑是N卡的双显卡,目前的大黄蜂技术不成熟,在Linux我没法用我的主显卡……
作者: starwing83    时间: 2013-12-22 22:43
https://bugzilla.mozilla.org/show_bug.cgi?id=945292

这是在mozilla找到的bug贴
作者: 群雄逐鹿中原    时间: 2013-12-22 22:54
本帖最后由 群雄逐鹿中原 于 2013-12-22 22:54 编辑

非得那么麻烦吗。
我看到某软件,它的插件dll只export一个函数。大概意思是这样 ---

typedef struct
{
    void (*func1)(int, int);
    void (*func2)(int, int, int);
    int (*func3)(int, int);
   。。。
} FUNCS_T;

export这个函数。。
FUNCS_T *get_functions();



作者: starwing83    时间: 2013-12-22 23:02
回复 5# 群雄逐鹿中原


    我也想这样啊……而且对OpenGL来说,是有一个事实标准的东西的,叫glGetProcAddress,理论上有这个就OK了。

问题是,谷歌看上去不打算这样搞啊……他们都是直接上VC了,根本不考虑那么多问题…………从这个角度看起来,这的确就是MinGW本身的问题了,应该是本身就对stdcall的支持有问题吧。
作者: 群雄逐鹿中原    时间: 2013-12-22 23:22
回复 6# starwing83

看来 stdcall 一点也不 std,
何不用 VC写个中间dll,将 stdcall  转成普通调用
    _Function@n  -->  proxy_Function

mingw的程序只调用 proxy_Function。傻了点,应该有效。
作者: starwing83    时间: 2013-12-22 23:26
回复 7# 群雄逐鹿中原


    现在的问题根本就不是我想怎么办啊……现在的问题是,ANGLE这个库根本在MinGW下编译不过嘛……

这也不是第一次Google的东西在MinGW下面编译不过了,上次是skia………………

总的来说,Google什么的………………


PS:编译不过的原因是,里面的一个组件用到了另外一个组件的未命名导出函数,而MinGW没法处理这种情况……
作者: sonicling    时间: 2013-12-23 00:09
用vc连接mitab库也有类似的问题,按照mitab头文件的声明,vc2012要找的是Function@n格式的,但是mitab导出的是_Function@n。

方法是先用dumpbin从dll中提取def,
然后修改def,将所有_Function@n 替换为 Function@n
然后用def生成lib,项目中导入这个lib,就OK了。
作者: starwing83    时间: 2013-12-23 00:18
回复 9# sonicling


    恩,MinGW的导出格式就是_Function@n类型的。
作者: OwnWaterloo    时间: 2013-12-23 02:52
群雄逐鹿中原 发表于 2013-12-22 22:54
typedef struct
{
    void (*func1)(int, int);
    void (*func2)(int, int, int);
    int (*func3)(int, int);
   。。。
} FUNCS_T;

export这个函数。。
FUNCS_T *get_functions();


如果要减缓PE/ELF,...以及F,_F,_F@n,..之类的各种麻烦我觉得这是最保险的方式。  完全将平台本身的一些功能架空。
不过不是所有的库都会这么做。。。

作者: OwnWaterloo    时间: 2013-12-23 03:18
回复 1# starwing83

做一个产生导入库的工具?
以前也遇到过类似问题。 不记得有没有找到不那么ad-hoc的方法了。 不过貌似Mozilla的做法也是凑合着能用, 估计是没有这样的工具。。。
主要依赖的是PE格式以及windows加载它的行为。  编译器肯定会顺从这些行为, 但在一些细节上就各种见招拆招。
如果一个工具能桥接这两者之间的行为而不需要等到编译器提供支持 —— 比如要是想用tcc该怎么办? 它会有--enable-stdcall-fixup这种参数么? 即使有也要很熟悉才能找到 —— 还是有实用价值的?

导入库的格式是很死板的。
每个函数有两个符号。 一个是函数指针。 在加载时会初始化为对应的函数地址。 另一个是很简单的函数。 获取对应的函数指针变量里初始化后地址然后跳转过去。
这里已经没有函数签名的概念了。 于是不需要对头文件做什么手脚。
拿不准的地方是:
1. 命名
究竟哪个是_f哪个是_imp_f我记不清了。。。  假设_imp_f 是指针。 而且它们之间的命名好像在PE格式里有强制的几种关联。  也许可以不使用PE的这部分而自己产生这两个符号来避免这种关联。  反正无论是否使用PE格式的相关部分最终都是会产生这两个符号。
2. 变量
变量是肯定有一个指针符号的。 _imp_x 。 至于_x的情况忘记了。 而且导入/导出变量的情况本身就少。
3. i386
以前没有随时可用的64位机器。 于是经验都是32位上的。

作者: OwnWaterloo    时间: 2013-12-23 03:41
为了阐述清楚。 这里给出一个原型。

我从这里: http://www.dll-files.com/dllindex/dll-files.shtml?libglesv2 抓到的 libglesv2.dll 。 Linux上。。。 想编译都编译不了。。。

  1. $ objdump -p libglesv2.dll | awk -f implib.awk > libglesv2.cpp
  2. $ cat implib.awk
  3. #!/usr/bin/awk -f
  4. BEGIN {
  5.   FS="[][ \t]+"
  6.   print "#include <windows.h>"
  7.   print "extern \"C\" {"
  8.   printf "static HMODULE libglesv2 = LoadLibrary(%s);\n", "\"libglesv2.dll\""
  9. }
  10. END { print "}" }

  11. /^\[Ordinal\/Name Pointer\] Table$/ , /^$/ {
  12.   if ($2!="Ordinal/Name" && $3)
  13.   {
  14.     printf "int (*__imp_%s)(void) = (int(*)(void))GetProcAddress(libglesv2,(LPCSTR)%s);\n",$3,$2
  15.     printf "int %s(void) { return (*__imp_%s)(); }\n",$3,$3
  16.   }
  17. }
复制代码
产生的cpp文件大致是这样:

  1. #include <windows.h>
  2. extern "C" {
  3. static HMODULE libglesv2 = LoadLibrary("libglesv2.dll");
  4. int (*__imp_glActiveTexture)(void) = (int(*)(void))GetProcAddress(libglesv2,(LPCSTR)0);
  5. int glActiveTexture(void) { return (*__imp_glActiveTexture)(); }
  6. int (*__imp_glAttachShader)(void) = (int(*)(void))GetProcAddress(libglesv2,(LPCSTR)1);
  7. int glAttachShader(void) { return (*__imp_glAttachShader)(); }
  8. ...
  9. }
复制代码
将它编译之后就是一个导入库了。 也许吧。。。  能检查的地方我尽量检查了。  包括g++ -O2 -c 后会使用尾调用优化。 这样就与实际的签名无关了。
但没有真正环境很恼火。。。

导入库大致也就是做这个事情。需要改进的地方:
1. 写一个真正的PE分析器而不是objdump -p | awk 。。。 来得到导出的名字与序号。
2. 如果c++文件编译出的目标文件能和其他c文件编译出的目标文件顺利链接起来 —— 尤其是会将cpp产生的初始化代码放到合适的位置 —— 其实就不需要写一个PE的产生器了?
直接用穷人的元编程。。。
还有一些细节的地方就是用cpp的初始化机制好像比“普通”的__imp_f要来得晚可能会造成问题。 (至于错误检查这里为了简单就没有加)。
3. 如果就是不想用c++编译器。。。 其实也可以写一个PE产生器。 如果想避免 _f 与 __imp_f 名字之间的关联可以把PE的相应部分架空, 为每个指令集写分别写一点点代码。 其实也就2个指令。。。
作者: OwnWaterloo    时间: 2013-12-23 03:57
回复 1# starwing83

有没有兴趣把这个想法做成实际的工具?

我之前的笔记本里有很多和PE格式相关的原型代码。 不过已经给我爸用了。 拆下来的硬盘估计不是随便找个机器就能启动的。。。 而且还放在办公室里的。。。
也不想把家里的其他电脑上弄个环境。。。 我估计明天就会去办公室。

我也许能挤出一点时间来实现。 但短时间内应该没有机会去真正使用。 原因你懂的。。。
感觉你遇到这种问题的机会也比较多。 因为你经常用MinGW。。。
作者: starwing83    时间: 2013-12-23 04:56
回复 14# OwnWaterloo


    Lua有个pe-parser,貌似可以完成很多事情,我还没去看,准备搞定现在的麻烦事儿(OpenGL ES的一堆shader要写)以后就去研究这个,我记在TODO里面了。

你这方法敢情不错,直接下载个dll……恩,我也打算这样了,这几天编译ANGLE把我编译伤了,而且MinGW产生的dll居然比VC的大了三倍还不止(直接从Chrome取的686KB,自己编译的2120KB),一怒之下下了个Visual Studio 2013 Express还没试,不过至少项目里面有sln文件还是好事儿……恩,写到这里的时候刚刚打开sln文件试了试,编译出来的有1039kb,还没strip,不过就这样也小了一半了,MinGW这东西没有lto什么的完全没法玩C++啊……(所有文件都未包含其他的导入库,除了MinGW包含了一个msvcrt.dll,VC编译的甚至连msvcrt.dll都没包含……)

我之前想的工具什么的差不多就是你干的事情了,无非就是获取某个dumpbin什么的结果然后扔awk/sed处理一下然后做成def扔给gcc神马的,当然产生C++什么的最好了。。

说到C++,最近写向量运算算是服气了,这东西没C++完全没法写……一个vec2用C写简直是要死人……C++直接加减乘除完事儿。不过就是需要时刻对写下的代码到底做了什么要十分谨慎,路径顶点处理这绝逼是性能热点什么的……

反正最近就是这样吧……我大概对MinGW有某种神奇的选项能够智能识别@n和不带@n的符号(就像VC的lib导出库那样的)已经不抱多少希望了……


作者: OwnWaterloo    时间: 2013-12-23 08:41
回复 15# starwing83

别这样。。。 我又没打算执行或者加载这个dll。 只是看看它的内容而已。 反正linux下它也掀不起什么浪。
如果是要实际使用。。。 从这种不太可信的网站下载下来的东西靠谱么?  从Chrome里抓会比较好?

你不是用的MinGW4.7+么? 没有lto? 还是gcc在windows下不支持lto? 记得好像是将它的中间代码写在目标文件的某个位置。 难道只管elf不管pe的?

awk/sed 这类工具。。。 quick and dirty地搞搞原型也许还凑合。。。  要正儿八经地写个程序还是有点困难。。。
如果坚持都用gnu那套也还行。。。 cygwin就是。 msys不记得了。
ubuntu里gawk是要装的。。。  mac os x下的gnu sed还不知道怎么弄。。。   否则默认的mawk和不知道哪来的sed功能很弱。。。

有可以扩展含义的中缀四则运算符其实很幸福。。。
作者: OwnWaterloo    时间: 2013-12-23 09:00
回复 15# starwing83

执行与否与从哪里获取这个dll先不考虑。。。

dll里有些符号是用序号导出的? 那使用这个dll的程序或者库怎么发布?
使用这个dll程序和它附带在一起? 使用这个dll的库。。。 只能让库的使用者去选该用那个libglesv2.dll了吧?

用序号导出不是作死的节奏么。 那6个符号本来就是不打算公开使用还是怎么的?

作者: starwing83    时间: 2013-12-23 21:21
回复 17# OwnWaterloo


    是这样的,ANGLE的DLL是同时发布编号和函数名的……除了六个函数以外。

这六个函数(其实就是类似内部函数神马的),是libGLESv2.dll专门提供给libEGL.dll使用的,因此特意没有导出函数名。使用的时候,编译libGLESv2.dll不是会有exp和lib么,VC的lib里面就有符号和入口点,只是DLL里面没有罢了,VC本身是可以很轻易地链接OK的,这点我试过了。

MinGW因为其DLL的导出符号名字的问题,基本上无解了……而且是很无语的无解方法,有两个符号是__stdcall的,即使是在def文件里面写了NONAME,但是导出库里的符号仍然是没有带@n,然后后果你就懂了……MinGW啊……无语了……

mingw-w64官网的说法是lto还不成熟,我这边能编译带lto支持的MinGW,但是:
1. 据说32位还是64位某一个对lto支持不佳
2. 我用的自己编译的MinGW,如果要lto支持,需要一些其他的库编译进去
3. 需要在运行时增加-flto参数,且(官网说的)很可能导致内存不足问题

总而言之就是不成熟神马的……哎……真正好用的,还是等clang吧。

顺便说一下,拿别人编译的DLL基本上是破产了,所有其他第三方(除了firefox的以外)都试验过了,结果是,只有我自己编译的那个可以工作,其余的,都会出现“EGL无法初始化”的错误……等我有时间了详细调试一下吧。

总的来说就是……真坑爹啊……




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2