最近项目使用的C++的版本到C++11了,但是由于有些静态库(.a)没有源码,因此链接时还在使用非C++11版本的库文件。目前跑了几天,似乎是没出什么问题,但是我还是想说一下这样做有哪些潜在的风险。

首先需要说明的是,升级到C++11之后,部分std的数据结构的内存布局有可能发生改变(待考究)。最开始,我认为只要静态库暴露出来的接口没有使用这些不兼容的数据结构即可。也就是说,如果静态库暴露的所有接口都是纯C风格的,没有使用任何C++ std的数据结构,则链接这种静态库应该是安全的。但是后来发现似乎并不是这样...

来看看C++消除重复代码的机制

假设有一个模板类MyClass<T>定义在头文件my_template.h中。源文件x.cpp和y.cpp都包含了这个头文件,并且用类型int实例化了这个模板类。由于在编译时这两个源文件是完全独立的,因此两个源文件生成的object file(x.o和y.o)中都会包含了MyClass<int>的代码。但是实际上对应一个可执行的程序来说,MyClass<int>的代码存在一份即可。因此在链接阶段会有一个重复代码消除的步骤,回到上述例子就是x.o和y.o里的MyClass<int>的代码会被合并,最后在可执行程序里仅存在一份。

Linux GCC通过ELF的COMDAT section来实现消除重复的模板代码。COMDAT是一种特殊的section(ELF和COFF都有COMDAT section的概念),通常它会关联一个字符串(也可能就是section的名字)。链接器在处理object file时,对遇到的同名的section会执行去重操作,保证在输出的output file中仅存在一份实例。

看看下面的代码

// my_template.h

template <typename T>
class MyClass
{
public: void func1()
{
i1 = ;
i2 = ;
} public: #ifdef TEST
int i1;
int i2;
#else
int i2;
int i1;
#endif
};

上述代码定义了模板类MyClass<T>,并且其内存布局依赖于一个TEST宏。然后我们这样去使用它:

// x.cpp

#include <stdio.h>

#include "my_template.h"

void func_in_x()
{
MyClass<int> c;
c.func1(); printf("i1=%d, i2=%d\n", c.i1, c.i2);
} // y.cpp #include <stdio.h> #define TEST
#include "my_template.h" void func_in_y()
{
MyClass<int> c;
c.func1(); printf("i1=%d, i2=%d\n", c.i1, c.i2);
}

可以看到,在x.cpp中没有定义宏TEST,而在y.cpp中定义了宏TEST。因此这两个编译单元看到的MyClass<int>的内存布局应该是不一样的。

objdump -S x.o 看到成员函数func1的代码是这样的:

 <_ZN7MyClassIiE5func1Ev>:
template <typename T>
class MyClass
{
public: void func1()
: push %rbp
: e5 mov %rsp,%rbp
: 7d f8 mov %rdi,-0x8(%rbp)
{
i1 = ;
: 8b f8 mov -0x8(%rbp),%rax
c: c7 movl $0x1,0x4(%rax)
i2 = ;
: 8b f8 mov -0x8(%rbp),%rax
: c7 movl $0x2,(%rax)
}
1d: nope: 5d pop %rbp
1f: c3 retq

objdump -S y.o 看到成员函数func1的代码是这样的:

 <_ZN7MyClassIiE5func1Ev>:
template <typename T>
class MyClass
{
public: void func1()
: push %rbp
: e5 mov %rsp,%rbp
: 7d f8 mov %rdi,-0x8(%rbp)
{
i1 = ;
: 8b f8 mov -0x8(%rbp),%rax
c: c7 movl $0x1,(%rax)
i2 = ;
: 8b f8 mov -0x8(%rbp),%rax
: c7 movl $0x2,0x4(%rax)
}
1d: nope: 5d pop %rbp
1f: c3 retq

看上述生成的汇编代码可知,MyClass<int>的内存布局确实不一样。在x.o中成员i1的起始地址在对象内存的第4个字节处(偏移是0x4),而在y.o中成员i1的起始地址就是对象的地址(偏移是0x0)。

主函数代码如下:

void func_in_x();
void func_in_y(); int main()
{
func_in_x();
func_in_y();
return ;
}

运行程序,发现结果如下:

$ ./a.out
i1=, i2=
i1=, i2=

虽然我们在x.cpp和y.cpp都是对i1赋值为1,对i2赋值为2。但是却出现了一个i1值为2,i2值为1的结果。

objdump -S a.out 发现func1的代码如下:

