c++混合使用不同标准编译潜在的问题
最近项目使用的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++混合使用不同标准编译潜在的问题的更多相关文章
- codeblocks按c99标准编译c文件的设置
作者:朱金灿 来源:http://blog.csdn.net/clever101 早上用codeblocks编译一个c文件,出现这样一个编译错误: +'for'+loop+initial+declar ...
- gcc/g++ 如何支持c11 / c++11标准编译
如果用命令 g++ -g -Wall main.cpp 编译以下代码 : /* file : main.cpp */ #include <stdio.h> int main() { in ...
- 【转】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 ...
- gcc g++支持C++11 标准编译及其区别
g++ -g -Wall -std=c++11 main.cpp gcc -g -Wall -std=c11 main.cpp 如果不想每次写这个-std=C++11这个选项该怎么办呢? 方法出处:h ...
- 标准编译安装(configure make)
./configure --prefix=安装目录 这里注意,安装目录可以自己选择地方,但是自己选择地方的话就要把编译出的bin.include.lib三个文件夹分别加入XXX XXX XXX三个 ...
- 标准编译安装(cmake make)
为什么要编译安装?因为根据需求可以个性化定制功能. 关键是阅读cmakelist,看都有哪些依赖,都有哪些选项可用,哪些选项是自己可以配置的. 一般流程: mkdir build cd build c ...
- Android系统移植与调试之------->MTK 标准编译命令
命令格式:./maketek [option] [project] [action] [modules]Option: -t ,-tee :输出log信息到当前终端 -o , -opt=-- ...
- Linux-编译器gcc/g++编译步骤
gcc和g++现在是gnu中最主要和最流行的c&c++编译器.g++是c++的命令,以.cpp为主:对于c语言后缀名一般为.c,这时候命令换做gcc即可.编译器是根据gcc还是g++来确定是按 ...
- sublime text 2 + Dev-C++/MinGW 组合配置更方便快捷的 C/C++ 编译环境(原创)
首先看一下配置后的效果: 1.直接在底部文本框中显示运行结果(不需要从键盘输入的时候使用): 2.在cmd中运行结果(需要从键盘输入的时候使用): 快捷键说明: 运行: 在底部文本栏显示结果:Ctrl ...
随机推荐
- 【Leetcode_easy】872. Leaf-Similar Trees
problem 872. Leaf-Similar Trees 参考 1. Leetcode_easy_872. Leaf-Similar Trees; 完
- html5 横向滑动导航栏
前提 需要引入: <script src="../assets/js/iscroll.js"></script> v4.2版本 ####html <! ...
- 对 Watchbog Botnet 渗透过程和 Payload 的分析
漏洞利用 CVE-2018-1000861 https://jenkins.io/security/advisory/2018-12-05/ Watchbog在做什么? Watchbog僵尸网络为其所 ...
- Jira强制退出时(如意外停电)再启动报Locked错误的几个解决办法
查看jira_home的路径在/opt/atlassian/jira/atlassian-jira/WEB-INF/classes/jira-application.properties文件中查看 方 ...
- [转帖]linux操作系统测试工具
linux操作系统测试工具 http://cfdtesting.com/879156.html 作者: minions_222 来源: CFDTesting.com采编 发布于: ...
- Linux大道——博客目录
Linux基础 第一章 计算机基础 计算机基础 网络基础 第二章 Linux基础
- 20 闭包、nonlocal
闭包的概念 闭包就是能够读取其他函数内部变量的函数. 从模块级别调用函数内部的局部变量. 闭包 = 函数+环境变量(函数外部的变量) 闭包存在的条件 闭包必须返回一个函数 被返回的函数必须调用环境变量 ...
- svn钩子(hooks)
目录 钩子脚本的具体写法就是操作系统中shell脚本程序的写法,请根据自己SVN所在的操作系统和shell程序进行相应的写作 所谓钩子就是与一些版本库事件触发的程序,例如新修订版本的创建,或是未版本化 ...
- hadoop 伪分布启动-fs格式化
1.独立模式(standalone|local) nothing! 本地文件系统. 不需要启用单独进程. 2.pesudo(伪分布模式) 等同于完全分布式,只有一个节点. SSH: //(Socket ...
- 在微服务架构中service mesh是什么?
在微服务架构中service mesh是什么 什么是 service mesh ? 微服务架构将软件功能隔离为多个独立的服务,这些服务可独立部署,高度可维护和可测试,并围绕特定业务功能进行组织. 这些 ...