本帖最后由 Qter 于 2023-11-18 17:27 编辑
https://zhuanlan.zhihu.com/p/556726543 C/C++以运行高效著称,但其可怜的二进制(ABI Application Binary Interface)兼容性却让人头大。不仅C和C++的代码相互引用会出现二进制兼容问题,甚至C++代码之间相互引用也会出现兼容问题,实际生产中非常让人头大。 本文通过简单示例说明ABI兼容到底是怎么回事儿。 一个简单的示例我们都知道C++可以自由引用C的代码而不出现二进制兼容问题,那么反过来C能否引用C++的代码呢?以下示例通过hello.cpp文件提供了一个say_hello()函数,然后在main.c中试图调用该函数: 请注意两个文件的后缀,一个是.cpp,另一个是.c gcc -c hello.cpp -o hello.ogcc -c main.c -o main.ogcc main.o hello.o # 链接
编译报错说找不到sys_hello()函数,可是该函数分明已经在hello.cpp中定义了啊。我们通过nm命令看一下hello.o中暴露的符号是什么: _Z9say_helloPKc是什么鬼?看起来像是个函数名字,要不调用一下试试看? C程序调用_Z9say_helloPKc对main函数稍作修改,调用_Z9say_helloPKc()试一下? 程序竟然编译和运行成功了! 所以_Z9say_helloPKc这串字符到底是什么?毫无疑问它就是.cpp文件中的say_hello()函数编译之后的名字,这个过程叫做名字修饰(Name Decoration)或名字改编(Name Mangling)。 名字修饰(name decoration),也称为名字重整、名字改编(name mangling),是现代计算机程序设计语言的编译器用于解决由于程序实体的名字必须唯一而导致的问题的一种技术。 为什么C++需要名字修饰?一个很简单的原因是因为C++允许函数重载,同名函数在C语言中会冲突,必须想办法让他们在编译器层面区分开来。当然namespace、类名等在名字修饰中都会有体现。名字修饰发生在编译阶段(李豪:C/C++程序编译过程为什么要分为四个步骤?),既目标文件中的符号已经是修饰之后的。 所以只需要知道修饰后的函数名,就可以通过C语言调用C++的函数,比如say_hello()函数修饰之后的名字是_Z9say_helloPKc()。只可惜名字修饰没有规范,不同编译器的修饰方式不一样,直接调用修饰后的函数名很容易导致二进制不兼容。 可通过c++filt命令查看修饰前的名字,比如
$ c++filt _Z9say_helloPKc
say_hello(char const*) 使用extern "C"关键字使用extern ”C“关键字可以强制C++不做名字修饰,所以如下代码也可以正常工作,事实库函数通常都是这么写的: C++自身的ABI兼容说出来你可能不信,即使是C++自己调动自己也可能出现ABI不兼容,一个明显的改变是C++11前后一些关键类的名字在ABI层面生了改变(比如std::string和std::list),导致C++11之前和之后编译出来的目标文件不兼容! 换句话说如果某个库的提供方使用的是C++11之前的ABI编译的,那么依赖这个库的项目必须也用旧的ABI编译。可通过_GLIBCXX_USE_CXX11_ABI来控制是否使用C++11的ABI,形如: gcc -D_GLIBCXX_USE_CXX11_ABI=1 main.cpp 总结C++的ABI兼容是个让人头大的问题,这对于熟悉Java等跨平台语言的程序员来说兼职不可思议。了解ABI兼容有助于解决实际开发中程序编译和运行时的问题。 参考:《程序员的自我修养——链接、装载与库》
|