记一起由 Clang 编译器优化触发的 Crash - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
NebulaGraph
V2EX    推广

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

  •  1
     
  •   NebulaGraph 2020-12-10 09:58:31 +08:00 2898 次点击
    这是一个创建于 1848 天前的主题,其中的信可能已经有所发展或是发生改变。

    摘要:一个有意思的 Crash 探究过程,Clang 有 GCC 没有

    本文首发于 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/troubleshooting-crash-clang-compiler-optimization/

    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 中存储的是一个 01 之外的值[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. 

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

    1. clang++ 假设了 bool 类型的值非 01
    2. 在编译期,”true””false” 长度已知
    3. 使用异或指令( 0x5 ^ false == 5, 0x5 ^ true == 4)计算要拷贝的字符串的长度
    4. bool 类型不符合假设时,长度计算错误
    5. 因为 memcpy 目标地址在栈上(仅对本例而言),因此栈上的缓冲区也可能溢出,从而导致程序跑飞,backtrace 缺失。

    注:

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

    喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂♀ [手动跪谢]

    交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~

    推荐阅读

    12 条回复    2020-12-11 01:45:27 +08:00
    codehz
        1
    codehz  
       2020-12-10 12:59:01 +08:00 via Android   9
    看到 union 就可以结案了...
    标准约定,在没有共同初始化序列的类型上,不可以在 union 的不同 field 上存取数据,也就是你放什么,就只能拿什么,不然就是未定义行为
    (这个规则可以扩展到任何重新解释内存的尝试,比如用指针强制转换也是未定义的,但是用 static_cast 转基础数字类型是合法的)
    codehz
        2
    codehz  
       2020-12-10 13:05:14 +08:00 via Android
    bool 类型即使在实现上不能只用一个 bit,但是标准约定,只有两个合法的状态,其他的状态都是非法的(
    不开优化没炸只是因为 cpu 指令刚好有非零和零的条件跳转,能匹配标准规定的语义,所以多数情况不会出问题
    但不代表你放别的数值就是合法的布尔类型了
    nightwitch
        3
    nightwitch  
       2020-12-10 13:20:56 +08:00
    #1 加一

    如果你仔细阅读 cppreference 的 union 章节,你会找到这样一句。
    It's undefined behavior to read from the member of the union that wasn't most recently written. Many compilers implement, as a non-standard language extension, the ability to read inactive members of a union.

    读如果读 union 最近被写的成员以外的成员是未定义行为,虽然许多编译器提供了非标准的扩展用于读 union 的非活跃成员。

    触发了未定义行为这还能说啥
    6ufq0VLZn0DDkL80
        4
    6ufq0VLZn0DDkL80  
       2020-12-10 13:36:03 +08:00
    不优化不炸,优化就炸,ub 无疑了
    fuxiuyin
        5
    fuxiuyin  
       2020-12-10 13:41:51 +08:00
    很好奇这个优化器是怎么写的。如果是 return b? "aaaaaaaa" : "bbbbbbbbbbbb" 他会优化成(0xc ^ (b << 2))?
    e583409
        6
    e583409  
       2020-12-10 14:45:17 +08:00
    哈哈 在 v 站见到你
    ivan_wl
        7
    ivan_wl  
       2020-12-10 16:59:50 +08:00
    @codehz c 标准中也有类似的规定么?
    typetraits
        8
    typetraits  
       2020-12-10 17:03:58 +08:00
    所以推荐使用 std::variant
    codehz
        9
    codehz  
       2020-12-10 17:12:23 +08:00
    @ivan_wl #7 c 可以使用 union 作为类型双关使用,但是 c 里也没严格的 bool 类型啊(标准提供的 stdbool 只是给你一个 bool 的 alias,实际 c 标准没有规定 bool 的行为)
    不过即使允许类型双关,还有 strict alias 规则等着你(
    ivan_wl
        10
    ivan_wl  
       2020-12-10 17:52:13 +08:00
    @codehz c 中用 union 做类型双关不违背 strict aliasing 的吧
    codehz
        11
    codehz  
       2020-12-10 18:32:30 +08:00
    @ivan_wl #10 strict aliasing 是说指针(数组)一类的间接访问的问题,所以只要不涉及指针 /数组类型(或者取地址然后解引用),就是合法的(不过转换结果仍然是未定义的)
    wty
        12
    wty  
       2020-12-11 01:45:27 +08:00 via Android
    如果是写库或者读取文件的话,bool 传入奇怪的值这种有办法解决吗?我经常会写 xxx ? true:false 这种,感觉挺蠢,而且可能还没用的样子。。。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2364 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 11:35 PVG 19:35 LAX 03:35 JFK 06:35
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86