最近项目使用的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. 解决no module named setuptools

    To install setuptools on Debian: sudo apt-get install python-setuptools For Python 3.x: sudo apt-get ...

  2. SUBLIME必备插件FOR PHP

    Sublime Text真是一款写代码的利器,轻巧快捷,而且功能强大,用来写PHP代码再好不过了,告别以前用的笨重臃肿的Zend Studio,感觉一身轻松,PHP代码也更加优雅.但是PHP开发也经常 ...

  3. 海思HI2115芯片-NB-IOT模块向外发短信测试

    1. 说是有短信这个功能,测试下怎么使用?先使能BIP功能 AT+NCONFIG=ENABLE_BIP,TRUE 给SIM卡上电 AT+NUICC= 查询下短信中心服务号码 AT+CSCA? 发送短信 ...

  4. CHIMA网络安全攻防大赛经验分享

    比赛模式 第一轮:20分钟基础知识赛(50道题) 安全运维,法律法规,linux操作系统等 第二轮:50分钟CTF夺旗(5道题) 题目涵盖 密码学 运用多种工具,如ASCII对照,古典密码,凯撒密码, ...

  5. 【计算机视觉】目标检测之ECCV2016 - SSD Single Shot MultiBox Detector

    本文转载自: http://www.cnblogs.com/lillylin/p/6207292.html SSD论文阅读(Wei Liu--[ECCV2016]SSD Single Shot Mul ...

  6. win10卸载office2010的工具

    本来想装一个高版本的office,于是想先卸载老版本的.结果在win10的应用和功能中,愣是没找到安装的office2010,使用360也找不到,没法卸载. 网上搜了一下,找到一个好工具,micros ...

  7. [转帖]新iPhone的黑科技:UWB技术揭秘

    新iPhone的黑科技:UWB技术揭秘 http://blog.nsfocus.net/iphone-black-technology-uwb-technology-revealed/    阅读:  ...

  8. go get 使用proxy来下载

    http_proxy=https://127.0.0.1:1087 go get -v github.com/Shopify/sarama https_proxy=https://127.0.0.1: ...

  9. Sping Aop日志实现(五)--使用Mybatis注解保存日志

  10. mouseover mouseleave

    mouseover在当鼠标移入元素或其子元素的时候都会触发,是一个重复触发,冒泡的过程.见下面例子,一个父元素包含一个子元素,在其父元素内滑动鼠标,超出子元素的范围时,也会触发事件. 而mouseen ...