点击链接阅读原文,获取更多技术内容:从一个crash问题展开,探索gcc编译优化细节-阿里云开发者社区
问题分析的过程也是技术之路的成长,本文以一个gcc编译优化引发的崩溃为切入点,逐步展开对编译器优化细节的探索之路,在分析过程中打开了新世界的大门……
作者 | 瞳尘
来源 | 阿里云开发者公众号
背景:一次平平无奇的车祸
去年,客户提了个bug,并甩了给我们一个分段错误截图,必现崩溃。

这个必现问题我根本不慌的,由于有一个伟人曾经说过:“必现问题都不是问题!”
段错误,无非就是use after free、越界读写器等导致的非法内存访问上限。平平无奇的崩溃,且看我分析!
一、寻找元凶
1.1 头部分析猛如虎
经过初步分析,最终问题锁定在了一个循环赋值函数中,
void* readTileContentIndexCallback(TileContentIndexStruct *tileIndexData, int32_t count) {
TileContentIndex* tileContentIndexList = new TileContentIndex[count];
for (int32_t index = 0; index < count; index++) {
TileContentIndexStruct &inData = tileIndexData[index];
TileContentIndex &outData = tileContentIndexList[index];
outData.urID = inData.urCode;
outData.adcode = inData.adcode;
outData.level = inData.levelNumber;
outData.southWestTileId = inData.southWestTileId;
outData.numRows = inData.numRows;
outData.numColumns = inData.numColumns;
outData.tileIndex = inData.tileContentIndex;
}
return tileContentIndexList;
}
这里面的赋值引发了问题,导致上层访问数据的时候地址非法了。
但是这个循环赋值逻辑超级简单,的确 看不出有啥毛病,问题的分析一时陷入了僵局。
1.2 机智的宗翰
宗翰是我们组的一个超级机智的小伙子。他反馈这块代码一直没改过,由于本次必现崩溃是修改了gcc编译优化级别,从O2改成O3导致的,发现修改回O2之后必目前崩溃就不见了。
因此,问题就很明朗了,我们来看看gcc O3相对于O2做了哪些优化是不是就行了?
进一步优化。 -O3 打开 -O2 指定的所有优化,还打开以下优化标志:
-fgcse-重新加载后
-fipa-cp-克隆
-floop-交汇处
-floop-unroll-and-jam
-fpeel-循环
-f预测共同点
-fsplit-循环
-fsplit-路径
-ftree-循环分布
-ftree-部分预
-funswitch-循环
-fvect-成本模型=动态
-fversion-loops-for-strides
1.3 问题模拟复现
虽然知道了是编译器优化的问题,但是gcc官网上对于各个优化选项没有代码示例,只有几句解释,看着他们的解释我还是不知道我们的代码命中了哪个优化。
还好,我也很机智!
机智的我决定仿照我们出问题的代码写个小demo,用出问题的环境编译链去复现这个问题,然后具体做法如下,我写了跟个问题代码类似逻辑的demo, 然后用问题环境的工具链尝试编译,先用O2试一下。
g++ -O2 -S -o main2.s main.cpp //这个命令可以生成O2下的安装文件
g++ -o main2 main2.s //根据编译文件生成执行程序main2
执行一下,发现一切正常

再用O3搞一下:
g++ -O3 -S -o mainO3.s main.cpp
g++ -o mainO3 mainO3.s
问题复现!

1.4 编译优化选项排查
先查一下当前版本gcc编译器O2和O3分别打开哪些编译优化,使用命令:
gcc/g++ -Q -O<数字>; –help=优化器
例如:
gcc/g++ -Q -O2 –help=优化器
gcc/g++ -Q -O3 –help=优化器
差异如下(左边O3,右边O2):

可以看到除了上面官网说的几个选项外,O3还比O2多了下面几个优化:
- -ftree-循环-分布模式
- -ftree-循环向量化
- -finline 函数
- -ftree-slp-向量化
其中从字表面看跟循环相关的有如下几个:
- -floop-交汇处
- -floop-unroll-and-jam
- -ftree-循环分布
- -funswitch-循环
- -fversion-loops-for-strides
- -ftree-循环-分布模式
- -ftree-循环向量化
拿-ftree-loop-vectorize举例,-f表明打开某个选项,改成-fno-邻居就是关闭,改成-fno-tree-loop-vectorize再查一下:

