C++的一些语言特性使之必须由编译器和链接器共同支持才能完成工作。最主要的有两个方面,其一,C++的重复代码的消除;其二,全局构造与析构。此外,由于C++的各种特性,比如虚函数、函数重载、继承、异常等,使得C++背后的数据结构异常复杂。而且最为不幸的是,这些数据结构往往在不同的编译器和链接器之间不能相互通用,使得C++程序的二进制兼容性成为一个难题。本篇博客将结合项目经验初步讨论C++程序的二进制兼容性问题。

1.C++中重复代码是如何消除的?

C++编译器在很多时候会产生重复的代码,比如模板(Templates)、外部内联函数(External Inline Function)和虚函数表(Virtual Function Table)都有可能在不同的编译单元里生成相同的代码。最简单的情况就是拿模板来说事,模板从本质上来讲很像宏,当模板在一个编译单元里被实例化时,他并不知道自己是否在别的编译单元也被实例化了。所以当一个模板在多个编译单元同时实例化成相同的类型的时候,必然会生成重复的代码。当然,最简单的方式就是不管这些,就将这些重复的代码都保留下来。但这会存在以下几个问题:

1.空间浪费。可以想象一个由好几百个编译单元的工程同时实例化了许多个模板,最后链接的时候必须将这些重复的代码消除掉,否则最终程序的大小坑定会膨胀的很厉害。

2.地址容易出错。有可能两个指向同一个函数的指针会不想等。

3.指令运行效率低。因为现代的CPU都会对指令和数据进行缓存,如果同样一份指令有多个副本,那么指令Cache的命中率就会降低。

一个比较有效的做法就是将每个模板的示例代码都单独地存放在一个段里,每个段只包含一个模板实例。比如有个模板函数是add<T>(),某个编译单元以int类型【add<int>()】和float类型【add<float>()】实例化了该模板函数,那么该编译单元的目标文件中就包含了两个该模板实例的段,为了简单起见,我们假设这两个段的名字分别叫.temp.add.<int>和 .temp.add<float>。这样,当别的编译单元也以int和float类型实例化该模板函数后,也会生成相同的名字,这样链接器在最终链接的时候可以区分这些相同的模板实例段,然后将他们合并入最后的代码段。

这种重复代码消除对于模板来说是这样的,对于外部的内联函数和虚函数表的做法也类似。

函数级别链接

由于现在的程序和库通常来讲都非常庞大,一个目标文件可能包含成百上千个函数或变量。当我们需要用到某个目标文件中的任意一个函数或变量时,就必须把这个文件整个的链接起来,也就是说那些没有用到的函数也被一起链接了进来。这样导致的直接结果就是:链接输出文件的输出结果会变得相当的庞大,所有用到到没用到的变量和函数都一起塞到了输出文件中。

无论是VC++编译器还是GCC编译器都提供了类似模板代码冗余消除的方法!具体为:让所有的函数氮素保存到一个段里面,当连接器需要用到某个函数的时候,就会将他直接合并到输出文件中,对于那些没有用到的函数进行舍弃。该方法很大程度上减小了输出文件的长度,减少了空间浪费。

2.全局的构造函数与析构函数是如何执行的?

我们知道一般一个C++/C程序是从main函数开始执行的,随着main函数的结束而结束。然而,其实在main函数被调用之前,为了保证程序可以顺利地进行,首先要初始化进程执行环境,比如堆分配初始化(malloc/free)、线程子系统等,C++全局对象的构造函数就是在该时期被执行的。【实际上C++的全局对象的构造函数在main之前被执行,C++全局对象的析构函数在main之后被执行】

根据前面的情境,我们能够发现,对于某些场合,程序的一些特定的操作必须在main函数之前被执行,还有一些操作必须在main之后进行执行。当然啦,我最习惯举得例子就是,C++中全局对象的构造与析构的过程。为了满足这一特殊情况,linux系统下的ELF文件结构还定义了两种特殊的段:

.init: 该段里面保存的是可执行指令,他构成了进程的初始化代码。在main函数之前被调用。