 <_ZN7MyClassIiE5func1Ev>:
template <typename T>
class MyClass
{
public: void func1()
: push %rbp
: e5 mov %rsp,%rbp
: 7d f8 mov %rdi,-0x8(%rbp)
{
i1 = ;
71a: 8b f8 mov -0x8(%rbp),%rax
71e: c7 movl $0x1,0x4(%rax)
i2 = ;
: 8b f8 mov -0x8(%rbp),%rax
: c7 movl $0x2,(%rax)
}
72f: nop
: 5d pop %rbp
: c3 retq

可以发现,到了可执行程序中确实仅有一份MyClass<int>的代码,并且是用了x.cpp的那一份。因此在x.cpp中输出的结果是对的,在y.cpp中输出的结果确是相反的。

改变一下链接顺序,这次我们让链接器先处理y.o再处理x.o。再次运行程序,结果如下:

$ g++ y.o x.o main.o
$ ./a.out
i1=, i2=
i1=, i2=

这次变成x.cpp中输出是错的,y.cpp中输出是对的了。

从上述例子可知:链接器在对COMDAT section去重时,并没有辨别section的内容是否一致。因此在不同的object file中内存布局不一致的C++模板被链接到一起是有潜在的风险的。静态库(.a)文件实际上是单纯的object file集合再加上一个符号表,因此链接静态库(.a)文件情况和链接object file是一样的。而动态库(.so)的情况可能稍有不同。

因此剩下的问题就是,使用不同C++标准去编译std的数据结构会生成同名COMDAT section吗?

以vector<int>::push_back函数为例:

$ g++ -std=c++ x.cpp -c
$ readelf -g x.o | grep push_back
COMDAT group section [ ] `.group' [_ZNSt6vectorIiSaIiEE9push_backERKi] contains 2 sections:
[ ] .text._ZNSt6vectorIiSaIiEE9push_backERKi
[ ] .rela.text._ZNSt6vectorIiSaIiEE9push_backERKi $ g++ -std=c++ x.cpp -c
$ readelf -g x.o | grep push_back
COMDAT group section [ ] `.group' [_ZNSt6vectorIiSaIiEE9push_backEOi] contains 2 sections:
[ ] .text._ZNSt6vectorIiSaIiEE9push_backEOi
[ ] .rela.text._ZNSt6vectorIiSaIiEE9push_backEOi

发现为vector<int>::push_back生成的COMDAT section确实是不同名字的。但是在没有彻底弄清楚这个命名规则(mangling)之前,也仅能说明的是vector<int>::push_back这个函数是没有问题,不代表其它的情况。

还有一些问题尚未解决,这里记录一下:

(1)不同C++版本编译对mangling的影响?对生成COMDAT section的影响?

(2)动态库(.so)是否也存在这种问题?对动态库不同的使用方式会有影响?比如进程运行时链接和通过dlopen方式是否会不同?

参考资料:

(1)https://forum.osdev.org/viewtopic.php?f=13&t=28618

(2)https://www.airs.com/blog/archives/52

========================================================

2019/07/22 更新

经大神同事提醒,GCC保证了如果都使用同一版本的编译器编译是二进制兼容的:is-it-safe-to-link-c17-c14-and-c11-objects

c++混合使用不同标准编译潜在的问题的更多相关文章

  1. codeblocks按c99标准编译c文件的设置

    作者:朱金灿 来源:http://blog.csdn.net/clever101 早上用codeblocks编译一个c文件,出现这样一个编译错误: +'for'+loop+initial+declar ...

  2. gcc/g++ 如何支持c11 / c++11标准编译

    如果用命令 g++ -g -Wall main.cpp  编译以下代码 : /* file : main.cpp */ #include <stdio.h> int main() { in ...

