前段時間開發的一個後端C子產品上線後,線上出core,初始時,因為訪問壓力不大,是以崩潰是上線3天左右出現的。當時用gdb跟進調用堆棧并檢查源代碼,發現出core位置的代碼沒有啥問題。因為當時開發任務較重,且該子產品不儲存狀态(崩潰重新啟動不影響對外服務),是以沒有深入跟進。後來随着client版本号逐漸放量導緻訪問壓力上升,噩夢開始了。。。
該子產品會不定時core掉,并且差點兒每次崩潰時的調用堆棧都不一樣,關鍵是最後幾層堆棧非常多都位于差點兒不可能出問題的代碼中,比方庫函數或廠裡的公共庫。
好在在衆多core檔案裡發現規律:每次基本都是在對記憶體動态操作時挂掉,比方malloc/realloc/free/new/delete都引起了崩潰。并且幸運的是,崩潰程序還是輸出了一些關鍵資訊,比方以下這些(這些是在不同的崩潰時刻分别輸出的):
*** glibc detected *** malloc(): memory corruption: 0x0000002a95c1ff10 ***
*** glibc detected *** double free or corruption (out): 0x0000000000f0d910 ***
*** glibc detected *** free(): invalid next size (normal): 0x0000002a96103b00 ***
*** glibc detected *** free(): invalid next size (fast): 0x0000000000f349d0 ***
*** glibc detected *** corrupted double-linked list: 0x0000002a95f062e0 ***
從上面的日志也能夠看到,每次引起崩潰的直接原因都可能不同。用gdb又細緻檢視core檔案發現,有時程序是收到SIGABRT信号後退出,有時又是收到SIGSEGV信号後退出。
由此,基本定位了崩潰原因:記憶體訪問越界導緻破壞了heap的資料結構。用valgrind線上下環境啟動程序,試圖重制崩潰或定位越界訪問的代碼,遺憾的是,腳本壓了1個小時也沒出現崩潰,而valgrind的輸出報告也沒有越界代碼位置的提示。
終于,細緻檢查源代碼後發現,在某個回調函數中,new出來的buffer接收完通過http post方式發送過來的2進制資料後,我又多寫了1行代碼,相似于:recv_buf[data_len] = '\0',導緻越界多寫1個位元組,終于引起各種莫名其妙的記憶體崩潰。
經驗教訓:
1)調用堆棧資訊對定位問題幫助非常大,但也不可盡信。比方這次遇到的情況,每次出core的調用堆棧差點兒都不一樣,并且最後幾層棧幀都是不可能出現故障的庫函數或久經考驗的公司公共庫,這樣的情況下,思維須要跳出局部,在更高的層次尋找規律或原因
2)一旦定位崩潰屬于堆記憶體讀寫越界問題,就細緻檢查自己的代碼吧,因為庫函數或公共庫出問題的機率太小了,是以不要存在僥幸心理,這個時候,盲目的自信要不得
3)本來自覺得對記憶體操作已經非常小心了,沒想到還是在想當然的瞬間寫下犯錯的代碼,導緻終于花費非常多時間和精力去“捉蟲”。隻是好在跟進崩潰的過程中添加了一點分析/定位問題的經驗,也算有些收獲吧。