記一起由 Clang 編譯器優(yōu)化觸發(fā)的 Crash

摘要:一個有意思的 Crash 探究過程荡澎,Clang 有 GCC 沒有

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

troubleshooting-crash-clang-compiler-optimization

如果有人告訴你艾少,下面的 C++ 函數(shù)會導(dǎo)致程序 crash,你會想到哪些原因呢讹躯?

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

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

  • Crash 以一定的概率復(fù)現(xiàn)
  • Crash 原因是段錯誤(SIGSEGV)
  • 現(xiàn)場的 Backtrace 經(jīng)常是不完整甚至完全丟失的尘惧。
  • 只有優(yōu)化級別在 -O2 以上才會(更容易)復(fù)現(xiàn)
  • 僅在 Clang 下復(fù)現(xiàn)炬转,GCC 復(fù)現(xiàn)不了

好了,一些老鳥可能已經(jīng)有線索了述吸,下面給出一個最小化的復(fù)現(xiàn)程序和步驟:

// 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 的。面對這種情況蝌矛,為了快速找出第一現(xiàn)場道批,我們可以試試 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 給出的信息,我們可以定位到是函數(shù) b2s(bool) 在讀取字符串常量 "true" 的時候入撒,發(fā)生了“全局緩沖區(qū)溢出”隆豹。好了,我們再次以上帝視角審視一下問題函數(shù)和復(fù)現(xiàn)程序茅逮,“似乎”可以得出結(jié)論:因為 b2s 的布爾類型參數(shù) b 沒有初始化璃赡,所以 b 中存儲的是一個 01 之外的值[1]。那么問題來了献雅,為什么 b 的這種取值會導(dǎo)致“緩沖區(qū)溢出”呢碉考?感興趣的可以將 b 的類型由 bool 改成 char 或者 int,問題就可以得到修復(fù)挺身。

想要解答這個問題侯谁,我們不得不看下 clang++ 為 b2s 生成了怎樣的指令(之前我們提到 GCC 下沒有出現(xiàn) crash,所以問題可能和代碼生成有關(guān))。在此之前良蒸,我們應(yīng)該了解:

  • 樣例程序中技扼,b2s 的返回值是一個臨時的 std::string 對象,是保存在棧上的
  • C++ 11 之后嫩痰,GCC 的 std::string 默認實現(xiàn)使用了 SBO(Small Buffer Optimization)剿吻,其定義大致為 std::string{ char *ptr; size_t size; union{ char buf[16]; size_t capacity}; }。對于長度小于 16 的字符串串纺,不需要額外申請內(nèi)存丽旅。

OK,那我們現(xiàn)在來看一下 b2s 的反匯編并給出關(guān)鍵注解:

(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         # “測試” 參數(shù) b 是否非零
   0x00401213 <+19>:    cmovne %rcx,%rax         # 如果 b 非零纺棺,則將 "true" 地址保存至 rax
   0x00401217 <+23>:    lea    0x10(%rdi),%rdi   # 將 string 中的 buf 起始地址保存至 rdi
                                                 # (同時也是后面 memcpy 的第一個參數(shù))
   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 的第二個參數(shù)
   0x00401227 <+39>:    mov    %rbx,%rdx         # 將字符串的長度保存至 rdx巨朦,即 memcpy 的第三個參數(shù)
   0x0040122a <+42>:    callq  <memcpy@plt>      # 調(diào)用 memcpy
   0x0040122f <+47>:    mov    %rbx,0x8(%r14)    # 將字符串長度保存到 string::size
   0x00401233 <+51>:    movb   $0x0,0x10(%r14,%rbx,1)  # 將 string 以 '\0' 結(jié)尾
   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++ 假設(shè)了 bool 類型的值非 01
  2. 在編譯期糊啡,”true””false” 長度已知
  3. 使用異或指令( 0x5 ^ false == 5, 0x5 ^ true == 4)計算要拷貝的字符串的長度
  4. bool 類型不符合假設(shè)時拄查,長度計算錯誤
  5. 因為 memcpy 目標地址在棧上(僅對本例而言),因此棧上的緩沖區(qū)也可能溢出棚蓄,從而導(dǎo)致程序跑飛堕扶,backtrace 缺失。

注:

  1. C++ 標準要求 bool 類型至少能夠表示兩個狀態(tài): truefalse 梭依,但并沒有規(guī)定 sizeof(bool) 的大小稍算。但在幾乎所有的編譯器實現(xiàn)上, bool 都占用一個尋址單位役拴,即字節(jié)糊探。因此,從存儲角度扎狱,取值范圍為 0x00-0xFF侧到,即 256 個狀態(tài)勃教。

喜歡這篇文章淤击?來來來,給我們的 GitHub 點個 star 表鼓勵啦~~ ???♂????♀? [手動跪謝]

交流圖數(shù)據(jù)庫技術(shù)故源?交個朋友污抬,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你進交流群~~

推薦閱讀

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子印机,更是在濱河造成了極大的恐慌矢腻,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件射赛,死亡現(xiàn)場離奇詭異多柑,居然都是意外死亡,警方通過查閱死者的電腦和手機楣责,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門竣灌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秆麸,你說我怎么就攤上這事初嘹。” “怎么了沮趣?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵屯烦,是天一觀的道長。 經(jīng)常有香客問我房铭,道長驻龟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任育叁,我火速辦了婚禮迅脐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘豪嗽。我一直安慰自己谴蔑,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布龟梦。 她就那樣靜靜地躺著隐锭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪计贰。 梳的紋絲不亂的頭發(fā)上钦睡,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音躁倒,去河邊找鬼荞怒。 笑死,一個胖子當著我的面吹牛秧秉,可吹牛的內(nèi)容都是我干的褐桌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼象迎,長吁一口氣:“原來是場噩夢啊……” “哼荧嵌!你這毒婦竟也來了呛踊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤啦撮,失蹤者是張志新(化名)和其女友劉穎谭网,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赃春,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡愉择,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了织中。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薄辅。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖抠璃,靈堂內(nèi)的尸體忽然破棺而出站楚,到底是詐尸還是另有隱情,我是刑警寧澤搏嗡,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布窿春,位于F島的核電站,受9級特大地震影響采盒,放射性物質(zhì)發(fā)生泄漏旧乞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一磅氨、第九天 我趴在偏房一處隱蔽的房頂上張望尺栖。 院中可真熱鬧,春花似錦烦租、人聲如沸延赌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挫以。三九已至,卻和暖如春窃祝,著一層夾襖步出監(jiān)牢的瞬間掐松,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工粪小, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留大磺,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓探膊,卻偏偏與公主長得像杠愧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子突想,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容