自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

用gdb分析coredump的一些技巧

開發(fā) 前端
前幾天我們正在運營的一款產(chǎn)品發(fā)生了崩潰,我花了兩天嘗試用 gdb 分析了 coredump ,雖然最后還是沒能找到 bug ,但還是覺得應(yīng)該做一些總結(jié)。

前幾天我們正在運營的一款產(chǎn)品發(fā)生了崩潰,我花了兩天嘗試用 gdb 分析了 coredump ,雖然最后還是沒能找到 bug ,但還是覺得應(yīng)該做一些總結(jié)。

產(chǎn)品是基于 skynet 開發(fā)的,由于歷史原因,它基于的是 skynet 1.0 之前 2015 年中的一個版本,由于這兩年一直沒出過什么問題,所以維護人員懈怠而沒有更新。

崩潰的時候,關(guān)于 Lua 部分的代碼缺少調(diào)試符號信息,這加大了分析難度?,F(xiàn)在的 skynet 在編譯 lua 時,加入了 -g 選項,這應(yīng)該可以幫助未來出現(xiàn)類似問題時更好的定位問題。

[[190796]]

導致代碼崩潰的直接原因是 rip 指向了一個數(shù)據(jù)段的地址,準確的說,跳轉(zhuǎn)到了當前工作線程擁有的 lua 虛擬機的主線程 L 那里。

發(fā)現(xiàn)這條線索很容易,skynet 的其它部分是有調(diào)試符號的,可以在崩潰的調(diào)用棧上看到,服務(wù)的 callback 函數(shù)的 ud 和崩潰地址一致,而 lua 服務(wù)的 ud 正是 L 。用 gdb 的 p ( lua_State *)地址 查看這個結(jié)構(gòu),也能觀察到這個數(shù)據(jù)結(jié)構(gòu)的內(nèi)容正是一個 lua_State 。

由于用 bt 查看的調(diào)用棧是不正常的,所以可以斷定在函數(shù)調(diào)用鏈的過程中應(yīng)該是發(fā)生了某種錯誤改寫了 C 棧的內(nèi)容。在這種情況下,gdb 多半靠猜測來重建調(diào)用鏈(就是用 bt 看到的那些)。

現(xiàn)代編譯器經(jīng)過優(yōu)化代碼之后, C 棧上已經(jīng)沒有 stack frame 的基地址了,所以現(xiàn)在不能簡單的看堆棧的數(shù)據(jù)內(nèi)容來推測 stack frame 。也就是經(jīng)過優(yōu)化的代碼不一定適用 rbp 來保存 stack frame ,它也不一定入棧。對于 gcc ,這個優(yōu)化策略是通過 -fomit-frame-pointer 開啟的,只要用 -O 編譯,就一定打開的。在 stack 本身出問題時,gdb 的猜測很可能不準確,人工來猜或手工補全或許更靠譜一些。方法就是先用 x/40xg $rsp 打印出 C stack 的內(nèi)容,然后觀察確定 stack 上的哪些數(shù)據(jù)落在代碼段上。所有有函數(shù)調(diào)用的地方,一定有處于代碼段上的某處返回地址指針。

主程序的代碼段一般都地址偏低,動態(tài)鏈入的代碼段可以用 info sharedlibrary 來查看。返回地址肯定是落在函數(shù)代碼的內(nèi)部,而肯定不會是函數(shù)入口,而這些地址除了函數(shù)調(diào)用外,都不可能用正常的 C 代碼生成出來,所以識別性很強,不會有歧義。

如果覺得某個指針是函數(shù)返回地址,可以用 x/10i 地址 來反匯編確認。

但是需要注意的是,即使在 C stack 上發(fā)現(xiàn)一個函數(shù)返回地址,并不說明這個函數(shù)調(diào)用尚未返回。它只能說明這個函數(shù)至少被調(diào)用過。這是因為,匯編在 call 一個函數(shù)時,會把當前調(diào)用處的地址壓棧。而調(diào)用結(jié)束后,ret 指令返回只是修改了 rsp 這個棧指針,而數(shù)據(jù)本身是殘留在棧上的。這也是為什么 gdb 有時候也會猜錯。

