升级gcc7.3之后MOVAPS指令导致的程序coredump解决过程

问题点

在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会产生。可见编译器的优化还是做了很多隐式的工作的。

 

经验教训:

1.  __attribute__((aligned(64)))和inplacement NEW的混用要特别当心,这是一个坑。

2. 对于比较奇怪的crash点,需要进行汇编级别的调试,更容易发现问题的本质。