關於VC和GCC中調用C DLL的一些問題

原文地址: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
#if defined(__GNUC__)
#  define PRE_CDECL
#  define POST_CDECL __attribute__((cdecl))
#  define PRE_STDCALL
#  define POST_STDCALL __attribute__((stdcall))
#  define PRE_FASTCALL
#  define POST_FASTCALL __attribute__((fastcall))
#  ifdef BUILD_DLL /* DLL export */ 
#    define EXPORT __declspec(dllexport)
#  else /* DLL import */
#    define EXPORT __declspec(dllimport)
#  endif /* BUILD_DLL */  
#else
#  define PRE_CDECL __cdecl
#  define POST_CDECL
#  define PRE_STDCALL __stdcall
#  define POST_STDCALL
#  define PRE_FASTCALL __fastcall
#  define POST_FASTCALL
#  ifdef BUILD_DLL /* DLL export */ 
#    define EXPORT _declspec(dllexport)
#  else /* DLL import */
#    define EXPORT _declspec(dllimport)
#  endif /* BUILD_DLL */  
#endif /* __GNUC__ */
#endif /* 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
#define TESTDLL_HEADER_FILE
#include "CallingConvention.h"
#ifdef __cplusplus
extern "C" {
#endif
EXPORT int PRE_CDECL GetSquare(int x) POST_CDECL;

class CA
 {
 public:
  CA();
  ~CA();
  int Set(int x);
 };

#ifdef __cplusplus
}
#endif
#endif /* TESTDLL_HEADER_FILE */


TestDll.c:

添加類
#include "TestDll.h"
int PRE_STDCALL
DllMain()
{
    return 1;
}
EXPORT int PRE_CDECL 
GetSquare(int x)
{

    CA a;
    return a.Set(5);
}

#ifdef __cplusplus
extern "C" {
#endif
CA::CA()
{
 
}
CA::~CA()
{

}

int CA::Set(int x)
{
 return x * 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] 獲取批處理參數的路徑文件名等

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章