问题点
在CentOS上升级gcc7.3之后,程序编译成功,但是运行的时候,程序发生了coredump,gcc5.3是可以正常work的。使用gdb加载core文件: gdb -c core.xxx XXXX, 发现居然crash在一个指针的赋值语句里: p=nullptr.
调试过程
甚为奇怪,进一步并通过"disassemble"命令观察引发coredump的汇编指令,发现是movaps指令引发的,如下图:
通过google了解到movaps指令是需要16字节对齐的,check了一下传入的参数,确实不是16字节对齐的。如下:
SSE指令学习
SSE指令可以分为以下几类:
1)数据移动指令:支持内存到寄存器、寄存器到内存、寄存器到寄存器的数据移动
例如:movups指令, 对128位(由4个打包的单精度浮点数组成)做上述的移动处理
__asm
{
float af[4] = {0, 0 ,0 ,0}; float bf[4];
movups xmm0, af;
movups xmm1, xmm0;
movups bf, xmm1;
}
movaps指令,也是对128位(由4个打包单精度浮点数组成)做上述的移动处理,不同的是,如果移动的内存如果不满128位,程序将抛出一个异常,所以movaps指令处理的内存和寄存器必须是16字节对齐的。因此上面的代码需要部分修改才能运行正常
__asm
{
__declspec(align(16)) af[4] = {0, 0, 0, 0};
__declspec(align(16)) af[4];
movaps xmm0 , af;
movaps xmm1, xmm0;
movaps bf, xmm1;
}
相信大家对比movups和movaps指令就看出来了,mov表示移动,u,a分别表示不必16自己对齐和16自己对齐,而ps(packed single-precision floating-point)表示打包的单精度浮点数。对指令的构成有了初步了解之后,相信大家也很容器理解movupd和movapd的意思。
实际上不论是单精度浮点数还是双精度浮点数,数据移动更关注的是数据位是否是128位,并不关注内存中的具体数据类型,只有算术运算才会关注数据类型。
例如:
__asm
{
float af[4] = {5.0f, 5.0f, 5.0f, 5.0f}; float bf[4];
movupd xmm0, af;
movupd xmm1, xmm0;
movupd bf, xmm1;
}
movupd 更够实现与movups一样的效果,而不出任何异常。
了解了常用的128位指令移动指令,再来看看特殊的移动指令
movsd指令,可以实现将64位内存的数据移动到寄存器的低64,将寄存器的低64位移动到内存中,以及寄存器a的低64位移动到寄存器b的低64位并保持高64位不变。movss指令与movsd指令类似,只不过是对32位数据的移动.
又回去仔细追踪了一下代码,发现这个类声明的时候,指定了64字节对齐:__attribute__((aligned(64)))
但是程序运行过程中,会使用inplacement NEW来生成该类的对象,此时传入的地址并不能保证是64字节对齐的,至此可以断定是 __attribute__((aligned(64))) 和 inplacement NEW 混用引发的问题,这也算是c++的一个经典深坑了。不过为什么gcc5.3可以正常work, 但是gcc 7.3就生成了movaps指令了呢? 确认了一下gcc 5.3生成的汇编指令,确实没有movaps.
此时,有位大神同事发现了一个解决方案,调整了一下类成员的顺序,居然奇迹般的解决问题了,不crash了。
调整前 | 调整后 |
class A { void* p1; void* p2; uint64_t n; } | class A { void* p1; uint64_t n; void* p2; } |
出问题的reset函数是要把这些成员全部set为0. |
分析了汇编指令,发现调整顺序之后,gcc 7.3并没有生成movaps指令。很显然,如果是连续的两个指针变量,则gcc 7.3生成了movaps,否则没有movaps指令。
我的猜测是这样的: 没有调整成员顺序之前,因为reset函数里面要把 那两个指针都设为nullptr, 而且声明的时候,这两个指针是连续的,所以gcc优化试图使用movaps来做这个事情,因为可以一个指令把两个指针都清空。
另外根据另外一名同事的猜测:类声明的时候指定的 __attribute__((aligned(64))) , 导致gcc 7.3认为这个类一定是64字节对齐的,所以优化产生了movaps指令。但是对于inplace new, 不能保证是64字节对齐的。
为了验证这个猜测,我把__attribute__((aligned(64)))去掉试了一下 (不调整成员顺序),确实也不crash了。
同时比较了有__attribute__((aligned(64))),和没有__attribute__((aligned(64)))下生成的汇编代码: 没有__attribute__((aligned(64)))的时候,确实没有产生movaps指令。
%disassemble <function name>
至此,root cause算是找到了:
用__attribute__((aligned(64)))修饰了类的声明,但是在inplacement new的场合,又满足不了传入的地址符合__attribute__((aligned(64)))。。。“聪明”的gcc又根据__attribute__((aligned(64)))做了一些优化。。所以悲剧发生了。
解决方案有两个:
1. 对于所有可能被inplacement NEW的类,去掉__attribute__((aligned(64)))修饰。
2. 改用gcc的O2编译选项。根据验证结果,O2的时候不产生movaps指令,O3会产生。可见编译器的优化还是做了很多隐式的工作的。20220616追加:好像O2并不能确保gcc不产生movaps, 好像只有-mno-sse 可以。参考:c - gcc -O3 optimize :: xmm0 register? - Stack Overflow
经验教训:
1. __attribute__((aligned(64)))和inplacement NEW的混用要特别当心,这是一个坑。
2. 对于比较奇怪的crash点,需要进行汇编级别的调试,更容易发现问题的本质。