这样最差也可以挨个关闭O3默认比O2多的优化选项来确认是哪个优化选项引起的问题了~
经过简单测试发现是优化选项-ftree-loop-vectorize导致的问题,编译命令如下:
g++ -O3 -fno-tree-loop-vectorize -S -o main3t.s main.cpp //打开O3,但是关闭tree-loop-vectorize
g++ -o main3t main3t.s // 生成执行程序main3t
必然出现的崩溃消失了!
1.5 了解-ftree-loop-vectorize
gcc官网上说这个优化选项是O2默认开启的
对树执行循环矢量化。默认情况下,此标志在 -O2 以及 -ftree-vectorize、-fprofile-use 和 -fauto-profile 中启用。
这与实测不符(官网说的不准或者有版本号前提),
g++ -Q -O2 –help=optimizers|grep 树循环向量化

说明问题环境中O3才会开启这个优化选项,原来如此,这个编译优化导致崩溃,那么它都优化了点啥呢?官网上的描述有点少,其大体思想就是将简单的循环语句展开,减少循环体执行次数,例如赋值循环代码:
for (int i = 0; i < 16; i++) {
a[i] = b[i];
}
可能会被优化成:
for (int i = 0; i < 16; i+=4) {
a[i] = b[i];
a[i + 1] = b[i + 1];
a[i + 2] = b[i + 2];
a[i + 3] = b[i + 3];
}
通过增加循环的步长减少了循环体执行次数,提高了代码效率。上面的例子中一个检测单元内部做了四个赋值,而优化之前需要执行四次循环才可以。
我们先查一下demo中哪些代码正在被优化优化了,使用命令g++ -fopt-info-vec-optimized main.cpp -O3