.fini: 该字段保存着进程终止代码指令,在main函数执行结束之后被调用。

3.C++的兼容问题及跨平台编程初步

既然每个编译器都能将源代码编译成目标文件,那么不同编译器编译出来的目标文件可以相互链接吗?比如说:MSVC编译出来的目标文件和GCC编译出来的目标文件能否链接在一起,从而形成可执行文件呢?

对于上面这些问题,首先可以想到的是,如果要将两个不同编译器编译出来的编译结果链接到一起,那么,链接器必须同时支持这两个编译器产生的目标文件格式。比如说,MSVC编译器的目标文件是PE/COFF格式,而GCC编译的结果是ELF格式的。只有连接器同时认识这两个格式才行,否则肯定没戏!那么链接器是不是只要同时认识目标文件的格式就可以了呢?不见得!!!

事情绝对没有这么简单,如果我们要使两个编译器编译出来的目标文件能够相互链接,那这两个/多个目标文件必须满足下面的几个条件:

1.采用相同的目标文件格式;

2.拥有同样的函数符号修饰标准;

3.变量的内存分布方式相同;

4.函数的调用方式相同。

其中,国际惯例,把符号修饰标准、变量内存分布布局、函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI(Application Binary Interface)。

C++一直为人诟病的一大原因就在于他的二进制兼容性不好!或者说,比起C语言来说更不容易。一方面,不同的编译器编译出来的二进制代码之间无法相互兼容,甚至有时候,连同一个编译器的不同版本之间的兼容性也不好。最普通的例子:我有一个库A是Company A 用Compiler A编译的;还有一个库B是Company A 用Compiler B编译的,当我写一个C++程序来同时使用库A和库B是,就会相当的棘手!!!我的朋友曾和我说过,只要我可以把一个开源项目的所有源代码在同一个编译环境下跑一遍,那不就OK了?!当然,这对于小型项目而言是容易可行的,但是考虑到一些大型程序,逐一进行编译明显是不合理的,这从某种意义上来讲,就造成软件开发的效率低下。

此外,很多时候,库厂商往往不希望用户看到哭的源代码,所以一般都会以二进制的方式提供给用户。这样就会带来一个问题,如果库厂商提供产品的编译器型号和版本编译器型号不同时,代码基本上就被判了死刑。当然,这个问题不仅仅存在用户和库厂商之间;当一个程序由多个部门或多个公司联合开发时,类似的问题更明显。

所以绝大多数程序员一直期待能有统一的C++二进制兼容标准(C++ ABI),诸多的团体以及社区都在致力于C++ ABI标准的统一,但是前景非常的不乐观。基本形成了以微软的Visual C++和GNU的GCC为首的两大派系。

4.难兄难弟——API与ABI

很多时候,我们都会碰到这两个词,他们长得太像了,仅仅一字之差。API = Application Programming Interface; ABI = Application Binary Interface,实际上他们都是应用程序接口。只是他们所描述的接口所在的层面不一样。API往往是指源代码的应用程序接口;而ABI是指二进制层面的接口,所以ABI的兼容程度要比API的兼容性严格得多。比如:我们可以说C++的对象内存分布(object Memory Layout)是C++ ABI的一部分。相比之下API更关注源代码层面。
ABI的概念相比较之下历史更悠久,因为每一个程序员都希望在不经过任何修改的情况下,重新利用辛辛苦苦写的程序!如果可以,最好的情况就是,二进制的指令和数据还能够不加修改地得到重用,但是由于MS和GNU恶性竞争的原因,二进制级别的重用任重而道远。
下面是百度给的一段理解:
正式因为这些挑战的存在,才有了诸君存在的价值,开源社区算是MS和GNU之外的第三股力量,希望某天开源社区能够一统ABI,这样广大程序猴们就能够去做更有意义的事了......

