前段時間開發的一個后端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)本來自覺得對內存操作已經非常小心了,沒想到還是在想當然的瞬間寫下犯錯的代碼,導致終於花費非常多時間和精力去“捉蟲”。只是好在跟進崩潰的過程中添加了一點分析/定位問題的經驗,也算有些收獲吧
=============== EOF =================