可以看到就是循环赋值的代码(行号38)。
1.6 初获战果&局部性撤退
至此,元凶已经缉拿归案——循环支持化优化导致了本次崩溃。
SDK交付团队,我们必须跟客户的编译工具链保持一致,编译选项也需要维护一致,因此应该开启O3(正常发布版本也是如此),但是单独关闭tree-loop-vectorize来规避此问题。
那么,循环优化到底是如何导致崩溃的?是gcc的bug还是我们自己的代码写的有问题呢?
由于当时练习功底过于薄弱,难以查出凶手的作案动机与详细手法,我选择了局部性撤退,重新开始练习内功来日再战。
二、再探作案细节
一年后。
最近学习达成了共识(我觉得我又行了!!),所以再次找到了这个问题!决定把它的根源一挖,将凶手作案过程详细展开,昭告天下——正义虽然迟缓但到!
2.1 重写demo复现问题
隔一年,数据一些许散佚现,遂再次构造demo复此问题。demo代码见文末链接test2(方便gdb调试入参和出参用了全局变量)。
核心代码如下:
struct TileContentIndexStruct {
int32_t urCode; // 4字节
int32_t adcode; // 4字节
int32_t levelNumber; // 4字节
int32_t southWestTileId; // 4字节
int32_t numRows; // 4字节
int32_t numColumns; // 4字节
uint8_t* tileContentIndex; // 8字节
int32_t dataSize; // 4字节
//64位系统8字节对齐,填充4字节
}; // 共40字节
struct TileContentIndex {
uint16_t urID; // 2字节
uint16_t level; // 2字节
uint32_t adcode; // 4字节
uint32_t southWestTileId; // 4字节
uint16_t numRows; // 2字节
uint16_t numColumns; // 2字节(至此正好8字节对齐,无需填充)
uint8_t* tileIndex; // 8字节
}; // 共24字节
int g_rowCount = 10;
TileContentIndex g_tileContentIndexList[10]; // 被复制
TileContentIndexStruct g_tileContentVector[10]; // 源数据
void* readTileContentIndexCallback(TileContentIndexStruct *tileIndexData, int32_t count)
{
for (int32_t index = 0; index < count; index++) {
TileContentIndexStruct &inData = tileIndexData[index];
TileContentIndex &outData = g_tileContentIndexList[index];
outData.urID = (uint16_t)inData.urCode;
outData.adcode = (uint32_t)inData.adcode;
outData.level = (uint16_t)inData.levelNumber;
outData.southWestTileId = (uint32_t)inData.southWestTileId;
outData.numRows = (uint16_t)inData.numRows;
outData.numColumns = (uint16_t)inData.numColumns;
outData.tileIndex = inData.tileContentIndex;
}
return g_tileContentIndexList;
}
// 注:readTileContentIndexCallback调用时入参传的是g_tileContentVector和g_rowCount
2.2 对比优化的做法
优化之前的准备涉及到指令,看上去就很轻松了。
需要注意的是
readTileContentIndexCallback函数只有两个参数,所以通过注册传入即可。函数
readTileContentIndexCallback的第一个参数使用的是后面的x0注册传递,第二个参数使用的是x1个注册,而w1注册是指x1的低32位。
_Z28readTileContentIndexCallbackP22TileContentIndexStructi:
.LFB48:
.cfi_startproc
cmp w1, 0 // w1寄存器的值和0比较(w1对应入参count)
ble .L2 // 比较结果w1 <= 0则跳转到.L2
mov x2, x0 // x2 = x0 (x0对应入参tileIndexData)
adrp x3, .LANCHOR0 // 将.LANCHOR0高地址(低12bit清零后的地址)load到x3寄存器
add x3, x3, :lo12:.LANCHOR0 // 将.LANCHOR0低12位加到x3中,至此x3拥有了完整的.LANCHOR0,即全局变量 g_tileContentIndexList 的地址
sub w1, w1, #1 // w1减一后存入w1,w1是x1的低32bit
add x1, x1, x1, lsl 2 // 将寄存器x1的值与寄存器x1左移2位的值相加,并将结果存储回寄存器x1. 相当于将寄存器x1的值乘以5
add x0, x0, 40 // x0 = x0 + 40
add x0, x0, x1, lsl 3 // x0 = (x1 << 3) + x0
// 综合这4行相当于x0 = (x1 - 1) * 40 + (x0 + 40) = x1 * 40 + x0
.L3:
ldr w1, [x2] // w1 = tileIndexData->urCode,将寄存器x2指向的地址的值加载到寄存器w1
strh w1, [x3] // 将寄存器w1的值存储到寄存器x3指向的地址(由于强转成了uint16_t所以是半字存储,只存低16bit)
ldr w1, [x2, 4] // w1 = tileIndexData->adcode
str w1, [x3, 4] // [x3 + 4] = w1
ldr w1, [x2, 8] // w1 = tileIndexData->levelNumber
strh w1, [x3, 2] // g_tileContentIndexList.level = (uint16_t)inData.levelNumber
ldr w1, [x2, 12] // w1 = tileIndexData->southWestTileId
str w1, [x3, 8] // 存到 g_tileContentIndexList.southWestTileId
ldr w1, [x2, 16] // tileIndexData->numRows
strh w1, [x3, 12] // 存到 g_tileContentIndexList.numRows
ldr w1, [x2, 20] // tileIndexData->numColumns
strh w1, [x3, 14] // 存到 g_tileContentIndexList.numColumns
ldr x1, [x2, 24] // x1 = tileIndexData->tileContentIndex
str x1, [x3, 16] // 存到 g_tileContentIndexList.tileIndex
add x2, x2, 40 // tileIndexData++(sizeof(TileContentIndexStruct) = 40)
add x3, x3, 24 // g_tileContentIndexList++ (sizeof(TileContentIndex) = 24)
cmp x2, x0 // 比较x2和x0
bne .L3 //若x2不等于x0则跳转到.L3继续循环
.L2:
adrp x0, .LANCHOR0
add x0, x0, :lo12:.LANCHOR0 // 两行指令将.LANCHOR0的地址赋给寄存器x0
ret // 返回
.cfi_endproc
.LFE48:
.size _Z28readTileContentIndexCallbackP22TileContentIndexStructi, .-_Z28readTileContentIndexCallbackP22TileContentIndexStructi
.align 2
.global _Z15readTileContentRiPFPvP22TileContentIndexStructiE
.type _Z15readTileContentRiPFPvP22TileContentIndexStructiE, %function
我果然行了,这几个指令根本难不倒我,直接逐行读完。
可以看到这个循环就是把 struct TileContentIndexStruct g_tileContentVector[10] 这个内存的内容复制到 struct TileContentIndex g_tileContentIndexList[10] 这个内存中。其中sizeof(TileContentIndexStruct) = 40,sizeof(TileContentIndex) = 24。设置中通过x0 = (x1 – 1) * 40 + (x0 + 40) = x1 * 40 + x0
将x0分配成g_tileContentVector的地址 + 40 * count,即队列结束地址,循环开始前x2为g_tileContentVector[0]的地址,循环中给各个结构体分配分配,每次循环结束就执行< /span> 让x2自增40,变成内存下一个元素的地址,直到x2 == x0时循环结束。add x2, x2, 40
开启循环管理优化后的看到文末链接中的est_bad.S,内容必须不再逐行展开,参见下文解析。
2.3 详解优化后的架构
2.3.1 感受循环提供优化的威力
看之前,我们先来感受下循环发挥化的威力。
如下所示,进入循环分配之前,阵列g_tileContentIndexList队列所有成员还都是初始化的0,执行两行代码后,发现g_tileContentIndexList[0]~g_tileContentIndexList[3]这四个集群成员已经有两个数据被分配了:

这就是循环带来优化的威力——减少了循环的次数,一次循环就能完成4个数据库的分配成员!
内容剩余60%,完整内容可点击下方链接查看:从一个crash问题展开,探索gcc编译优化细节-阿里云开发者社区
阿里云开发者社区,千万开发者的选择。百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,尽在:阿里云开发者社区-云计算社区-阿里云


















- 最新
- 最热
只看作者