在這次的案例里,崩潰發(fā)生在執(zhí)行跳轉(zhuǎn)到了數(shù)據(jù)段,這種情況多半是因為 call 指令調(diào)用的是一個間接引用,在 C 層面來看,就是調(diào)用了一個函數(shù)指針。這種情況下,跳轉(zhuǎn)地址肯定還在寄存器里。用 info registers 可以查看。(注:在 64bit 平臺下,查看寄存器內(nèi)容非常重要,因為 64bit 下,函數(shù)調(diào)用的前四個參數(shù)是通過寄存器 rdi rsi 傳遞而非堆棧,往往需要結(jié)合 disass 反匯編看代碼去推算。)

當然,按 lua 自己的正常邏輯,是不可能把 L 作為一個函數(shù)指針來調(diào)用的。按我的猜測,這里出錯比較大的可能是 longjmp 的時候數(shù)據(jù)出錯,恢復了錯誤的寄存器。btw, setjmp 在生成 jmp_buf 時,對于 rsp rbp 這類很可能用于地址的寄存器,crt 做了變形(mangling)處理,所以很難簡單的靠寫越界寫出一個巧合的錯誤值。

對于調(diào)試崩潰在 lua 內(nèi)部的情況,比較關(guān)鍵的線索通常是 L 本身的狀態(tài)。因為業(yè)務(wù)的主流程其實是用 lua vm 驅(qū)動的,L 的 callinfo 也就是 lua 的 stack frame 信息更多。

對于 skynet ,在正常運行的時候通常會有兩個活動的 L 。一個是主線程,用來分發(fā)消息;但消息本身是在一個獨立的 coroutine 中進行的。以上可以確定主線程,而子線程的 L 可以在寄存器和 stack frame 里找。由于沒有調(diào)試符號,所以可以靠猜來尋找,這并不算太麻煩。要確定一個地址是否是 L ,只需要查看 L->l_G 看是否和前面找到的主線程 L 的對應(yīng)值是否相同。

在缺少調(diào)試符號的情況下,會發(fā)現(xiàn) lua 下的一些內(nèi)部數(shù)據(jù)結(jié)構(gòu) gdb 無法識別。這個時候可以用 add-symbol-file 來導入需要的結(jié)構(gòu)信息。方法是加上 -g 重新編譯一下 lua ,把一些包含這些結(jié)構(gòu)的文件,例如 ldo.o 加進來。

我在分析這次的問題時,寫了腳本查看兩個 L 的 lua 調(diào)用棧,這些腳本只要對 lstate.h 里的 callinfo 數(shù)據(jù)結(jié)構(gòu)熟悉就很容易寫出來。lua 的調(diào)試信息很豐富,找到源文件名和行號都很容易。另外,L 棧頂?shù)臄?shù)據(jù)是什么也是重要的線索,可以推導出崩潰發(fā)生時 Lua 的狀態(tài)。

這次我們崩潰的程序最后停在主線程的 resume 調(diào)用子線程上。子線程調(diào)用了 skynet.sleep ,也就是最后把 "SLEEP", session 通過 yield 傳給了主線程。這些要傳出的量可以在子線程的 L->top 上查到。雖然 lua 本身已經(jīng)把值 pop 出去了,但 pop 本身是不清空棧的,只是調(diào)整了棧頂指針,所以在 gdb 下依然可見。主線程也接收到了傳過來的數(shù)據(jù),數(shù)據(jù)棧上可見。

不過這次的吊詭之處在于,lua 線程間拷貝數(shù)據(jù)這個過程是在 lcorolib.c 中的 auxresume 函數(shù)中執(zhí)行的,在 luaB_coresume 里還需要在結(jié)果中插入一個 boolean 。而我在 coredump 數(shù)據(jù)中發(fā)現(xiàn)了拷貝過程已經(jīng)完成,但是 boolean 卻沒有壓入。那么事故發(fā)生點只可能在兩者之間。不過在 auxresume 返回到后續(xù) push boolean 之間只有幾行匯編代碼,絕對不可能出錯。

唯一能解釋的就是在 lua_resume 期間,子線程運行的流程破壞了 C 的 stackframe ,讓 auxresume 沒能正確的返回到調(diào)用它的 luaB_coresume 中。但怎樣才能制造出這種情況,我暫時沒有想法。

在 C 層面制造出崩潰的可能性并不是很多,數(shù)據(jù)越界是一類常見的 bug (這次并不像);另一類是內(nèi)存管理出錯,比如對同一個指針 free 多次,導致內(nèi)存管理器出錯,把同一個地址分配給兩個位置,導致兩個對象地址重疊。后一類問題能干擾到 C 的 stack frame 可能性比較小,除非有堆上的對象指針指向了棧地址,然后并引用。這次的 bug 中,最打的線索是 L->errorJmp ,也就是 lua 線程中指向恢復點的 jmp_buf ,它是在 C 棧上的。

