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 ...
随机推荐
- Python - Django - 上传文件
upload.html: <!DOCTYPE html> <html lang="en"> <head> <meta charset=&q ...
- Pythonrandom模块(获取随机数)常用方法和使用例子
Python random模块(获取随机数)常用方法和使用例子 这篇文章主要介绍了Python random模块(获取随机数)常用方法和使用例子,需要的朋友可以参考下 random.random ra ...
- kubernetes&prometheus 【组件】
查看prometheus target页面可得组件 kube-state-metric: https://github.com/kubernetes/kube-state-metrics/blob/m ...
- python 求交集、并集、差集
需要用到set类型 交集,两种方法 retA = [i for i in listA if i in listB] retB = setA.intersection(setB) 并集 retC = s ...
- Django:使用模态框新增数据,成功后提示“提交成功”,并刷新表格bootstrap-table数据
废话不说先看图: 代码实现: 前台代码: {% load staticfiles %} <!DOCTYPE html> <html lang="en"> ...
- 服务发现--初识Consul
前言 服务注册.服务发现作为构建微服务架构得基础设施环节,重要性不言而喻.在当下,比较热门用于做服务注册和发现的开源项目包括zookeeper.etcd.euerka和consul.今天在这里对近期学 ...
- 封装transform函数(设置和获取transform的属性和属性值)
(function (w) { /** * 设置或者获取元素的transform属性值 * @param node 要设置的元素 * @param param 变换属性: translate\scal ...
- ubuntu 安装 typora
# or run: # sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE wget -qO ...
- memmove和memcpy函数的区别及实现
一.memmove()和memcpy()函数和strcpy()函数的区别: (1)使用的类型不同,strcpy()函数只对字符串进行操作:memmove()和memcpy()函数对所有类型都适用,为内 ...
- Mkdown常用语法
这篇笔记停了两天,今天开始整理, 写这篇笔记主要是加强下自己的mkdown语法知识, 也当作练手之作. Mkdown 语法简介 标题 [h1 - h6] # ~ ###### 一般# 作为标题, 只有 ...