静态链接中的那点事儿(2):C++二进制兼容性及跨平台初步相关推荐

  1. 静态链接中的那点事儿(1)

    作为一个程序员或者说C++程序开发人员,想必对ELF目标文件从整体轮廓到某些局部的细节都非常熟知.该系列帖子主要为了解决一个疑惑:当我们有多个目标文件时,如何将它们连接起来形成一个可执行文件?这个过程 ...

  2. 动态链接库dll,静态链接库lib, 导入库lib

    目前以lib后缀的库有两种,一种为静态链接库(Static Libary,以下简称"静态库"),另一种为动态连接库(DLL,以下简称"动态库")的导入库(Imp ...

  3. linux gcc 静态编译,GCC 程序编译的静态链接和动态链接

    (给Linux爱好者加星标,提升Linux技能)转自:Mr_Bluyee 在链接阶段中,所有对应于源文件的 .o 文件.'-l' 选项指定的库文件.无法识别的文件名(包括指定的.o目标文件和.a库文件 ...

  4. 动态链接库dll,静态链接库lib, 导入库lib 转

    动态链接库dll,静态链接库lib, 导入库lib 在用VS编译工程的时候,我们会选择动态链接库dll,静态链接库lib(static library),可是为什么在编译动态链接库的时候也可以指定输出 ...

  5. libcurl linux 静态链接库_GCC 程序编译的静态链接和动态链接

    转自:Mr_Bluyee 在链接阶段中,所有对应于源文件的 .o 文件."-l" 选项指定的库文件.无法识别的文件名(包括指定的.o目标文件和.a库文件)按命令行中的顺序传递给链接 ...

  6. 动态连接库和静态链接库

    文章出处:http://www.cnblogs.com/gaoyihan/p/4723332.html 本文参考了以下博客:      1. http://blog.csdn.net/gamecrea ...

  7. 静态链接库(LIB)和动态链接库(DLL),DLL的静态加载和动态加载,两种LIB文件。

    静态链接库(LIB)和动态链接库(DLL),DLL的静态加载和动态加载,两种LIB文件. 一. 静态链接库(LIB,也简称"静态库")与动态链接库(DLL,也简称"动态库 ...

  8. “.dll .obj .lib”和“ .so .o .a”文件与动态链接和静态链接

    ".dll .obj .lib"和" .so .o .a"文件 (1) .dll .obj .lib使用在windows平台下. .dll:动态链接库,作为共享 ...

  9. 静态链接库与动态链接库区别

    一.        静态链接库与动态链接库区别 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了.但是若使 ...

最新文章

  1. php100例代码教程,php实例代码_php用户登录实例教程代码
  2. python结束线程池正在运行的线程_python之线程与线程池
  3. qc linux mysql 安装教程_linux下安装mysql
  4. 【转】使用Eclipse的代码追踪功能
  5. SAP Spartacus B2B OrgUnit 和 OrgUser 的路由映射差异比较
  6. 阿里架构师教你处理高并发:2种方法,解决Redis和Mysql一致性
  7. MSN登陆以后没有响应处理方法
  8. 云主机搭建linux web,linux云主机web服务的搭建和配置
  9. oracle参数错误,解决oracle参数系统文件出错
  10. DPlayer开发弹幕后台
  11. Redis设计与实现-监视器
  12. (1)C# 创建控制台应用程序
  13. Docker 教程:如何将Helix QAC作为容器创建并运行 上
  14. 163VIP邮箱注册,163邮箱使用评价
  15. JavaScript 鼠标事件
  16. 推荐一个视频网站-播布客
  17. 好心情:跌落凡间的星星天使,爱终将弥补一切
  18. Mysql数据库视频教程
  19. 转载:徐家骏:我在华为工作十年的感悟
  20. 中专计算机寒假作业,中职计算机寒假作业[优质课资]

热门文章

  1. Errors running builder 'DeploymentBuilder' on project '工程名'
  2. 看文艺青年怎么玩微信客户端
  3. 树的更多相关算法-3
  4. 《Head First Python》第五章--理解数据
  5. 动态规划之矩阵连乘讲解
  6. Golang之 ==和deepEqual
  7. Mysql 连接查询
  8. 岭回归——减少过拟合问题
  9. 杂项-权限管理:RBAC
  10. 闭包引起的onclick不起作用