原文地址:https://blog.csdn.net/funkri/article/details/8550290
關於VC和GCC中調用C DLL的一些問題
最近在羅雲彬的琢石成器上看到DLL部分,產生了讓GCC和VC互相調用對方產生的DLL的想法。由於C++的函數名改編問題,其dll不具有二進制級別的共享性,也就是說VC和GCC的C++ dll不能混用,但C的可以。我試驗了一下,使用VC和GCC分別生成了一個簡單的DLL然後由對方調用,結果可以成功,另外還順帶完成了一個DLL導出lib和a的腳本,附件有下載。
首先有一些概念:(以下內容來自《函數命名規則及調用約定》)
1、調用約定(Calling Convention)
調用約定是指在程序中實現函數調用的一種協議。協議規定了函數中參數的傳遞方式,參數個數,由誰來處理堆棧等問題,不同的語言使用不同的調用約定。
(1) stdcall調用約定
Pascal程序的缺省調用方式,參數採用從右到左的壓棧方式,被調函數自身在返回前清空堆棧(其實就是在call該函數之前push參數,call完後因爲函數內部處理了所以不需要pop)。WIN32 Api都採用stdcall調用方式。
(2) cdecl調用約定
C/C++的缺省調用方式,參數採用從右到左的壓棧方式,傳送參數的內存棧由調用者維護(即在call該函數之前push參數,call完後必須pop)。cedcl約定的函數只能被C/C++調用。由於cdecl調用方式的參數內存棧由調用者維護,所以變長參數的函數能(也只能)使用這種調用約定。
(3) fastcall調用約定
fastcall調用較快,它通過CPU內部寄存器傳遞參數。
2、名稱改編(Name Mangling)/名稱修飾(Name Decoration)
(1) windows下C的函數名改變規則
stdcall調用約定在輸出函數名前加上一個下劃線前綴,後面加上一個“@”符號和其參數的字節數,格式爲_functionname@number。
cdecl調用約定僅在輸出函數名前加上一個下劃線前綴,格式爲_functionname。
fastcall調用約定在輸出函數名前加上一個“@”符號,後面也是一個“@”符號和其參數的字節數,格式爲@functionname@number。
它們均不改變輸出函數名中的字符大小寫,這和PASCAL調用約定不同,PASCAL約定輸出的函數名無任何修飾且全部大寫。
(2) C++的函數名改編規則
由於C++中有重載函數的存在,所以直接使用C的改編規則就會產生相同名稱的不同函數,從而引發錯誤。C++的改編規則比較複雜,下面表格以幾個函數爲例,列舉了在不同平臺不同編譯器環境下C++的函數名改編示例(來自How different compilers mangle the same functions)。
Compiler | void h(int) | void h(int, char) | void h(void) |
---|---|---|---|
Intel C++ 8.0 for Linux | _Z1hi | _Z1hic | _Z1hv |
HP aC++ A.05.55 IA-64 | _Z1hi | _Z1hic | _Z1hv |
GNU GCC 3.x and 4.x | _Z1hi | _Z1hic | _Z1hv |
HP aC++ A.03.45 PA-RISC | h__Fi | h__Fic | h__Fv |
GNU GCC 2.9x | h__Fi | h__Fic | h__Fv |
Microsoft VC++ v6/v7 | ?h@@YAXH@Z | ?h@@YAXHD@Z | ?h@@YAXXZ |
Digital Mars C++ | ?h@@YAXH@Z | ?h@@YAXHD@Z | ?h@@YAXXZ |
Borland C++ v3.1 | @h$qi | @h$qizc | @h$qv |
OpenVMS C++ V6.5 (ARM mode) | H__XI | H__XIC | H__XV |
OpenVMS C++ V6.5 (ANSI mode) | CXX$__7H__FI0ARG51T | CXX$__7H__FIC26CDH77 | CXX$__7H__FV2CB06E8 |
OpenVMS C++ X7.1 IA-64 | CXX$_Z1HI2DSQ26A | CXX$_Z1HIC2NP3LI4 | CXX$_Z1HV0BCA19V |
SunPro CC | __1cBh6Fi_v_ | __1cBh6Fic_v_ | __1cBh6F_v_ |
Tru64 C++ V6.5 (ARM mode) | h__Xi | h__Xic | h__Xv |
Tru64 C++ V6.5 (ANSI mode) | __7h__Fi | __7h__Fic | __7h__Fv |
Watcom C++ 10.6 | W?h$n(i)v | W?h$n(ia)v | W?h$n()v |
通過上面的介紹大致可以看出VC和GCC編譯C程序的話,只需要注意調用約定就可以,函數名改編是一致的;但編譯C++程序函數名改編就完全不一樣了。
有些人可能會有疑問,爲什麼C++標準委員會不將函數名改編規則做一個規範呢?下面的內容說明了原因(來自Standardised name mangling in
C++)。
Standardised name mangling in C++
While it is a relatively common belief that standardised name mangling in the C++ language would lead to greater interoperability between implementations, this is not really the case. Name mangling is only one of several application binary interface issues
in a C++ implementation. Other ABI issues like exception handling, virtual table layout, structure padding, etc. cause differing C++ implementations to be incompatible. Further, requiring a particular form of mangling would cause issues for systems where implementation
limits (e.g. length of symbols) dictate a particular mangling scheme. A standardised requirement for name mangling would also prevent an implementation where mangling was not required at all — for example, a linker which understood the C++ language.
The C++ standard therefore does not attempt to standardise name mangling. On the contrary, the Annotated C++ Reference Manual (also known as ARM, ISBN 0-201-51459-1, section 7.2.1c) actively encourages the use of different mangling schemes to prevent linking
when other aspects of the ABI, such as exception handling and virtual table layout, are incompatible.
Real-world effects of C++ name mangling
Because C++ symbols are routinely exported from DLL and shared object files, the name mangling scheme is not merely a compiler-internal matter. Different compilers (or different versions of the same compiler, in many cases) produce such binaries under different
name decoration schemes, meaning that symbols are frequently unresolved if the compilers used to create the library and the program using it employed different schemes. For example, if a system with multiple C++ compilers installed (e.g. GNU GCC and the OS
vendor’s compiler) wished to install the Boost C++ Libraries, it would have to be compiled twice — once for the vendor compiler and once for GCC.
It is good for safety purposes that compilers producing incompatible object codes (codes based on different ABIs, regarding e.g. classes and exceptions) use different name mangling schemes. This guarantees that these incompatibilities are detected at the linking
phase, not when executing the software (which could lead to obscure bugs and serious stability issues).
For this reason name decoration is an important aspect of any C++-related ABI.
然後回到開頭的問題。使用VC和GCC分別生成了一個簡單的DLL然後由對方調用。
CallingConvention.h:
#ifndef CALLINGCONVENTION_HEADER_FILE#define CALLINGCONVENTION_HEADER_FILE |
TestDll.h:
#ifndef TESTDLL_HEADER_FILE #define TESTDLL_HEADER_FILE #include "CallingConvention.h" #ifdef __cplusplus extern "C" { #endif EXPORT int PRE_CDECL GetSquare(int x) POST_CDECL; #ifdef __cplusplus } #endif #endif /* TESTDLL_HEADER_FILE */ |
添加類
#ifndef TESTDLL_HEADER_FILE
class CA |
TestDll.c:
添加類
CA a; }
int CA::Set(int x)
#ifdef __cplusplus #endif |
簡單說明下,CallingConvention.h是應對不同編譯器定義的宏,定義了__GNUC__表示使用GCC編譯,否則使用VC。還有個BUILD_DLL,這個宏在編譯DLL時需要加上,用來和調用區分。TestDll.h中的extern “C”表示使用C函數名改編規則,在C++調用C庫時需要用到,但如果只是C調用這個庫,必須去掉extern “C”,否則會出錯(所以使用__cplusplus條件編譯)。TestDll.c中的DllMain函數是DLL的入口函數,windows中必須有(linux的so不需要有),這裏簡單的返回了1(TRUE)。這個Dll只有一個GetSquare函數,用來計算輸入值的平方。
其次是調用該DLL的測試程序。
Test.c:
#include <stdio.h> #include "TestDll.h" int main() { printf("5^2=[%d]\n", GetSquare(5)); getchar(); return 0; } |
Makefile:
CL=cl GCC=gcc ML=ml LINK=link RC=rc NASM=nasm RM=del VC8_PATH=D:/Program Files/Microsoft Visual Studio 8/VC VC10_PATH=D:/Program Files/Microsoft Visual Studio 10.0/VC INCLUDE=/I"$(VC10_PATH)/include" LIBS=/LIBPATH:"$(VC10_PATH)/lib" /LIBPATH:"$(VC8_PATH)/lib" all: gcctest vctest libso: TestDll_gcc.dll TestDll_vc.dll TestDll_gcc.dll: $(GCC) -c -DBUILD_DLL TestDll.c $(GCC) -fPIC -shared -o TestDll_gcc.dll TestDll.o DllExporter.bat TestDll_gcc.dll TestDll_vc.dll: $(CL) -c -DBUILD_DLL $(INCLUDE) TestDll.c $(LINK) $(LIBS) /out:TestDll_vc.dll /dll TestDll.obj DllExporter.bat TestDll_vc.dll gcctest: $(GCC) -c Test.c $(GCC) -o gcctest_gccdll.exe Test.o -L. -lTestDll_gcc $(GCC) -o gcctest_vcdll.exe Test.o -L. -lTestDll_vc vctest: $(CL) -c $(INCLUDE) Test.c $(LINK) /subsystem:console $(LIBS) /out:vctest_gccdll.exe TestDll_gcc.lib Test.obj $(LINK) /subsystem:console $(LIBS) /out:vctest_vcdll.exe TestDll_vc.lib Test.obj clean: $(RM) *.obj $(RM) *.o $(RM) *.res |
這裏面之所以要設置VC8_PATH和VC10_PATH,因爲我在VC10_PATH中找不到kernel32.lib,我也不明白爲什麼。
編譯時運行make libso,會生成TestDll_gcc.dll(GCC編譯出的DLL)和TestDll_vc.dll(vc編譯出的DLL)及響應的lib和a文件。
再運行make all,會生成gcctest_gccdll.exe(GCC調GCC DLL),gcctest_vcdll.exe(GCC調VC DLL),vctest_gccdll.exe(VC調GCC DLL),vctest_vcdll.exe(VC調VC DLL)四個可執行文件。輸出正常,結果相同。
另外DllExporter.bat就是我寫的用來從DLL導出lib(a)的批處理文件(參考《用VC和MinGW導出dll的def和lib(a)文件》,話說MS的批處理語法還真是怪異啊。。),內容如下。
DllExpoter.bat:
@echo off SET DllExporter_path=%~dp0 SET exec_pexports=%DllExporter_path%tools\pexports.exe SET exec_dlltool=%DllExporter_path%tools\dlltool.exe SET exec_lib=%DllExporter_path%tools\lib.exe SET local_path=%~dp1 SET export_dll_name=%~n1 SET export_file_ext=%~x1 if "%~1" EQU "" ( echo Usage: %~nx0 [ filename ] goto :END ) if NOT EXIST "%~1" ( echo Error: file[%1] not exist goto :END ) if "%export_dll_name%" EQU "" ( echo Error: filename cannot be null goto :END ) if "%export_file_ext%" NEQ ".dll" ( echo Error: file type[%export_file_ext%] should be [.dll] goto :END ) echo ----- export .def file ----- "%exec_pexports%" "%local_path%%export_dll_name%.dll" > "%local_path%%export_dll_name%.def" echo ----- export .a file for MinGW ----- "%exec_dlltool%" --dllname "%local_path%%export_dll_name%.dll" --input-def "%local_path%%export_dll_name%.def" --output-lib "%local_path%lib%export_dll_name%.a" echo ----- export .lib file for VC ----- "%exec_lib%" /machine:i386 /def:"%local_path%%export_dll_name%.def" /out:"%local_path%%export_dll_name%.lib" del "%local_path%%export_dll_name%.def" "%local_path%%export_dll_name%.exp" :END PAUSE |
具體使用請下載附件。
附件中還包含了腳本調用的工具,分別是:
pexports.exe:MinGW工具,通過DLL生成DEF文件
dlltool.exe:MinGW工具,通過DLL和DEF文件生成MinGW鏈接用的a文件
as.exe:dlltool需要調用的程序
lib.exe:MinGW工具,通過DLL和DEF文件生成VC鏈接用的lib文件
腳本的使用方法,可以如上面Makefile中一樣調用,或者直接將dll文件拖動到腳本圖標上。結果是會在dll文件所在目錄生成相應的lib文件和a文件。
參考資料:
[1] 《PC彙編語言》(作者Paul A. Carter)4.5節 調用約定
[2] 函數命名規則及調用約定
[3] WIKIPEDIA Name mangling
[4] Linux平臺gcc和動態共享庫的基礎知識
[5] 用VC和MinGW導出dll的def和lib(a)文件
[6] 編寫簡單DLL及CL編譯鏈接
[7] GCC編譯dll,Java調用dll
[8] 批處理set命令變量字符截取點點通
[9] 獲取批處理參數的路徑文件名等