千家信息网

如何理解Clang编译器优化触发的Crash

发表于:2026-11-01 作者:千家信息网编辑
千家信息网最后更新 2026年11月01日,本篇内容主要讲解"如何理解Clang编译器优化触发的Crash",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"如何理解Clang编译器优化触发的Crash"
千家信息网最后更新 2026年11月01日如何理解Clang编译器优化触发的Crash

本篇内容主要讲解"如何理解Clang编译器优化触发的Crash",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"如何理解Clang编译器优化触发的Crash"吧!

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

std::string b2s(bool b) {    return b ? "true" : "false";}

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

  • Crash 以一定的概率复现

  • Crash 原因是段错误(SIGSEGV)

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

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

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

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

// file crash.cpp#include #include 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.outtruefalse,d$x4DdzRxSegmentation fault (core dumped)$ gdb ./a.out core.3699Core 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 0x7ffd7610c9f0READ 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 '' defined in 'crash.cpp:6:25' (0x552840) of size 6  '' is ascii string 'false'0x000000552805 is located 0 bytes to the right of global variable '' defined in 'crash.cpp:6:16' (0x552800) of size 5  '' is ascii string 'true'SUMMARY: AddressSanitizer: global-buffer-overflow (/home/dutor.hou/Wdir/nebula-graph/build/bug/a.out+0x4ff839) in __asan_memcpyShadow 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 b2sDump 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   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>:    retqEnd of assembler dump.

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

  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 个状态。

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

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

到此,相信大家对"如何理解Clang编译器优化触发的Crash"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

地址 长度 字符 字符串 程序 问题 编译 参数 类型 起始 编译器 函数 缓冲区 C++ 缓冲 信息 兴趣 内容 原因 指令 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 互联网高科技 互联网科技公司简介 互联网科技活动banner 鸠鸠互联网科技 工业互联网科技新基建 互联网的科技创新 奇酷互联网科技公司 易通互联网络科技有限公司 广州紫鲸互联网科技 科技互联网创业计划书 互联网科技公司介绍 厦门比较好的科技互联网公司 徐州市互联网科技有限公司 互联网方面的科技前沿 2021世界互联网领先科技成果发布 山东大美朱楼互联网科技有限公司 广东鹅鹅鹅互联网科技有限公司 保山浩联互联网科技有限公司 杭州科技互联网公司有哪些 互联网领先科技成果宣传视频 九亚互联网科技 美国互联网黑科技 上海卓越科技 互联网 江苏南京365互联网科技 彤创互联网科技有限公司 河南猎农互联网科技有限公司 广东工业互联网大赛深圳学泰科技 宇信科技在杭州互联网排第几 北京军民互联网科技公司 武汉亿视互联网科技有限公司
0