C++ ABI

 C++ ABI

 作者俞甲子

http://mscenter.edu.cn/zhuanti/dianziqikan/12/teco4.htm

      C++ 在不停的发展中也经历了很多的变革,但是这些变革导致每当新的变革产生时,人们就需要重新编译所有的已存的源文件来跟现有的系统和 C++ 标准兼容。所以 C++ 一直为人诟病之一的原因是他的二进制模块兼容性不好。不光是不同的编译器编译的二进制代码之间兼容性不好,就连同一个编译器的不同版本之间兼容性也不好。比如我有一个库 A 是公司 Company A Compiler A 编译,我有另外一个库 B 是公司 Company B Compiler Compiler B 编译的,当我想写一个 C++ 程序来同时使用库 A B 将会很是棘手。有人说,那么我每次只要用同一个编译器编译一下所有的源代码就 OK 了,不错,对于小型项目来说这个方法的确可行,但是考虑到几种情况,以上的方法将不再可行:

      库的厂商往往不希望使用者看到库的源代码,所以一般是以二进制的方式提供,这样的话当你的编译器型号和版本与编译库所用的编译器型号和版本不同时,就会产生不兼容。如果让库的厂商提供所有的编译器型号和版本编译出来的库给用户,这根本就不可行。

      以上的情况对于系统中已经存在的静态库或者动态库需要被多个应用程序使用的情况也几乎相同。

      所以人们一直期待着能有统一的 C++ 二进制兼容标准 (C++ ABI) ,这个情况一直延续到 1998 年的 C++ 标准出来后才有所改观。首先我将介绍一下 C++ 产生二进制兼容性的原因,并且我认为在理解 C++ ABI 的同时,将会理解到 C++ 很多内部机制,这对 C++ 程序员来说,无疑是非常有帮助的。

 

      ABI(Application Binary Interface) 的概念其实存在于远古时期,早在有程序的时候,就几乎有了 ABI ,人们希望程序能够在不经任何修改的情况下得到重用,最好的情况是二进制代码能够不加修改地得到重用,人们始终在朝这个方向上努力,早从开始的打孔纸带的纸带库到后来的 C,C++ 等高级语言以及现在的 COM,COBAR 等本质上都是希望重用。虽然至今还未实现完全的二进制重用。为了实现二进制重用,就是要规定二进制的标准,具体的在 C 语言的时候体现在如下几个方面:

  内置类型 ( int, float, char ) 的大小和在存储器中的放置方式 (big-endian, little-endian, 对齐方式等 )

  组合类型 ( struct , union ,数组等 ) 的存储方式

  外部符号( external-linkage )与用户定义的符号之间的命名方式和解析方式 ( 如函数名 func C 中被解析成外部符号 _func )

  函数机器指令级别的调用方式 (stdcall )

  堆栈的分布方式

  寄存器使用约定(哪些寄存器可以修改,哪些需要保存等等)

 

       C++ 的时代,又增加了很多额外的内容,大家可以看到,正是这些内容使 C++ 要做到二进制兼容比 C 来得更为不易:

  继承类体系的内存分布,如 base class, virtual base class

  指向成员的指针 (pointer-to-member) 的内存分布

  隐含的函数参数( this

  如何调用虚函数 (vtable 的内容和分布形式, vtable 指针在 object 中的位置,如何调整 this 指针等 )

  如何得到基类在继承类中的位置

  如何通过指向成员函数的指针 (pointer-to-member) 来调用成员函数

  template 实例的符号编码 (mangling template instance)

  外部符号的编码 (name mangling)

  全局对象的构造和析构

  异常的产生和捕获机制

  标准库的细节问题 (RTTI 支持等 )

  内联函数访问细节

 

      接下来的几个部分将会详细介绍每个项目的细节问题。

 

Name Mangling (名称修饰)

C++ 允许多个函数拥有一样的名字,就是所谓的函数重载,同时也允许多个同样名字的符号在不同的名称空间:

int func(int);

float func(float);

class C {

int func(int);

class C {

int func(int);

};

};

namespace N {

int func(int);

class C {

int func(int);

};

};

 

这段代码中的有 6 个函数叫 func ,只不过他们的返回类型和参数以及所在的名称空间不同。当他们被编译器编译后就变成了 (GCC 4.0.0 )

int func(int) __Z4funci

float func(float) __Z4funcf

int C::func(int) __ZN 1C 4funcEi

int C::C2::func(int) __ZN 1C 2C 24funcEi

int N::func(int ) __ZN1N4funcEi

int N::C::func(int) __ZN1N 1C 4funcEi

 

可以看到不仅是函数名,而且参数和名称空间都被加入了 decorated name( 修饰后名称 ) ,这样就可以区别不同参数和名字空间的函数,而不会导致 link 的时候函数多重定义。但是这个名字的修饰方法不同的编译器有一套不同的标准,比如上面的函数在 VC 7.0 中就成了:

 

int func(int) ?func@@YAHH@Z

float func(float) ?func@@YAMM@Z

int C::func(int) ?func@C@@AAEHH@Z

int C::C2::func(int) ?func@C2@C@@AAEHH@Z

int N::func(int) ?func@N@@YAHH@Z

int N::C::func(int) ?func@C@N@@AAEHH@Z

 

这样不同的名字修饰方法必然会导致 linker 无法正常连接。这里值得一提的是 int func() float func() decorated name 是一样的,这就从一个侧面证明了同样 namespace( 名字空间 ) 相同参数不同返回类型的函数不能成为重载函数,编译器会提示你函数重复定义。

 

继承类体系的内存分布

 

继承是面向对象语言 C++, java 等的基本功能,继承体系的内存分布也是 C++ ABI 的一部分,考虑以下代码:

class B {

};

 

class D : public class B {

int i;

};

 

子类的成员 i 在内存中的分布位置是在 B 的后面还是前面以及对齐方式等一系列问题都应该是 ABI 需要规定的。上面的情况是比较好解决的,一般按照地址从低到高为 B D 的非静态成员变量,如果 D 还被其他类继承的话,后面再放 D 的子类的非静态成员变量。但是考虑到多重继承特别是虚继承:

class B{

};

 

class C1 : virtual public B {

};

 

class C2 : virtual public B {

};

 

class D : public C1, public C2 {

};

 

      这种情况就比较不好办了,因为虚继承是靠 virtual table(vtable) 来实现的, class B D 中只有一个实例,而这个实例的位置如何确定,不同的编译器如果不统一的话必将产生问题。所以 C++ ABI 应该规定类似的继承体系产生的内存分布问题。题外话:一般推荐不要使用多重继承和虚继承,除非万不得已。
原文地址:https://www.cnblogs.com/zhangyunlin/p/6167856.html