字节对齐和强制类型转换引发的ARM指令Crash问题
综述:
这个bug是由于项目代码中对指针进行了强转,强转前后的结构体存在字节对齐不匹配问题,在GCC进行O2编译优化时,部分代码转换为LDMIA/STMIA指令,该指令要求指针四字节对齐,从而引发了崩溃。
现象
现有项目中需要使用diff算法对指令数据进行压缩,该算法实现的正确性已经在PC端经过原有项目的检验,但是在移植到arm平台时却遇到了诡异的crash,程序会莫名崩溃在diff代买中。其堆栈也已经被bugly捕获,看不到有用信息。
分析
1.log定位出错代码
- 通过在代码中增加log日志,初步判断出错在extend_cover中的如下代码块:
- 1234567891011121314
for
(
int
i=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
))
{
const
TInt linkEqLength=getEqualLength(diff.pOldData+lastOldPos+(newPos-lastNewPos), diff.pOldData_end,diff.pNewData+newPos, diff.pNewData_end);
const
TInt 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,符合预期。
后记
字节对齐很重要,强转需谨慎!