上一課,調查員學會了看屍體——搞懂 %ripstackcall/ret、撞牆暴斃是怎麼回事。但盯著單一屍體看,找不到兇手。

這一課要做兩件事:

  1. 進命案現場:那份封存的驗屍報告(core dump)是什麼、為什麼某些證物死後還留得住
  2. 身份升級:從法醫變成流行病學家,用全城一年的死亡資料,破掉第一個 bug

這是三部曲的中間課。學完這堂,調查員會抓到第一隻鬼。剩下那群死者——死在「瞬間傳送」當下的——就是 Post C 的主菜了 ╰⁠(⁠°⁠▽⁠°⁠)⁠╯


🏰 第 0 層:回顧 + 今天的任務

⚔️ Level 0 / 8 當機的驗屍課(中)
0% 完成

快速複習 Post A:

  • 程式當機 = 命案 / 暴斃
  • stack = 一疊拜訪便條,每進一層 function 壓一張,寫著「辦完回哪」
  • call / ret = 出門壓便條 / 撕便條照著回家
  • segfault = 便條被改成無效地址 → ret 照著跳 → 撞牆暴斃
  • C++ 是沒護欄的城市,誰都能改別人的便條

OpenAI 那批命案的狀況是:死者的回程便條被改成 NULL(空號),或者 %rsp 莫名偏移 8 個位元組。調查員知道「怎麼死的」,但不知道「誰幹的」。

盯著一具屍體猛看,看不出兇手。今天的任務:進命案現場、學會新的調查工具、然後換一個身份辦案。

小測驗

Post A 留下的核心問題是什麼?


🏰 第 1 層:core dump = 封存的命案現場

⚔️ Level 1 / 8 當機的驗屍課(中)
13% 完成

程式當機的時候,作業系統可以把「程式死亡那一瞬間的所有狀態」存成一個檔案。這個檔案叫做 core dump

Rockset 的工程師用 folly(Facebook 的 C++ 函式庫)的 fatal signal handler 來處理當機:當機一發生,就自動把 stack trace(呼叫鏈)記進 log,同時把 core dump 上傳到 Azure blob storage 保存起來。

Rockset 的查詢節點有做 replicated(複製),所以單一當機對使用者的衝擊不大。但每一個當機都代表一個要修的 bug。

Mogu 插嘴:

core dump 這個名字來自古早年代——那時候磁芯記憶體(magnetic-core memory)是主流,所以「把記憶體倒出來」叫 core dump。現代電腦早就不用磁芯了,但這個詞就這樣留下來了。工程師之間說「幫我抓那個 core」,講的就是這份封存的命案現場檔案。

小測驗

core dump 是什麼?


🏰 第 2 層:拜訪鏈——從 %rbp 追出「誰找誰來」

⚔️ Level 2 / 8 當機的驗屍課(中)
25% 完成

Post A 提過一個細節:Rockset 編譯時開了 -fno-omit-frame-pointer 這個選項。現在來解釋這為什麼重要。

每一個 stack frame(便條)裡面,除了回程地址,還存了「上一層的 %rbp」。這讓調查員可以從最上面那張便條開始,沿著 %rbp 一路往上追,看到完整的呼叫鏈:誰叫了誰、誰又叫了誰。

有些編譯器為了效能會把 %rbp 省略掉(開 -fomit-frame-pointer),這樣每層 function 少存一個暫存器,快一點點。但代價是:當機的時候,調查員很難重建呼叫鏈。

Rockset 反過來開了 -fno-omit-frame-pointer——當初可能只是為了方便偵錯。結果這個選項在這場命案調查裡救了工程師一命。沒有這條拜訪鏈,調查員連「死者生前見過誰」都拼不出來。

Mogu OS:

Post A 說「編譯選項救了他們一命」的伏筆,就是這個。-fno-omit-frame-pointer 看起來只是一個偵錯方便的選項,但在「便條被改、整個 stack 都可能壞掉」的情境下,這條拜訪鏈是調查員唯一能重建案發經過的工具。這就是為什麼很多 production 系統會刻意開這個選項,犧牲一點點效能換取可觀測性。

小測驗

拜訪鏈(用 %rbp 串成的 linked list)的功能是什麼?


🏰 第 3 層:封鎖線(red zone)——為什麼死後證物還在

⚔️ Level 3 / 8 當機的驗屍課(中)
38% 完成

現在要講一個關鍵細節:為什麼調查員可以看到「已經 return 掉的 function」的便條內容?

按理說,function B 跑完、執行 ret 之後,它的 stack frame 就「不再有效」了——那塊記憶體可以被下一個 function 覆蓋。但工程師在報告裡寫「X 這個剛被彈出的 stack frame 看起來正常,只差回程地址是 NULL」。他們怎麼看到的?

答案是 red zone(封鎖線)。

當機發生時,folly 的 signal handler(下一層會講)會在同一個 thread 的 stack 上跑。這個 handler 會踩亂已經 return 掉的舊便條——但封鎖線內(最後 128 位元組)留得住

這就是為什麼工程師能說:「X 這個剛被彈出的 stack frame 看起來正常,只差回程地址是 NULL。」封鎖線幫他們保存了最後一點證物。

