记一起由 Clang 编译器优化触发的 Crash

TeamD
发布于 2022-10-8 14:11
浏览
0收藏

如果有人告诉你,下面的 C++ 函数会导致程序 crash,你会想到哪些原因呢?

记一起由 Clang 编译器优化触发的 Crash-鸿蒙开发者社区

如果再多给一些描述,比如:

 

●  Crash 以一定的概率复现

●  Crash 原因是段错误(SIGSEGV)

●  现场的 Backtrace 经常是不完整甚至完全丢失的。

●  只有优化级别在 -O2 以上才会(更容易)复现

●  仅在 Clang 下复现,GCC 复现不了

好了,一些老鸟可能已经有线索了,下面给出一个最小化的复现程序和步骤:

记一起由 Clang 编译器优化触发的 Crash-鸿蒙开发者社区

记一起由 Clang 编译器优化触发的 Crash-鸿蒙开发者社区

因为 backtrace 信息不完整,说明程序并不是在第一时间 crash 的。面对这种情况,为了快速找出第一现场,我们可以试试 AddressSanitizer(ASan):

记一起由 Clang 编译器优化触发的 Crash-鸿蒙开发者社区

从 ASan 给出的信息,我们可以定位到是函数  b2s(bool)  在读取字符串常量  "true"  的时候,发生了“全局缓冲区溢出”。好了,我们再次以上帝视角审视一下问题函数和复现程序,“似乎”可以得出结论:因为  b2s  的布尔类型参数  b  没有初始化,所以  b  中存储的是一个  0  和  1  之外的值[1]。那么问题来了,为什么  b  的这种取值会导致“缓冲区溢出”呢?感兴趣的可以将  b  的类型由  bool  改成  char  或者  int ,问题就可以得到修复。

 

要解答这个问题,我们不得不看下 clang++ 为  b2s  生成了怎样的指令(之前我们提到 GCC 下没有出现 crash,所以问题可能和代码生成有关)。在此之前,我们应该了解:

 

●  样例程序中, b2s  的返回值是一个临时的  std::string  对象,是保存在栈上的

●  C++ 11 之后,GCC 的  std::string  默认实现使用了 SBO(Small Buffer Optimization),其定义大致为  std::string{ char *ptr; size_t size; union{ char buf[16]; size_t capacity}; } 。对于长度小于  16  的字符串,不需要额外申请内存。

OK,那我们现在来看一下  b2s  的反汇编并给出关键注解:

记一起由 Clang 编译器优化触发的 Crash-鸿蒙开发者社区

到这里,问题就无比清晰了:

 

 1. clang++ 假设了  bool  类型的值非  0  即  1  

2. 在编译期, ”true” 和  ”false” 长度已知

3. 使用异或指令( 0x5 ^ false == 5, 0x5 ^ true == 4 )计算要拷贝的字符串的长度

4. 当  bool  类型不符合假设时,长度计算错误

5. 因为  memcpy  目标地址在栈上(仅对本例而言),因此栈上的缓冲区也可能溢出,从而导致程序跑飞,backtrace 缺失。

注:

1. C++ 标准要求  bool  类型至少能够表示两个状态:  true  和  false  ,但并没有规定  sizeof(bool)  的大小。但在几乎所有的编译器实现上, bool  都占用一个寻址单位,即字节。因此,从存储角度,取值范围为  0x00-0xFF ,即  256  个状态。

分类
标签
已于2022-10-8 14:11:31修改
收藏
回复
举报
回复
    相关推荐