记一起由 Clang 编译器优化触发的 Crash
摘要:一个有意思的 Crash 探究过程,Clang 有 GCC 没有
本文首发于 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/troubleshooting-crash-clang-compiler-optimization/

如果有人告诉你,下面的 C++ 函数会导致程序 crash,你会想到哪些原因呢?
std::string b2s(bool b) {
    return b ? "true" : "false";
}
如果再多给一些描述,比如:
- Crash 以一定的概率复现
- Crash 原因是段错误(SIGSEGV)
- 现场的 Backtrace 经常是不完整甚至完全丢失的。
- 只有优化级别在 -O2 以上才会(更容易)复现
- 仅在 Clang 下复现,GCC 复现不了
好了,一些老鸟可能已经有线索了,下面给出一个最小化的复现程序和步骤:
// file crash.cpp
#include <iostream>
#include <string>
std::string __attribute__((noinline)) b2s(bool b) {
    return b ? "true" : "false";
}
union {
    unsigned char c;
    bool b;
} volatile u;
int main() {
    u.c = 0x80;
    std::cout << b2s(u.b) << std::endl;
    return 0;
}
$ clang++ -O2 crash.cpp
$ ./a.out
truefalse,d$x4DdzRx
Segmentation fault (core dumped)
$ gdb ./a.out core.3699
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000012cfffff0d4 in ?? ()
(gdb) bt
#0  0x0000012cfffff0d4 in ?? ()
#1  0x00000064fffff0f4 in ?? ()
#2  0x00000078fffff124 in ?? ()
#3  0x000000b4fffff1e4 in ?? ()
#4  0x000000fcfffff234 in ?? ()
#5  0x00000144fffff2f4 in ?? ()
#6  0x0000018cfffff364 in ?? ()
#7  0x0000000000000014 in ?? ()
#8  0x0110780100527a01 in ?? ()
#9  0x0000019008070c1b in ?? ()
#10 0x0000001c00000010 in ?? ()
#11 0x0000002ffffff088 in ?? ()
#12 0xe2ab001010074400 in ?? ()
#13 0x0000000000000000 in ?? ()
因为 backtrace 信息不完整,说明程序并不是在第一时间 crash 的。面对这种情况,为了快速找出第一现场,我们可以试试 AddressSanitizer(ASan):
$ clang++ -g -O2 -fno-omit-frame-pointer -fsanitize=address crash.cpp
$ ./a.out
=================================================================
==3699==ERROR: AddressSanitizer: global-buffer-overflow on address 0x000000552805 at pc 0x0000004ff83a bp 0x7ffd7610d240 sp 0x7ffd7610c9f0
READ of size 133 at 0x000000552805 thread T0
    #0 0x4ff839 in __asan_memcpy (a.out+0x4ff839)
    #1 0x5390a7 in b2s[abi:cxx11](bool) crash.cpp:6
    #2 0x5391be in main crash.cpp:16:18
    #3 0x7faed604df42 in __libc_start_main (/usr/lib64/libc.so.6+0x23f42)
    #4 0x41c43d in _start (a.out+0x41c43d)
0x000000552805 is located 59 bytes to the left of global variable '<string literal>' defined in 'crash.cpp:6:25' (0x552840) of size 6
  '<string literal>' is ascii string 'false'
0x000000552805 is located 0 bytes to the right of global variable '<string literal>' defined in 'crash.cpp:6:16' (0x552800) of size 5
  '<string literal>' is ascii string 'true'