L 中有一些相關(guān)變量可以推測 resume/yield/pcall 等的執(zhí)行狀態(tài): L->nny L->nCcalls L->ci->callstatus 等都是。我分析的結(jié)果是在 auxresume 返回后,沒有繼續(xù)運行 luaB_coresume 中的 push boolean 過程,卻又運行了新的一輪 luaD_pcall ,導致了最終的崩潰。這可以通過 L 的 errorJmp 的 status 得到一定的佐證。不過 C stack 上沒有 luaB_coresume 的返回地址比較難解釋,只能說是可能被錯誤的運行流程覆蓋掉了。

gc 會是觸發(fā) bug 的多發(fā)點,因為 gc 是平行于主流程同步進行的。這次崩潰點的子線程的 lua 棧幀停留在 yield 函數(shù)上,在此之前也的確調(diào)用了 gc step 。但是,我們可以通過查閱 L 的 gcstate 變量查看 gc 處于什么階段。在這次的事發(fā)現(xiàn)場,可以看到 gcstate 為 GCSpropagate 也就是 mark 階段,所以并不會引發(fā)任何 __gc 流程,也沒有內(nèi)存釋放。

結(jié)論:對于 bug ,暫時沒有結(jié)論。不過對于調(diào)試 lua 編寫的程序,還是積累了一些經(jīng)驗:

  1. 一定要在編譯 lua 時加 -g ,雖然 lua 本身出嚴重 bug 的可能性極低,但可以方便在出問題時用 gdb 分析。
  2. 在 gdb 中查看 lua 的調(diào)用棧很有意義,分析 L->ci 很容易拿到調(diào)用棧信息。
  3. 記得查看一下 lua 的數(shù)據(jù)棧內(nèi)容,包括已經(jīng) pop 出去,但還殘留在內(nèi)存中的數(shù)據(jù),可以幫助分析崩潰時的狀態(tài)。
  4. 記得查看 L 中保留的 gcstate GCdebt 等 gc 相關(guān)變量,可以用于推斷 lua gc 的工作狀態(tài)。
  5. L 中的 nny nCcalls errorJmp 可以幫助確定 lua 到 C 的調(diào)用層次。注意:一個 yield 狀態(tài)的 coroutine ,errorJmp 指針應(yīng)該為 NULL 。

另外,gdb 分析 skynet 可以從下面的線索入手:

  1. context 對象里能找到當前服務(wù)的地址、最后一個向外提起的請求的 session 、接收過多少條消息等。結(jié)合 log 文件來看會有參考價值。
  2. 如果想找到內(nèi)存中其它的服務(wù)對象(非當前線程上活動的),可以試試 p *H 。 H 是個數(shù)組,定義在 skynet_handle.c 中,里面有所有服務(wù)的地址。
  3. 如果想找到內(nèi)存中待喚醒的 timer ,可以試試 p *TI 。它定義在 skynet_timer.c 中。
責任編輯:未麗燕 來源: 云風的 BLOG
相關(guān)推薦

2018-01-09 18:06:41

Python爬蟲技巧

2012-05-21 10:13:05

XCode調(diào)試技巧

2013-03-29 13:17:53

XCode調(diào)試技巧iOS開發(fā)

2011-06-01 16:50:21

JAVA

2011-05-23 18:06:24

站內(nèi)優(yōu)化SEO

2021-10-12 23:10:58

UnsafeJavaJDK

2011-10-26 20:55:43

ssh 安全

2011-07-12 09:47:53

WebService

2022-12-02 14:58:27

JavaScript技巧編程

2017-09-20 15:07:32

數(shù)據(jù)庫SQL注入技巧分享

2022-02-17 13:58:38

Linux技巧文件

2018-05-07 08:22:19

LinuxImageMagick查看圖片

2009-11-26 10:32:57

PHP代碼優(yōu)化

2020-04-08 10:21:58

bash腳本語言

2020-04-14 09:22:47

bash腳本技巧

2024-03-11 15:08:26

Linux操作系統(tǒng)進程

2021-06-18 07:35:46

Java接口應(yīng)用

2020-10-19 19:25:32

Python爬蟲代碼

2015-08-17 15:53:58

Linux桌面

2011-07-03 19:07:47

關(guān)鍵詞
點贊
收藏

51CTO技術(shù)棧公眾號