GCC优化引发的一场血案

       正所谓人在江湖飘,那有不挨刀。从事软件行业,不写几个惊天地泣鬼神的BUG都不好意思说自己干过软件。
       本人从事C/C++开发工作,日常就是看看文档发发呆写写BUG。在一个风和日丽的上午,日常上班中,突然客户丢过来一个BUG,是System halt,灾难性BUG啊!通过Sorce Dump,最终出现问题的代码锁定在以下位置:
       因为项目保密需求,代码经过变形和加工,仅作示例,大家领会精神。

#defien STACK_SIZE_MAX  50

//此处省略一万字…… 

DATA_TYPE_ST* g_pstStack[STACK_SIZE_MAX];

//此处省略一万字…… 

while((NULL != g_pstStack[uiIndex]->pstData) && (uiIndex < STACK_SIZE_MAX))  
{  
    for(unsigned int uiIdx=0; uiIdx!=g_pstStack[uiIndex]->pstData->uiIdx; uiIdx++)  
    {  
        //此处省略一万字……  
    }
    uiIndex++;
}  

       介入运算的变量为:

g_pstStack;    //全局变量,指针数组。
uiIndex;       //局部变量,unsigned int型,循环索引。
STACK_SIZE_MAX //宏定义,值为50。

       看上去没啥异常的代码,通过Source Dump分析,崩溃的地方为以下代码:

    for(unsigned int uiIdx=0; uiIdx!=g_pstStack[uiIndex]->pstData->uiIdx; uiIdx++)  

       但是这句话貌似也没啥,一句循环而已,如果非要说有问题,那就是指针解引时候的问题。但是由于是交叉编译,手里又没有调试器,所以加上几行Log,对再次对现象进行再现。

while((NULL != g_pstStack[uiIndex]->pstData) && (uiIndex < STACK_SIZE_MAX))  
{  
    DBG_LOG("[DBG_LOG]uiIndex = %u.", uiIndex);
    for(unsigned int uiIdx=0; uiIdx!=g_pstStack[uiIndex]->pstData->uiIdx; uiIdx++)  
    {  
        //此处省略一万字……  
    }
    uiIndex++;
}  

       编译,再次触发BUG,我傻了,因为Log里赫然写着:

[DBG_LOG]uiIndex = 50.

       这啥情况啊,循环条件为((NULL != g_pstStack[uiIndex]->pstData) && (uiIndex < STACK_SIZE_MAX)),我反复看了几遍,没问题啊!
       现在问题已经找到了,对野指针进行解引,导致了System Halt。但是,野指针是因为索引越界导致的,换句话说,(uiIndex < STACK_SIZE_MAX)这个条件根本没起作用!为了防止眼花,我改了一下Log输出:

while((NULL != g_pstStack[uiIndex]->pstData) && (uiIndex < STACK_SIZE_MAX))  
{  
    DBG_LOG("[DBG_LOG]uiIndex = %u, Array size is %d.", uiIndex, STACK_SIZE_MAX);
    for(unsigned int uiIdx=0; uiIdx!=g_pstStack[uiIndex]->pstData->uiIdx; uiIdx++)  
    {  
        //此处省略一万字……  
    }
    uiIndex++;
}  

       再次运行,触发BUG,然后,Log文件里写着:

[DBG_LOG]uiIndex = 50, Array size is 50.

       完蛋了,灵异了……
       就在我觉得无从下手时候,迷茫之中,我将wihle中的两个条件对换了一下以下位置,代码变成了:

while((uiIndex < STACK_SIZE_MAX) && (NULL != g_pstStack[uiIndex]->pstData))  
{  
    DBG_LOG("[DBG_LOG]uiIndex = %u, Array size is %d.", uiIndex, STACK_SIZE_MAX);
    for(unsigned int uiIdx=0; uiIdx!=g_pstStack[uiIndex]->pstData->uiIdx; uiIdx++)  
    {  
        //此处省略一万字……  
    }
    uiIndex++;
}  

       BUG消失了!!!!!!!!!!
       无数只草泥马呼啸而过……
       但是很快,我冷静了下来,早前我遇到过本来正常稳定运行的程序,在O3优化开启后,重新编译,程序崩溃的情况,职业直觉告诉我,这次应该也类似,这应该是编译器的锅,或者再准确点,这是Makefile的锅。
       于是我开始找工程中的所有makefile和ruler,终于在xxxx.mk中找到了一句话:

#此处省略一万字……  
OPTIMIZATION_FLAG =? -O2
#此处省略一万字……  

       在确认其他地方没有额外的优化参数后,我确定了,问题就出在这里。但是为什么,不清楚……
       冷静一下,把之前修正的代码改回来,这个参数修改为O1,再次编译……
       BUG也没有再现!
       这下可以确定,就是这个-O2搞的鬼,那么下一步的工作也清楚了:搞清楚-O2都干了什么!
       这个项目使用的是GCC4.8.3,那么,去GCC官网,下载相应版本的文档,查看O2优化的具体说明,内容如下:

-O2: Optimize even more. GCC performs nearly all supported optimizations that do not involve a space-speed tradeoff. As compared to ‘-O’, this option increases both compilation time and the performance of the generated code.
‘-O2’ turns on all optimization flags specified by ‘-O’.  It also turns on the following optimization flags:
	-fthread-jumps
	-falign-functions -falign-jumps
	-falign-loops -falign-labels
	-fcaller-saves
	-fcrossjumping
	-fcse-follow-jumps -fcse-skip-blocks
	-fdelete-null-pointer-checks
	-fdevirtualize
	-fexpensive-optimizations
	-fgcse -fgcse-lm
	-fhoist-adjacent-loads
	-finline-small-functions
	-findirect-inlining
	-fipa-sra
	-foptimize-sibling-calls
	-fpartial-inlining
	-fpeephole2
	-fregmove
	-freorder-blocks -freorder-functions
	-frerun-cse-after-loop
	-fsched-interblock -fsched-spec
	-fschedule-insns -fschedule-insns2
	-fstrict-aliasing -fstrict-overflow
	-ftree-switch-conversion -ftree-tail-merge
	-ftree-pre
	-ftree-vrp

       简单点说,-O2优化就是在-O优化的基础上,额外再追加这些优化选项,大概三十几个的样子吧。
       根据这个说明,我把Makefile改成了这样:

#此处省略一万字……  
OPTIMIZATION_FLAG = -O
OPTIMIZATION_FLAG += -fthread-jumps
OPTIMIZATION_FLAG += -falign-functions -falign-jumps
OPTIMIZATION_FLAG += -falign-loops -falign-labels
OPTIMIZATION_FLAG += -fcaller-saves
OPTIMIZATION_FLAG += -fcrossjumping
OPTIMIZATION_FLAG += -fcse-follow-jumps -fcse-skip-blocks
OPTIMIZATION_FLAG += -fdelete-null-pointer-checks
OPTIMIZATION_FLAG += -fdevirtualize
OPTIMIZATION_FLAG += -fexpensive-optimizations
OPTIMIZATION_FLAG += -fgcse -fgcse-lm
OPTIMIZATION_FLAG += -fhoist-adjacent-loads
OPTIMIZATION_FLAG += -finline-small-functions
OPTIMIZATION_FLAG += -findirect-inlining
OPTIMIZATION_FLAG += -fipa-sra
OPTIMIZATION_FLAG += -foptimize-sibling-calls
OPTIMIZATION_FLAG += -fpartial-inlining
OPTIMIZATION_FLAG += -fpeephole2
OPTIMIZATION_FLAG += -fregmove
OPTIMIZATION_FLAG += -freorder-blocks -freorder-functions
OPTIMIZATION_FLAG += -frerun-cse-after-loop
OPTIMIZATION_FLAG += -fsched-interblock -fsched-spec
OPTIMIZATION_FLAG += -fschedule-insns -fschedule-insns2
OPTIMIZATION_FLAG += -fstrict-aliasing -fstrict-overflow
OPTIMIZATION_FLAG += -ftree-switch-conversion -ftree-tail-merge
OPTIMIZATION_FLAG += -ftree-pre
OPTIMIZATION_FLAG += -ftree-vrp
#此处省略一万字……  

       按照GCC文档中的说法,此方法等同于-O2优化。
       重新编译,再试,BUG再现了!
       这些增加的项目太多了,逐一读文档排查太慢,于是乎将这些编译条件对半注释,重编译,最终,将BUG的问题锁定在这个编译优化选项上:

OPTIMIZATION_FLAG += -ftree-vrp

       然后,GCC文档中对-ftree-vrp这个项目的说明如下:

Perform Value Range Propagation on trees. This is similar to the constant propagation pass, but instead of values, ranges of values are propagated. This allows the optimizers to remove unnecessary range checks like array bound checks and null pointer checks. This is enabled by default at ‘-O2’ and higher. Null pointer check elimination is only done if ‘-fdelete-null-pointer-checks’ is enabled.

       然后看一下代码,问题明了了!
       上述说明的大概意思是,-ftree-vrp选项会自动分析代码逻辑,对一些范围可以判定的索引值边界进行优化(也就是删除)。
       那么再来看下代码:

#defien STACK_SIZE_MAX  50

//此处省略一万字…… 

DATA_TYPE_ST* g_pstStack[STACK_SIZE_MAX];

//此处省略一万字…… 

while((NULL != g_pstStack[uiIndex]->pstData) && (uiIndex < STACK_SIZE_MAX))  
{  
    for(unsigned int uiIdx=0; uiIdx!=g_pstStack[uiIndex]->pstData->uiIdx; uiIdx++)  
    {  
        //此处省略一万字……  
    }
    uiIndex++;
}  

       符号STACK_SIZE_MAX被同时用来声明数组长度和边界判断,而且边界判断条件又在数组索引之后,编译器很自然的就认为uiIndex必然为有效值,从而忽略了uiIndex < STACK_SIZE_MAX这一判断,最终导致循环条件失去控制,解引了一个野指针。交换了判定条件后,由于编译过程中对条件的判断是由左至右,索引边界判断在索引内容判断之前,优化项目无法裁定,所以两个条件都为有效。        知道了问题和原因就好办了,将while条件中的索引边界判断提至最前即可,同时盘点其他代码,对相应问题进行修改。
       至此,问题解决,也算加了一个经验点。

转载于:https://my.oschina.net/Polarix/blog/2480651

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值