SUMMARY: AddressSanitizer: global-buffer-overflow (/home/dutor.hou/Wdir/nebula-graph/build/bug/a.out+0x4ff839) in __asan_memcpy
Shadow bytes around the buggy address:
…
...
从 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 的反汇编并给出关键注解:
(gdb) disas b2s
Dump of assembler code for function b2s[abi:cxx11](bool):
   0x00401200 <+0>:     push   %r14
   0x00401202 <+2>:     push   %rbx
   0x00401203 <+3>:     push   %rax
   0x00401204 <+4>:     mov    %rdi,%r14         # 将返回值(string)的起始地址保存到 r14
   0x00401207 <+7>:     mov    $0x402010,%ecx    # 将 "true" 的起始地址保存至 ecx
   0x0040120c <+12>:    mov    $0x402015,%eax    # 将 "false" 的起始地址保存至 eax
   0x00401211 <+17>:    test   %esi,%esi         # “测试” 参数 b 是否非零
   0x00401213 <+19>:    cmovne %rcx,%rax         # 如果 b 非零,则将 "true" 地址保存至 rax
   0x00401217 <+23>:    lea    0x10(%rdi),%rdi   # 将 string 中的 buf 起始地址保存至 rdi
                                                 # (同时也是后面 memcpy 的第一个参数)
   0x0040121b <+27>:    mov    %rdi,(%r14)       # 将 rdi 保存至 string 的 ptr 字段,即 SBO
   0x0040121e <+30>:    mov    %esi,%ebx         # 将 b 的值保存至 ebx
   0x00401220 <+32>:    xor    $0x5,%rbx         # 将 0x5 异或到 rbx(也即 ebx)
                                                 # 注意,如果 rbx 非 0 即 1,那么 rbx 保存的就是 4 或 5,
                                                 # 即 "true" 或 "false" 的长度
   0x00401224 <+36>:    mov    %rax,%rsi         # 将字符串起始地址保存至 rsi,即 memcpy 的第二个参数
   0x00401227 <+39>:    mov    %rbx,%rdx         # 将字符串的长度保存至 rdx,即 memcpy 的第三个参数
   0x0040122a <+42>:    callq  <memcpy@plt>      # 调用 memcpy
   0x0040122f <+47>:    mov    %rbx,0x8(%r14)    # 将字符串长度保存到 string::size
   0x00401233 <+51>:    movb   $0x0,0x10(%r14,%rbx,1)  # 将 string 以 '\0' 结尾
   0x00401239 <+57>:    mov    %r14,%rax         # 将 string 地址保存至 rax,即返回值
   0x0040123c <+60>:    add    $0x8,%rsp
   0x00401240 <+64>:    pop    %rbx
   0x00401241 <+65>:    pop    %r14
   0x00401243 <+67>:    retq
End of assembler dump.
到这里,问题就无比清晰了:
- clang++ 假设了 bool类型的值非0即1
- 在编译期,”true”和”false”长度已知
- 使用异或指令( 0x5 ^ false == 5,0x5 ^ true == 4)计算要拷贝的字符串的长度
- 当 bool类型不符合假设时,长度计算错误
- 因为 memcpy目标地址在栈上(仅对本例而言),因此栈上的缓冲区也可能溢出,从而导致程序跑飞,backtrace 缺失。
注:
- C++ 标准要求 bool类型至少_能够_表示两个状态:true和false,但并没有规定sizeof(bool)的大小。但在几乎所有的编译器实现上,bool都占用一个寻址单位,即字节。因此,从存储角度,取值范围为0x00-0xFF,即256个状态。
喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢]
交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~
推荐阅读
记一起由 Clang 编译器优化触发的 Crash的更多相关文章
- 转:GCC,LLVM,Clang编译器对比
		GCC,LLVM,Clang编译器对比 转自: http://www.cnblogs.com/qoakzmxncb/archive/2013/04/18/3029105.html 在XCode中, ... 
- VS编译器优化诱发一个的Bug
		VS编译器优化诱发一个的Bug Bug的背景 我正在把某个C++下的驱动程序移植到C下,前几天发生了一个比较诡异的问题. 驱动程序有一个bug,但是这个bug只能 Win32 Release 版本下的 ... 
- 深入研究Clang(四) Clang编译器的简单分析
		作者:史宁宁(snsn1984) 首先我们确定下Clang编译器的详细内容和涵盖范围.之前在<LLVM每日谈之二十 Everything && Clang driver>中 ... 
- 【转】C 编译器优化过程中的 Bug
		C 编译器优化过程中的 Bug 一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现非常诡异的行为.这使我想起以前见过的一个 GCC bug ... 
