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 ...
随机推荐
- pix2pix&Cycle GAN&pix2pix HD
这里简短地谈一下如题的三篇论文: 参考:https://blog.csdn.net/gdymind/article/details/82696481 (1)pix2pix:从一张图片生成另一张图片 p ...
- 【kubectl 原理】kubectl 命令执行的时候究竟发生了什么(kubectl、apiserver、etcd)
参考: https://www.yangcs.net/posts/what-happens-when-k8s/ 总而言之,kubectl命令执行的时候,先在本地封装请求,然后过kube-apiserv ...
- JS和vue文本框输入改变p标签的内容测试
文本框输入,p标签的内容自动变成文本框的内容,如下是三种方法的测试: 方法1:JS里的onchange,当文本框内容改变事件,该事件里写的方法是,获取p标签本身,然后获取文本框的值,赋值给变量,最后给 ...
- git push时出现大文件的处理方法
最近在提交项目时出现报错 文件限制只能100M,但是里面有个文件202M,超过了码云的限制. 所以顺手就把这个文件删除了 然后发现还是同样的报错,反复检查目录还是不行,找了资料说,需要git rm 命 ...
- LeetCode 142. 环形链表 II(Linked List Cycle II)
142. 环形链表 II 142. Linked List Cycle II 题目描述 给定一个链表,返回链表开始入环的第一个节点.如果链表无环,则返回 null. 为了表示给定链表中的环,我们使用整 ...
- java抽象类及接口
Java抽象类: 抽象类特点:抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量.成员方法和构造方法的访问方式和普通类一样. 由于抽象类不能实例化对象,所以抽象类必须被extends [抽象 ...
- visio 绘图素材
1. 前言 visio是个绘图的好工具,可是自带图形元素有限,没有还要自己画. 推荐几个矢量图形素材库,里边有很多图形,很方便的导入到visio中,放大也不失真. 阿里巴巴矢量图库网 stockio ...
- Ubuntu的apt命令详解()deepin linux是在Ubuntu基础上开发的
apt-cache和apt-get是apt包的管理工具,他们根据/etc/apt/sources.list里的软件源地址列表搜索目标软件.并通过维护本地软件包列表来安装和卸载软件. 查看本机是否安装软 ...
- PAT(B) 1054 求平均值(Java)
题目链接:1054 求平均值 (20 point(s)) 题目描述 本题的基本要求非常简单:给定 N 个实数,计算它们的平均值.但复杂的是有些输入数据可能是非法的.一个"合法"的输 ...
- 【SCALA】3、模拟电路
Simulation package demo17 abstract class Simulation { type Action = () => Unit case class WorkIte ...