循跡追蹤使人頭禿的Crash,十倍程序員的Debug平常(2)

做者|陶建輝node

原文首發於:git

循跡追蹤使人頭禿的Crashgithub

咱們寫 C 程序,常常碰到 Crash,絕大部分狀況下都是空指針或野指針形成,從 call stack 來看,通常很容易找出問題。可是有一類 Crash 很難 debug,那就是內存溢出。溢出的部分的內存空間正好覆蓋另一個線程訪問的數據(好比一個結構體),那麼另一個線程讀取這塊數據時,獲取的數據就是無效的,每每致使不可預見的錯誤,甚至 Crash。但由於形成數據溢出的線程已經離開現場,致使問題很難定位。這是我在 2020 年 5 月寫的一篇內部博客,以我當時碰到的一個錯誤爲例,將解決這類問題的方法分享出來,供你們參考,觸類旁通。

具體問題

在feature/query分支上,在community倉庫,執行如下腳本,出現Crash。session

./test.sh -f general/parser/col_arithmetic_operation.sim

重現問題

我登陸到指定的機器,查看了 core dump, 確實如此。Call Stack 截圖以下:async

第一步:看哪一個地方 crash。是 shash.c:250 行,使用 GDB 命令 「f 1″ 查看 stack 1,查看*pObj,一下就看到 hashFp 爲 NULL,天然會 crash。但爲何會設置爲空?其餘參數呢?dataSize 設爲 0 也必定是錯的。所以能夠判定,這個結構體是不對的。咱們須要看上一層調用是否傳入了正確的參數。函數

第二步:使用 GDB 「f 2″查看 stack 2,rpcMain.c:605 行,查看*pRpc,這些結構體裏的參數顯得很正常,包括指向 hash 的指針值看不出有什麼問題。那所以能夠判定調用是 OK 的,調用 taosGetStrHashData 應該提供了正確的參數。工具

第三步:既然參數對的,看 shash.c 的程序,那隻多是 SHashObj 這個結構體已經被釋放,訪問的時候,天然無效。再看代碼,只有一個可能,函數 taosCleanUpStrHash 被調用,所以我在改函數裏立刻加上一行打印日誌(注意 TDengine 的日誌輸出控制,系統配置文件 taos.cfg 裏參數 asyncLog 要設置爲 1,不然 crash 時,有可能日誌不打印出來)。從新運行腳本,查看日誌,發現 taosCleanUpStrHash 沒有被調用過。那麼如今只有一個可能,這一塊數據的內存被其餘線程寫壞。測試

第四步:萬幸,咱們有很棒的運行時內存檢查工具 valgrind, 能夠經過運行它來找找蛛絲馬跡。一運行(valgrind 有不少選項,我運行的是 valgrind –leak-check=yes –track-origins=yes taosd -c test/dnode1/cfg),一下就發現有 invalid write,截圖以下:編碼

第五步:一看 valgrind 輸出,就知道 rpcMain.c:585 有 invalid write, 這裏是 memcpy。從編碼上來看,這應該沒有問題,由於拷貝的是固定大小的結構體 SRpcConn,每次運行到這裏都會執行的。那麼惟一的可能就是 pConn 指向的是無效的內存區,那 pConn 怎麼多是無效的?咱們看一下程序:spa

看 584 行,pConn = pRpc->connList + sid。這個 sid 是 taosAllocateId 分配出來的。若是 sid 超過 pRpc->sessions,那 pConn 毫無疑問就指向了無效的區域。那怎麼確認呢?

第六步:加上日誌 578 行,分配出的 ID 打印出來,編譯,從新運行測試腳本。

第七步:crash,看日誌,能夠看到 sid 能輸出到 99(max 是 100),還一切正常,但以後就崩潰。所以能夠斷言,就是因爲分配的 ID 超過了 pRpc→session。

第八步:查看分配 ID 的程序 tidpool.c,一看就知道緣由,ID 分配是從 1 到 MAX,而 RPC 模塊只能使用 1 到 Max-1。這樣當 ID 返回爲 max 時,RPC 模塊天然會產生 invalid write。

解決方案

既然知道緣由,那就很好辦,有兩套方法:

  1. 在 tidpool.c,taosInitIdPool 裏,將 maxId 減一,這樣分配的 ID 只會是 1 到 max-1。
  2. 在 rpcMain.c 的 rpcOpen() 函數裏,將
pRpc->idPool = taosInitIdPool(pRpc->sessions);

改成

pRpc->idPool = taosInitIdPool(pRpc->sessions-1);

若是應用要求最多 100 個 session,這麼改,RPC 至多建立 99 個,爲保證 100 個,再將

pRpc->sessions = pInit->sessions;

改成

pRpc->sessions = pInit→sessions+1;

驗證

兩種方法,都從新編譯,運行測試腳本經過,crash 再也不發生。

經驗總結

遇到內存被寫壞的場景,必定要用 valgrind 跑一次,看是否有 invalid write。由於它是一個動態檢查工具,報的錯誤都應該是對的。只有把 invalid write 先解決,再去看 crash 問題。

怎麼避免相似問題

這個 BUG 的核心是因爲 tidpool.c 分配的 ID 範圍是 1 到 max,而 RPC 模塊假設的 ID 分配是 1 到 max-1。所以是模塊之間的約定出了問題。

怎麼避免?每一個模塊對外 API 要作詳細說明,若是 API 作了調整,要通知你們,並且要運行完整的測試例,以避免破壞了某種約定。

本文爲 TDengine 的一則真實案例,git commit id:89d9d62,歡迎你們重現。

相關文章
相關標籤/搜索