- NDK clang编译器的一个bug
		NDK clang编译器的一个bug 问题代码 float32_t Sum_float(float32_t *data, const int count) { float32x4_t res = vd ... 
- 编译器优化:何为SLP矢量化
		摘要:SLP矢量化的目标是将相似的独立指令组合成向量指令,内存访问.算术运算.比较运算.PHI节点都可以使用这种技术进行矢量化. 本文分享自华为云社区<编译器优化那些事儿(1):SLP矢量化介绍 ... 
- 探索c#之尾递归编译器优化
		阅读目录: 递归运用 尾递归优化 编译器优化 递归运用 一个函数直接或间接的调用自身,这个函数即可叫做递归函数. 递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果. 递归最重 ... 
- clang编译器简介
		本文部分内容引用: 中文维基百科. 结构化编译器前端--clang介绍. 什么是clang编译器? clang是LLVM编译器工具集的一个用于编译C.C++.Objective-C的前端.LLVM项目 ... 
- 微软承诺将在今年的 Visual C++ 更新中加入 Clang 编译器
		微软最近发布将在2015年11月 Visual C++ 更新中加入 Clang 编译器 ,Clang 开源编译器以相比GCC更快的编译速度和更优的错误提示著称. Clang关于C,C++,及Objec ... 
随机推荐
- css中渐变的分割线和自定义滚动条样式
			css中渐变的分隔线: <div style="background:linear-gradient(to left,#efefef,#b6b6b6,#efefef);height:1 ... 
- java开发三年,Java中接口的使用你得知道,不然你凭什么涨薪
			接口概述: 接口是Java语言中的一种引用类型,是方法的"集合",所以接口的内部主要就是定义方法,包含常量,抽象方法(JDK 7及以前),额外增加默认方法和静态方法(JDK 8), ... 
- FL Studio通道乐器设置页详讲
			上一篇文章我们说到FL Studio通道乐器设置页每个标签页面中几乎都是由包络.低频振荡器和滤波器这三个部分组成.我们之前只对包络进行的简单的介绍,相信很多同学对它还有其他两个的功能的了解还是云里雾里 ... 
- 如何合理利用iMindMap中的模板创建思维导图
			思维导图的制作并不是一项简单的工作,尤其是对许多工作或学习有特殊要求的朋友而言,当我们需要应对不同场景制作不同的思维导图时,总不能都靠自己从头制作,这样难度比较大也比较耗时.而iMindMap(win ... 
- Sonar检测Math.abs(new Random().nextInt()) “Use the original value instead”
			今天早上旁边同事喊我看一个Sonar检测出的问题: 当时看了好几眼没觉得这个有太大问题,于是又看了下Sonar建议: 这是说Math.abs()方法使用在数字上面可能返回最小值,觉得这个挺有意思的,于 ... 
- selenium元素定位检查
			自动化测试的重点就是定位元素,然而定位元素没办法一步一步运行调试检查,每次都需要从代码的开始运行,检查起来效率极低. 一直想找一个能检查唯一性工具或者方法.百度发现一篇文章https://blog.c ... 
- 2018-div-matrix 题解(打表)
			题目链接 题目大意 要你求有多少个满足题目条件的矩阵mod 1e9+7 \(a[1][1]=2018\;\;a[i][j]为a[i-1][j]和a[i][j-1]的因子\) 题目思路 dp也就图一乐, ... 
- Java蓝桥杯——逻辑推理练习题
			逻辑推理题 谁是贼? 公安人员审问四名窃贼嫌疑犯.已知,这四人当中仅有一名是窃贼,还知道这四人中每人要么是诚实的,要么总是说谎.在回答公安人员的问题中: 甲说:"乙没有偷,是丁偷的.&quo ... 
- c++11-17 模板核心知识(九)—— 理解decltype与decltype(auto)
			decltype介绍 为什么需要decltype decltype(auto) 注意(entity) 与模板参数推导和auto推导一样,decltype的结果大多数情况下是正常的,但是也有少部分情况是 ... 
- select监听服务端
			# can_read, can_write, _ = select.select(inputs, outputs, None, None)## 第一个参数是我们需要监听可读的套接字, 第二个参数是我们 ... 
