从一个崩溃问题展开,探索gcc编译优化细节

从一个崩溃问题展开,探索gcc编译优化细节将 x0 分配成 g tileContentV 的地址 40 count 即队列结束地址 循环开始前 x2 为 g tileContentV 的地址 循环中给各个结构体分配分配 每次循环结束就执行 lt span gt 让

大家好,欢迎来到IT知识分享网。

点击链接阅读原文,获取更多技术内容:从一个crash问题展开,探索gcc编译优化细节-阿里云开发者社区

问题分析的过程也是技术之路的成长,本文以一个gcc编译优化引发的崩溃为切入点,逐步展开对编译器优化细节的探索之路,在分析过程中打开了新世界的大门……

作者 | 瞳尘

来源 | 阿里云开发者公众号

背景:一次平平无奇的车祸

去年,客户提了个bug,并甩了给我们一个分段错误截图,必现崩溃。

从一个崩溃问题展开,探索gcc编译优化细节

这个必现问题我根本不慌的,因为有一个伟人曾经说过:“必现问题都不是问题!”

段错误,无非就是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

执行一下,发现一切正常

从一个崩溃问题展开,探索gcc编译优化细节

再用O3搞一下:

g++ -O3 -S -o mainO3.s main.cpp

g++ -o mainO3 mainO3.s

问题复现!

从一个崩溃问题展开,探索gcc编译优化细节

1.4 编译优化选项排查

先查一下当前版本gcc编译器O2和O3分别打开哪些编译优化,使用命令:

gcc/g++ -Q -O<数字>; –help=优化器

例如:

gcc/g++ -Q -O2 –help=优化器

gcc/g++ -Q -O3 –help=优化器

差异如下(左边O3,右边O2):

从一个崩溃问题展开,探索gcc编译优化细节

可以看到除了上面官网说的几个选项外,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再查一下:

从一个崩溃问题展开,探索gcc编译优化细节

这样最差也可以挨个关闭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 树循环向量化

从一个崩溃问题展开,探索gcc编译优化细节

说明问题环境中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

从一个崩溃问题展开,探索gcc编译优化细节

可以看到就是循环赋值的代码(行号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]这四个集群成员已经有两个数据被分配了:

从一个崩溃问题展开,探索gcc编译优化细节

这就是循环带来优化的威力——减少了循环的次数,一次循环就能完成4个数据库的分配成员!

内容剩余60%,完整内容可点击下方链接查看:从一个crash问题展开,探索gcc编译优化细节-阿里云开发者社区

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/179805.html

(0)
上一篇 2025-06-02 11:15
下一篇 2025-06-02 11:20

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信