字节对齐和强制类型转换引发的ARM指令Crash问题
综述:
这个bug是由于项目代码中对指针进行了强转,强转前后的结构体存在字节对齐不匹配问题,在GCC进行O2编译优化时,部分代码转换为LDMIA/STMIA指令,该指令要求指针四字节对齐,从而引发了崩溃。
现象
现有项目中需要使用diff算法对指令数据进行压缩,该算法实现的正确性已经在PC端经过原有项目的检验,但是在移植到arm平台时却遇到了诡异的crash,程序会莫名崩溃在diff代买中。其堆栈也已经被bugly捕获,看不到有用信息。
分析
1.log定位出错代码
- 通过在代码中增加log日志,初步判断出错在extend_cover中的如下代码块:
- 1234567891011121314
for(inti=0; i<(int)diff.coverPos; ++i) {TInt newPos_next=(TInt)(diff.pNewData_end-diff.pNewData);if(i+1<(int)diff.coverPos)newPos_next=cover[i+1].newPos; TOldCover& curCover=cover[i]; TInt extendLength_front=getCanExtendLength(curCover.oldPos-1,curCover.newPos-1,-1,lastNewPos,newPos_next,diff);if(extendLength_front>0){/******crash begin******/curCover.oldPos-=extendLength_front; curCover.newPos-=extendLength_front; curCover.length+=extendLength_front;/******crash edn *****/}TInt extendLength_back=getCanExtendLength(curCover.oldPos+curCover.length,curCover.newPos+curCover.length,1,lastNewPos,newPos_next,diff);if(extendLength_back>0){curCover.length+=extendLength_back;}lastNewPos=curCover.newPos+curCover.length;} - 进一步定位是哪条语句出问题时,遇到一个诡异的事情,只要增加如下的log日志,则程序又能够正确运行,无crash:
- 123456
if(extendLength_front>0){/******诡异的log日志******/TRACER_LOG(TCG_INFO,"curCover [%x]", &curCover);curCover.oldPos-=extendLength_front;curCover.newPos-=extendLength_front;curCover.length+=extendLength_front;}
- 一条log语句导致程序运行情况完全不同,只有对比前后两种情况下汇编指令实现的差异,找到其中的不同点来查找线索。
2.汇编代码对比查找差异
- 无log出错情况下出错代码的arm汇编如下:

- 对应的有log可以正常运行的arm汇编如下:

- 在这个时候还没有怀疑是STMIA指令的问题,此时反而怀疑是否是外部某些地方将数据写错导致取指错误。重新梳理了一遍代码逻辑,diff代码是单开了一个线程进行处理,不存在其他线程写坏diff数据的问题。难道是无意中触发了编译器bug导致该处取值时出错了?
3.对比不同编译器和编译选项
出错的程序使用ndk r12b版本进行编译,优化级别是–O2。为了对比不同的编译器和编译选项问题,将ndk r10e/r11c/r12b/r13b/r14b都进行了测试,同时对比了不同的编译选项–O0/ -O1 / -O2。
- 所有编译器版本上,在-O0的优化级别上,编译出来的程序全部都能正确运行;
- 在-O1优化级别上,使用r10e/r11c/r12b编译出来的程序均可以正常运行,r13b/r14b使用的是clang编译器,编译出来的程序会有新的崩溃点:
- 123456789
if((curEqLength < lastLinkEqLength + kUnLinkLength) && (newPos - lastNewPos >0)){constTInt linkEqLength=getEqualLength(diff.pOldData+lastOldPos+(newPos-lastNewPos), diff.pOldData_end,diff.pNewData+newPos, diff.pNewData_end);constTInt linkLength=(newPos-lastNewPos)+linkEqLength;if(diff.coverPos ==0){/***********crash block begin**********/(diff.pCover+diff.coverPos)->newPos = lastNewPos; (diff.pCover+diff.coverPos)->oldPos = lastOldPos; (diff.pCover+diff.coverPos)->length = linkLength;diff.coverPos++;/***********crash block end**********/}else{TOldCover* curCover = diff.pCover+(diff.coverPos-1);curCover->length+=linkLength;}newPos+=std::max(linkEqLength,curEqLength);//实际等价于+=curEqLength;}
- 使用-O2优化级别,r10e/r11c/r12b0编译的程序重现问题,尝试对-O2中的优化选项进行分析,发现-O2时加上-fno-peephole2 -fno-schedule-insns编译选项,编译的程序则可以继续运行。
- 查找-fno-peephole2 -fno-schedule-insns的相关资料,发现前者是机器相关的优化,后者是指令重排的优化。难道是因为优化导致的部分代码逻辑错误,得,还是分析汇编。
4.去掉指令重排和机器优化的O2优化代码汇编分析
既然加上-fno-peephole2 -fno-schedule-insns进行编译的代码可以正常运行,那么就对比下和常规O2优化下代码的差异。
- 去掉指令重排和机器优化的汇编代码:

- 和之前的汇编代码进行对比,发现上述代码和加了log可以正常运行的汇编代码基本一致,每次数据存储时都是一个一个存储,而出错的arm汇编中使用了STMIA汇编指令一次存储了两个数据:

5.问题最终定位
上面讲到,两种正确情况下,都是对数据一次一次存储,出错的情况是用了STMIA一次存储两个数据,那么就从STMIA指令开始查起。
- 查找STMIA/LDMIA相关的资料,发现有如下表述:
Load/store instructions that act on multiple registers, for example LDM, are considered as working with multiple word quantities, so these instructions also require 4-byte aligned addresses.
资料链接:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html
- 通过日志输出,判断出出错代码的指针确实不是四字节对齐:

- 对代码进行分析
TOldCover结构体指针pCover由结构体stNetFrameBuffer中的byData强转而来,结构体stNetFrameBuffer设置了一字节对齐,但是TOldCover默认四字节对齐。
stNetFrameBuffer定义:
1 2 3 4 5 6 7 8 9 10 11 | // 考虑包大小,不考虑字节对齐性能 #pragmapack(1) // 消息基类 structstNetEvent { TCGubyte byEventFlag; TCGubyte byEventType; TCGuint unEventSize; TCGuint unOrgSize;}; structstNetFrameBuffer:publicstNetEvent { TCGubyte byData[FRAME_BUFFER_SIZE_EX]; stNetFrameBuffer() { }}; #pragmapack() |
TOldCover定义:
1 2 3 4 5 6 | structTOldCover{ TIntnewPos; TIntoldPos; TIntlength; inlineTOldCover():newPos(0),oldPos(0),length(0){}}; |
并且在代码中进行了强转(示例代码):
diff.pCover =(TOldCover*)pDiffFrame->byData;此时的pCover由于stNetFrameBuffer为1字节对齐的原因,其本身是2字节对齐的。
问题修复和测试验证
问题基本定位清楚,是由于不同的字节对齐方式,以及强制转换,导致优化后的代码执行错误。
- 问题修复:
问题找到之后修复方式很简单,保证两处结构体使用相同的字节对齐方式即可,此处改为TOldCover设为1字节对齐。
1 2 3 4 5 6 7 8 9 | #pragma pack(1) struct TOldCover { TInt newPos; TInt oldPos; TInt length; inline TOldCover():newPos(0),oldPos(0),length(0) { }}; #pragma pack() |
- 验证:
对修改后的代码在不同的ndk版本进行–O2编译测试,最终结果是都可以正常运行。NDK r12b -O2下编译后的arm指令如下,没有使用STMIA/LDMIA,符合预期。

后记
字节对齐很重要,强转需谨慎!
