字节对齐和强制类型转换引发的ARM指令Crash问题

发表于2017-05-17
评论0 2.1k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594

综述:

这个bug是由于项目代码中对指针进行了强转,强转前后的结构体存在字节对齐不匹配问题,在GCC进行O2编译优化时,部分代码转换为LDMIA/STMIA指令,该指令要求指针四字节对齐,从而引发了崩溃。

现象

现有项目中需要使用diff算法对指令数据进行压缩,该算法实现的正确性已经在PC端经过原有项目的检验,但是在移植到arm平台时却遇到了诡异的crash,程序会莫名崩溃在diff代买中。其堆栈也已经被bugly捕获,看不到有用信息。

分析

1.log定位出错代码

  • 通过在代码中增加log日志,初步判断出错在extend_cover中的如下代码块:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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
  • 1
    2
    3
    4
    5
    6
    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编译器,编译出来的程序会有新的崩溃点:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    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由于stNetFrameBuffer1字节对齐的原因,其本身是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,符合预期。

后记

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

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台