关于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] 获取批处理参数的路径文件名等

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