Mogu 想補充:

red zone 這個名字很有畫面感——「紅色區域,禁止進入」。在 System V ABI 的定義裡,這 128 位元組是「leaf function(不再呼叫別人的 function)可以拿來放區域變數、不用調整 %rsp」的空間。對調查員來說,它的價值是:當機後第一個跑進來的人(signal handler)會踩亂現場,但封鎖線內的證物撐得住。

小測驗

red zone(封鎖線)在當機調查中的價值是什麼?


🏰 第 4 層:第一個趕到現場的人(signal handler

⚔️ Level 4 / 8 當機的驗屍課(中)
50% 完成

程式當機的時候,kernel 會送一個 signal(訊號)給程式。以 segfault 來說,kernel 會送 SIGSEGVsegmentation violation,區段違規)。

程式可以事先註冊一個 signal handler——當這個訊號來的時候,先別直接死,先讓程式自己跑一段指定的程式碼。Rockset 用 folly 的 fatal signal handler,會在當機發生時記錄 stack trace、準備 core dump

問題是:這個 handler 就跑在當機的那條 thread 的 stack——跟死者同一條命脈。

現在來鋪一個伏筆:Rockset 用 signal 用得比一般程式兇。

他們有一個叫 coarse_thread_cputime_clock 的機制:用 timer_create 這個 API,每隔幾毫秒 CPU 時間,就送一個 SIGUSR2signal user 2,使用者自訂訊號 2 號)到每個 thread,讓 handler 更新一個 thread-local 的 CPU 時間值。這是為了追蹤「這個查詢總共用了多少 CPU 時間」。

這個「每幾毫秒就送 signal」的設計,現在只當背景知識記著。到 Post C,它會變成兇器的一部分。

Mogu 真心話:

SIGUSR2 的名字裡,SIGsignalUSR2user 2——「使用者自訂訊號 2 號」。Unix 系統留了兩個訊號(SIGUSR1SIGUSR2)讓程式自己定義用途。Rockset 拿 SIGUSR2 來做 CPU 時間追蹤。每幾毫秒就送一次——這個頻率在一般程式裡算非常兇。記住這個設定,Post C 會用到。

小測驗

signal handler 在當機調查中的角色是什麼?


🏰 第 5 層:法醫 vs 流行病學家

⚔️ Level 5 / 8 當機的驗屍課(中)
63% 完成

現在來到這堂課的核心。

OpenAI 的工程師卡了好幾天。他們:

  • 仔細檢視幾個 core dump
  • 在一個 misaligned-%rsp 的案例上深挖——重建案發前的歷史、從 stack 和暫存器內容倒推
  • kernel 原始碼、讀 Azure 專屬的 kernel 修補程式
  • 跑壓力測試

什麼都試了。沒結果。

這是整個調查的轉捩點:他們決定不再盯著單一 core dump,而是去蒐集高品質的族群資料

為什麼會卡這麼久?因為他們一直假設「這是同一個 bug」。有些案例的回程地址是 NULL,有些案例的 %rsp 偏移 8 位元組——症狀有點像,又不完全一樣。工程師把它們當成同一種 bug 在抓,每個假設都有反例,怎麼抓都抓不到。

流行病學家會問:這真的是一種病嗎?還是兩種病混在一起,只是剛好同時被發現?

Mogu 認真說:

這是我覺得這篇 OpenAI 原文最精彩的洞見。很多難除錯(debug)的問題,難的不是技術細節,而是「你以為在解一個問題,其實同時有兩個不相關的問題混在一起」。工程師的直覺是深挖單一案例——法醫模式。但當假設反覆被推翻,該做的是退一步,蒐集全體資料,看有沒有 pattern。這招在任何領域都適用。

小測驗

「法醫模式」卡住的時候,該怎麼突破?


🏰 第 6 層:建乾淨的族群資料

⚔️ Level 6 / 8 當機的驗屍課(中)
75% 完成

決定換成流行病學家模式之後,第一個問題是:資料從哪來?

工程師一開始想用 log 文字搜尋,自動抓所有案例。但 stack-corruption 的 bug 有個麻煩:log 本身就壞掉了。因為 stack 被改壞,所以記出來的 stack trace 也是亂的。用文字搜尋會有 false positive(誤報:抓到不是這個 bug 的案例)也有漏抓(這個 bug 但 log 長得不一樣)。

他們改成這樣:

  1. 讓 ChatGPT 寫一個腳本(script
  2. 腳本下載每個 core 檔的開頭(不用下載整個 GB 等級的檔案)
  3. 抽出暫存器的值
  4. 用 log 濾掉已知的 false positive
  5. 自動把每個當機標成:return-to-null(回程地址是 NULL)/ misaligned-stack%rsp 對不齊)/ other(其他)
  6. 平行跑過過去一整年所有 production Rockset 的 core dump

這就是「全城一年的屍體全建檔分類」。

乾淨資料一出來,相關性立刻浮現。

原本以為是一種 bug,其實是兩個完全不相關的當機族群。

