摘要:一個有意思的 Crash 探究過程,Clang 有 GCC 沒有ios
本文首發於 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/troubleshooting-crash-clang-compiler-optimization/git
若是有人告訴你,下面的 C++ 函數會致使程序 crash,你會想到哪些緣由呢?github
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
,問題就能夠獲得修復。post
想要解答這個問題,咱們不得不看下 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.
到這裏,問題就無比清晰了:ui
- 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 拉你進交流羣~~