雷蛇鼠标宏怎么录制:[原]也来谈谈extern "C"

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 02:49:54

基础知识: ABI 即application binary interface ,ABI即定义了应用程序和二进制数据之间的接口.

通俗的讲,我用Pascal编译出的obj,是否可以被C编译器链接到呢? 答案是可以的.如何保证链接时能够链接到正确的地址呢.这就需要ABI了,但是,头文件要对调用方式做出声明,让编译器知道,该如何操作来完成这个函数的调用.事实上,Win32的API,大多数都是不是C语言的调用规则,而那个宏 WINAPI,实际上是对调用规则的修饰,说明一个API函数是其他的调用规则.

至于stdcall,fastcall和cdecl这些调用规则,有兴趣的可以自己搜索下.

比如笔者曾经做过汇编和C语言的混合编程,可以使C调用汇编写的函数,也可以反过来,汇编调用C的函数.这是跟ABI分不开的.

比如C的符号命名规则是,前面加下划线,比如main函数,在编译以后,为了使runtime 找到这个main入口,生成的代码中,有这么一个符号_main ,同样,变量和C的库函数都是.
具体可参看: C语言和汇编的混合编程 以及: 解析main函数

注意这里的命名规则是生成的二进制代码中包含的链接符号的命名规则.这些符号为了保证链接正确,就有一定的规则.
ABI除了命名规则,还要说得就是函数的调用规则,比如C的规则是,调用者负责清理堆栈帧,平衡ESP,参数是从右至左逐个压入堆栈.由于是调用者维护栈,所以理论上调用者可以压入任意多个参数,这也是为什么C语言支持可变参数(var arg)的原因.
而返回值的规则,据我个人了解,是用寄存器eax作为返回值,浮点数就是浮点寄存器栈顶.

接着上面的讲,C为什么不支持函数重载呢?因为C的ABI定义了,函数的二进制符号是函数名前加下划线.比如int foo(int a); 和 void foo(char* a),他们生成的都是_foo这个符号,产生了歧义.链接的时候,有两个这样的二进制符号,该怎么选呢...呵呵
那为什么C++可以呢?以为C++规定了二进制符号里面除了函数名称以外,还需要附加参数信息,例如_foo_i 和_foo_s(这个没有标准,看下文的表格),这样,这个问题就解决了.

好了,说了这么多废话,开始入正题,extern "C"代表什么呢?

C++是兼容C的,但C++生成的二进制符号,为了支持函数重载,所以默认加上了其他信息.那么,我用C++写的普通函数,其二进制已经不是C的标准命名了(_foo_i 而不是_foo了),这个时候怎么才能让C的编译器在链接的时候找得到呢?
这就是extern "C"的作用,它告诉C++编译器,要按C的ABI来生成二进制数据.这是站在被调用(链接)方的角度来说的.
反过来如果调用方是C++,而被调用方是C呢,同样,C编译出的lib或者obj,C++编译器会按照它自己的默认方式生成外部链接符号(如_foo_i),链接的时候会找不到这些函数(因为外部是C的_foo而不是_foo_i),而加上了extern "C"之后,C++(编译器)就知道,要链接的外部符号,是C语言生成的二进制数据,这样在编译时,生成的对外部符号的引用,都是符合C标准的(_foo).这个过程是在编译期完成的,而不是链接期.

自此,问题告一段落,extern "C"其实跟 _stdcall 这种修饰类似,为了保证不同语言之间在二进制的兼容.所以,一个函数加上了_cdecl 修饰,跟extern "C"的效果一样.唯一不同的是,extern "C"是C++标准支持的,而_cdecl(带下划线的关键字)是不同的编译器指定的.

.然而事实上,不幸的是,C++没有自己标准的统一的ABI,全靠不同编译器实现自己的规则.对于重载函数,不同编译器的命名方法也不一样.

类似的,this调用也没有标准,MSVC的做法想必大家都知道,用寄存器ecx作为this参数.而g++的做法貌似不同.
类似还有v-table的实现,以及C++异常捕获,这些跟具体的编译器和OS平台有关系,而没有统一的ABI规则.


如下表,不同的编译器符号命名.摘自维基: (http://en.wikipedia.org/wiki/Name_mangling)

Compilervoid h(int)void h(int, char)void h(void)Intel C++ 8.0 for Linux_Z1hi_Z1hic_Z1hvHP aC++ A.05.55 IA-64_Z1hi_Z1hic_Z1hvGNU GCC 3.x and 4.x_Z1hi_Z1hic_Z1hvHP aC++ A.03.45 PA-RISCh__Fih__Fich__FvGNU GCC 2.9xh__Fih__Fich__FvMicrosoft VC++ v6/v7?h@@YAXH@Z?h@@YAXHD@Z?h@@YAXXZDigital Mars C++?h@@YAXH@Z?h@@YAXHD@Z?h@@YAXXZBorland C++ v3.1@h$qi@h$qizc@h$qvOpenVMS C++ V6.5 (ARM mode)H__XIH__XICH__XVOpenVMS C++ V6.5 (ANSI mode)CXX$__7H__FI0ARG51TCXX$__7H__FIC26CDH77CXX$__7H__FV2CB06E8OpenVMS C++ X7.1 IA-64CXX$_Z1HI2DSQ26ACXX$_Z1HIC2NP3LI4CXX$_Z1HV0BCA19VSunPro CC__1cBh6Fi_v___1cBh6Fic_v___1cBh6F_v_Tru64 C++ V6.5 (ARM mode)h__Xih__Xich__XvTru64 C++ V6.5 (ANSI mode)__7h__Fi__7h__Fic__7h__FvWatcom C++ 10.6W?h$n(i)vW?h$n(ia)vW?h$n()v

比如,笔者在工作时遇到一个诡异的问题,gnu的g++,在对变量符号的修饰上做得不够,导致一个严重的问题:

g++在对变量命名上,跟C一样,也是加下划线,于是乎,类型检查在链接时无法保证.
//a.cpp
classType obja;
//b.cpp
extern int obja;
如上例所示,a.cpp定义了一个对象obja,b.pp声明了一个外部变量,但是类型错了,但是按C的命名规则,链接没有问题...这样导致的后果可想而知.

而MS的C++编译器,在这个问题上,就处理得很好,它为变量也生成了二进制的类型信息,这样在链接的时候,如果时这种情况,肯定链接报错,而且能说出什么类型的变量找不到.

顺便说一下,如何避免上例中的问题,这个在C语言世界里,依旧是个潜在的隐患,解决方法其实很简单,要有统一的头文件,而避免半路程咬金式的外部引入,这样能保证全局使用的变量类型一致.我工作中见过类似这样的代码:
void foo()
{
extern int i;
}
//幸亏是msvc,这样做没有大问题.不知道别的编译器了,就像g++,万一类型错了,照样可以链接.很难找到原因的.

好了,不说了,今天主要说的是静态链接相关的内容,有空了说说个人对动态链接和COM的理解.