族群特徵
return-to-null散在很多 cluster、很多地區;最近變多,但沒有明確起始日;沒有乾淨的基礎設施邊界
misaligned-stack全來自一個地區;有明確起始日;從不發生在開機很久的節點;雖然橫跨多台 Azure VM,但 pattern 像「某一台實體機器壞掉,害到剛好排到它上面的 VM」

兩群的 pattern 完全不一樣。

工程師之前一直把它們混在一起看,所以每個假設都有反例——因為用 return-to-null 的假設去解釋 misaligned-stack 的案例,當然解釋不通。分開來看,答案就清楚了。

Mogu 補個刀:

「以為是一種 bug,其實是兩種」——這個教訓太重要了。工程師一開始(錯誤地)排除了硬體問題,因為「跨多個地區、多種硬體都有」。但那是因為把兩種 bug 混在一起看。分開之後,misaligned-stack 那群的 pattern 就很清楚:全來自同一個地區、有明確起始日、只發生在新開機的節點。這根本就是「某一台實體機器壞掉」的典型樣貌。

小測驗

建乾淨資料集之後,工程師發現了什麼?


🏰 最終關:Bug #1——毒井(John Snow)

⚔️ Level 7 / 8 當機的驗屍課(中)
88% 完成

1854 年,倫敦爆發霍亂,幾天內死了幾百人。當時主流理論是「瘴氣」——壞空氣導致疾病。

一個叫 John Snow 的醫生不相信。他畫了一張地圖,標出每個死亡案例的住址,發現一個 pattern:死者集中在 Broad Street 那口水井附近

John Snow 跑去檢查那口井,發現井水被附近的化糞池污染了。他說服當局拆掉那口井的把手,讓大家沒辦法再從那裡打水。

霍亂停了。

這是流行病學的起源故事。John Snow 沒有顯微鏡、沒有細菌培養,他用的是族群資料和模式辨識(pattern recognition

工程師把那台主機加進 denylist(封井)。

他們在受控環境裡壓力測試好幾週,也沒辦法重現那個暫存器被改壞的現象——不知道 CPU 到底怎麼壞的。但這不重要。把那台機器下線後,misaligned-stack 的當機就消失了。

就像 John Snow 不需要知道霍亂弧菌長什麼樣子。他只需要知道:拆掉那口井的把手,死亡就停了。

工程師還順手做了幾件事:

  • 改良 fatal signal handler,讓它記下暫存器狀態。以後光看 log 就能偵測類似問題,不用下載整個 core dump
  • control plane(控制面),讓 VM 盡量重用而不是回收。這樣比較容易抓「壞節點」——同一台實體機器上的 VM 死亡率異常高,就很可疑
  • 更新 runbook(操作手冊)

一口毒井,封井就好。

Mogu OS:

John Snow 的故事是流行病學的經典開場。1854 年那個年代,主流相信「瘴氣」,Snow 用資料打臉。他沒有顯微鏡、沒有培養皿,只有一張地圖和一堆死亡紀錄。這跟 OpenAI 工程師的處境很像:他們沒辦法在實驗室重現那個 bug,但有乾淨的資料,就夠了。現代醫學把 John Snow 叫做「流行病學之父」——而他的方法論,在為基礎設施除錯的時候一樣好用。


現在 misaligned-stack 那群分離掉了,剩下的 return-to-null 那群突然好懂多了。

工程師之前「排除了 exception 展開(exception unwinding)這個可能」,是因為以為找到反例:有些當機發生在「根本沒用 exception」的程式碼路徑裡。

但那些反例——全都來自壞硬體那一群

把壞硬體那群拿掉重看,剩下的當機 全都發生在 C++ 丟 exception、正在展開 stack 的當下。

這到底是什麼?那個「瞬間傳送」是怎麼回事?兇器怎麼下手?

Post C 見。

小測驗

工程師怎麼破掉 bug #1(壞主機)?


🎓 通關小結

調查員在這堂課學會了:

概念一句話解釋
core dump封存的命案現場——程式當機瞬間的完整狀態快照
拜訪鏈%rbp 串起來的 linked list,重建「誰找誰來」
red zone封鎖線,kernel 答應不踩;當機後這 128 位元組的證物還留得住
signal handler第一個趕到現場的人,會踩亂封鎖線外的舊便條
法醫 vs 流行病學家盯單一案例 vs 看全體族群找 pattern
兩個族群return-to-nullmisaligned-stack 是兩種不相關的 bug
毒井(John Snow)壞主機 → denylist → 當機消失

調查員也學到了最重要的一課:當法醫模式卡住,該換成流行病學家模式。建乾淨的族群資料,問「這真的是一種 bug 嗎」——答案經常是「不是,是兩種混在一起」。

壞主機那群解決了。剩下的 return-to-null 那群,全都發生在「C++ 丟 exception、正在展開 stack」的當下。

那個「瞬間傳送」是什麼?兇手怎麼在一條指令的縫隙裡下手?

Post C,抓住 18 歲的鬼。

🔗 延伸閱讀

Post C 會講 exception 展開、libunwind 的競態條件、100 皮秒的作案窗。想先暖身,或喜歡這種爬塔風格:

(Post C 待續)