记一起由 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 拉你进交流群~~
推荐阅读

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
时序数据库作为量化金融研究平台的优势在哪里?
大数据下 金融行业面临的四大痛点 当前整个金融市场环境日趋严峻,监管越来越严,无论是银行的零售、公司、交易或同业业务,都需要直面营销与风险的效率与准确率的问题。越来越多的金融机构都希望依靠大数据来拉动业务模式进行创新,但是由于行业特点,存在着四大痛点。 第一个痛点是数据来源多样化,需要整合后分析。金融行业的数据来源通常包含三大类:业务信息数据、行为数据和第三方数据。这些来源的数据包括结构化数据和非结构化的数据,在进行数据分析时通常需要进行一定程度的整合,例如客户信息与客户行为数据的整合,企业内部交易信息与上下游合作企业的交易信息的整合等等。 第二个痛点是技术和业务人员各司其职,部门协作成本高。金融行业的企业通常有专门的信息中心来进行数据的管理,这些技术人才通常精通数据分析技术,但对业务中涉及到的各种指标并不熟悉。业务管理人员则正好相反,精通业务指标的运用,但对数据分析技术难以掌握。这种场景常常导致一个分析报告的制作需要多个部门间反复沟通,期间的时间、人员成本巨大。 第三个痛点是金融行业数据量级大,分析性能要求高。众所周知,金融行业的数据量级大,通常总存储量达到TB级别,而单次计算数据量...
- 下一篇
抖音爬虫教程,AndServer+Service 打造 Android 服务器实现 so 文件调用
随着 Android 移动安全的高速发展,不管是为了执行效率还是程序的安全性等,关键代码下沉 native 层已成为基本操作。 native 层的开发就是通指的 JNI/NDK 开发,通过 JNI 可以实现 java 层和 native 层(主要是 C/C++ )的相互调用,native 层经编译后产生 so 动态链接库,so 文件具有可移植性广,执行效率高,保密性强等优点。 那么问题来了,如何调用 so 文件显得异常重要,当然你也可以直接分析 so 文件的伪代码,利用强悍的编程功底直接模拟关键操作,但是我想对于普通人来说头发还是比较重要的。 当前调用 so 文件的主流操作应该是: 1,基于 Unicorn 的各种实现(还在学习中,暂且不表) 2,Android 服务器的搭建,在 App 内起 http 服务完成调用 so 的需求(当然前提是过了 so 的效验等操作) 至于为什么选用 AndServer,好吧,不为什么,只是因为搜索到了它 为什么结合 Service,在学习 Android 开发的时候了解到了 Service 的生命周期,个人理解用 Service 去创建 Http...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS7设置SWAP分区,小内存服务器的救世主
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Hadoop3单机部署,实现最简伪集群
- CentOS7安装Docker,走上虚拟化容器引擎之路