  3. 【转】gcc/g++ 如何支持c11 / c++11标准编译

     如果用命令 g++ -g -Wall main.cpp  编译以下代码 : 1 2 3 4 5 6 7 8 9 10 11 12 /*     file : main.cpp */ #include ...

  4. gcc g++支持C++11 标准编译及其区别

    g++ -g -Wall -std=c++11 main.cpp gcc -g -Wall -std=c11 main.cpp 如果不想每次写这个-std=C++11这个选项该怎么办呢? 方法出处:h ...

  5. 标准编译安装(configure make)

      ./configure --prefix=安装目录 这里注意,安装目录可以自己选择地方,但是自己选择地方的话就要把编译出的bin.include.lib三个文件夹分别加入XXX XXX XXX三个 ...

  6. 标准编译安装(cmake make)

    为什么要编译安装?因为根据需求可以个性化定制功能. 关键是阅读cmakelist,看都有哪些依赖,都有哪些选项可用,哪些选项是自己可以配置的. 一般流程: mkdir build cd build c ...

  7. Android系统移植与调试之------->MTK 标准编译命令

    命令格式:./maketek [option] [project] [action] [modules]Option:   -t ,-tee :输出log信息到当前终端   -o , -opt=-- ...

  8. Linux-编译器gcc/g++编译步骤

    gcc和g++现在是gnu中最主要和最流行的c&c++编译器.g++是c++的命令,以.cpp为主:对于c语言后缀名一般为.c,这时候命令换做gcc即可.编译器是根据gcc还是g++来确定是按 ...

  9. sublime text 2 + Dev-C++/MinGW 组合配置更方便快捷的 C/C++ 编译环境(原创)

    首先看一下配置后的效果: 1.直接在底部文本框中显示运行结果(不需要从键盘输入的时候使用): 2.在cmd中运行结果(需要从键盘输入的时候使用): 快捷键说明: 运行: 在底部文本栏显示结果:Ctrl ...

随机推荐

  1. Python - Django - 添加首页尾页上一页下一页

    添加首页和尾页: views.py: from django.shortcuts import render from app01 import models def book_list(reques ...

  2. Python - Django - request 对象

    request.method: 获取请求的方法,例如 GET.POST 等 views.py: from django.shortcuts import render, HttpResponse # ...

  3. SignalR 传Model类型的参数

    目录 集线器方法 js调用 集线器方法 集线器写了一个方法是这样的 public void test(string name, Customer customer) 第一个参数是string类型的,第 ...

  4. php 3.2 生成压缩文件,并下载

    public function zip_download() { $array = array( 'http://local.qki.com/site_upload/erweima/20190826/ ...

  5. 【linux学习笔记三】链接命令

    链接命令:ln link =============华丽的分割线============= ln又有软链接和硬链接 //硬链接特征(不建议创建硬链接) 1.拥有相同的i节点和存储block块,可以看做 ...

  6. LeetCode 108. 将有序数组转换为二叉搜索树(Convert Sorted Array to Binary Search Tree) 14

    108. 将有序数组转换为二叉搜索树 108. Convert Sorted Array to Binary Search Tree 题目描述 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索 ...

  7. QPS、TPS、PV、UV、GMV、IP、RPS?

    QPS.TPS.PV.UV.GMV.IP.RPS QPSQueries Per Second,每秒查询数.每秒能够响应的查询次数. QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准, ...

  8. python基础学习(十四)

    28.模块当脚本执行 !!!! 注意  这是分两个文件的  一个是student.py和app3.py student.py name = "Song Ke" name_list ...

  9. Python-02-基础知识

    一.第一个Python程序 [第一步]新建一个hello.txt [第二步]将后缀名txt改为py [第三步]使用记事本编辑该文件 [第四步]在cmd中运行该文件 print("Hello ...

  10. 第二章 Python基础语法

    2.1 环境的安装 解释器:py2 / py3 (环境变量) 开发工具:pycharm 2.2 编码 编码基础 ascii ,英文.符号,8位为一个东西,2**8 unicode ,万国码,可以表示所 ...