做者|陶建輝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。
既然知道緣由,那就很好辦,有兩套方法:
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,歡迎你們重現。