__stdcall 调用约定已经产生很长的时间了,一些老的调用约定如__pascal 正被逐渐废弃,而__stdcall 却成为了Win32 API的标准调用约定。和__cdecl (C/C ++默认调用约定)不同,__stdcall 被C/C++、Visual Basic、Java等语言支持,使得它成为跨语言调用的DLL的首选调用约定。
在内部,__cdecl 和__stdcall 函数有一些命名修饰。比如在MSVC (Microsoft Visual C++) 和 MinGW (Minimalistic GNU for Windows) GCC里,__cdecl 函数有一个下划线前缀,__stdcall 函数不仅有一个下划线前缀而且还跟有一个@开头的参数列表字节数后缀。所以double __cdecl sin(double) 内部表示为_sin ,double __stdcall sin(double) 内部表示为_sin@8 。
但是事情还不止这些,当它们应用于DLL或用不同的编译器生成时修饰也会发生变化。下表列出了MSVC、MinGW、Digital Mars C/C++ Compiler (DMC)、Borland C++ Compiler/C++ Builder (BCC)生成的DLL的内部函数命名:
Calling Convention | Internal* | MSVC DLL (w/ DEF) | MSVC DLL (dllexport) | DMC DLL | MinGW DLL | BCC DLL |
__stdcall | _Function @n | Function | _Function @n | _Function @n | Function @n | Function |
__cdecl | _Function | Function | Function | Function | Function | _Function |
* 除了BCC,它们都使用了相同的命名约定
真是混乱!(特别是MSVC里DEF文件和__declspec(dllexport)
属性对命名修饰的影响)! 尽管后缀能清楚地表示出调用函数时有多少字节需要从堆栈中弹出,不过这并不是常见用法。比如提供Win32 API的系统DLL就没有这些修饰。本文剩余部分将专注于MSVC和MinGW的DLL建立和使用,包括相关工具的介绍和使用。(在http://www.bcbdev.com/articles.htm 里有一篇关于在C++Builder中使用DLL的文章,写得不错,所以在这里就不再费话了)。
DEF文件的相关工具
首先,要介绍一下DEF文件格式以及MSVC和MinGW的相关工具。DLL使用中的不少难点就在这里。
DEF文件格式
我们只要关心DEF文件里的两个段:LIBRARY段和EXPORTS段。LIBRARY段指出DLL的内部名,EXPORTS段指出导出的函数或数据。这里有一个小例子:
LIBRARY testdll.dll
EXPORTS
cdeclFunction @1
_stdcallFunction@8 @2
aliasName = cdeclFunction @3
这个DEF文件定义了testdll.dll 的三个输出:第一个是__cdecl 函数,第二个是__stdcall 函数,第三个是第一个函数的别名。这三个函数也被赋于了各自的序号,这样函数可以通过名字或序号被调用。
CL
CL 可以接受一个DEF文件,它只是简单地把这个文件名传给LINK 。如:
cl /LD testdll.obj testdll.def
相当于调用:
link /out:testdll.dll /dll /implib:testdll.lib /def:testdll.def testdll.obj
LINK
LINK 是MSVC处理DLL和DEF文件最重要的工具。上面提及的CL 命令行显示了使用DEF文件建立DLL所使用的选项,主要问题是:如果我们建立DLL时不用DEF文件,那么导出的__stdcall 函数名会是_Function @n 的形式,而使用DEF文件,导出的函数名既可以是Function 也可以是_Function @n 。如果想同时导出两种命名形式(类似于GNU ld 和 dllwrap 的--add-stdcall-alias 选项),我们可以在EXPORTS段里加入下面形式的函数声明(注意顺序不可颠倒):
TestFunction = _TestFunction@4
还有一件事要注意,当我们用LINK 以上面的形式在DLL中导出TestFunction (或其它别名)和_TestFunction@4 时,只有_TestFunction@4 (内部名)会输出到导入库(.lib)中。
LIB
如果我们从其它人那里拿到一个DLL文件(没有源码)并且有对应的DEF文件,那么建立导入库最简单的方法是使用LIB 工具。通常使用下面的语法就已经足够(想了解更多的话请参阅MSDN ):
lib /def:DEF_file
注意 :
- 貌似LIB 不接受别名形式(它会简单地忽略等号后面的部分);
- 它假设所有在DEF文件中的函数是__cdecl ,所以DLL中的每个元素会被映射成含有下划线前缀的内部名字。例如,链接器使用导入库时会试图把DLL中未定义的_Function 分解为Function, 所以一般不用特别在意。
gcc
这里我们通过gcc 来调用ld ,不直接使用ld 的原因只是因为使用gcc 更方便。-shared 选项是专门用于生成DLL的。我们也可以使用-Wl 选项来传递其它链接专用选项。
ld
GNU ld 有不少与DLL有关的选项,不过我们只要关注下面这四个:
--add-stdcall-alias 导出的元素中是否包含@nn
--kill-at 去除导出元素中的@nn
--out-implib <file> 生成导入库
--output-def <file> 为生成的DLL产生对应的.DEF文件
gcc 或者ld 可以在命令行直接接受一个DEF文件。当在EXPORTS段里有下面内容时,
TestFunction = TestFunction@4
两种形式都会被导出。这与稍后讲到的dllwrap 有些不同。
dllwrap
GNU dllwrap 可以按DEF文件生成一个DLL。一般用下面的语句来使用dllwrap
dllwrap --def DEF_file -o DLL_file OBJ_files [--output-lib LIB_file ]
dllwrap 将调用gcc 、ld 和dlltool 来履行这个任务,如果要求dllwrap 生成导入库(--output-lib ),它会让dlltool 来帮忙。和LINK 或ld 不同,当dllwrap 在EXPORTS段遇到下面行时
TestFunction = TestFunction@4
它只导出TestFunction 而没有TestFunction@4 ,并且只有TestFunction 被输出到导入库中(从某种角度讲有点类似于LIB )。
dlltool
GNU dlltool 用于需要使用动态链接库(DLLs)时。下面的选项是值得我们关注的:
-l --output-lib <outname> 生成导入库
-D --dllname <name> 指定用于生成导入库的DLL文件名
-d --input-def <deffile> 读取DEF文件
-U --add-underscore 为导入库里的元素加上下划线前缀
-k --kill-at 去除导出的名称里的@<n>
dlltool 和LIB 类似,不过有它自己的特色:-U 选项把DEF文件中的元素映射到DLL中有下划线前缀的名称上,-k 选项把DEF文件中的元素映射到DLL中有@n 后缀的名称上。
pexports
这是一个独立的开源工具,用于从DLL中生成DEF文件。它不和MSVC或MinGW一起发布,你可以从这里 下载(译者注:我的MinGW上怎么包含这个工具呀?)。
DLL和导入库
学习了上面的一堆工具,我们现在可以做我们想做的事了。这里我们还需要sed 工具(如果你没有,从网上下载一个,译者:MSYS里就有)和一些正则表达式的知识。
Microsoft Visual C++
生成DLL最简单的方法是使用CL 的/LD命令行参数:
cl /LD OBJ_files
生成的DLL将会导出像_MyFunction@8 这样的函数名,与前面“MSVC DLL (no DEF)”列显示的一样。要去除多余的命名修饰,我们必须用DEF文件。下面的命令能从以__declspec(dllexport) 建立的DLL中自动生成一个DEF文件:
link /out:DLL_file /dll OBJ_files
pexports DLL_file | sed "s/^_/([[:alnum:]_]/+/)@[[:digit:]]/+//1/" > DEF_file
上面的sed命令去除导出函数名里的下划线前缀和@n后缀,有了这个DEF文件和目标文件(.obj)以后,用下面的命令重新生成DLL和导入库:
link /out:DLL_file /dll /def:DEF_file /implib:LIB_file OBJ_files
现在你可以随意使用DLL和导入库了。
MinGW GCC
如果我们不用导出除__declspec(dllexport) 以外的函数,可以这样做:
gcc -shared -o DLL_file OBJ_files -Wl,--output-def,DEF_file
gcc -shared -o DLL_file OBJ_files -Wl,--kill-at
dlltool -d DEF_file --dllname DLL_file --output-lib LIB_file --kill-at
如果想使用DEF文件控制导出哪些函数,首先(假设__declspec(dllexport) 声明还在):
gcc -shared -o DLL_file OBJ_files -Wl,--kill-at,--output-def,DEF_file
产生一个DEF文件,内容类似于"Function = Function @n @Ordinal "。按我们的意愿编辑后,按下面的命令搞定剩下的工作:
dllwrap --def DEF_file -o DLL_file OBJ_files
sed "s/[[:alnum:]_]/+ *= *//" DEF_file > New_DEF_file
dlltool -d New_DEF_file --dllname DLL_file --output-lib LIB_file --kill-at
现在导入库已经在你手上了。
2002-8-20, written by Wu Yongwei
2003-3-6, last revised by Wu Yongwei
2009-9-18, 中文翻译 by 毛毛