標籤:nbsp 跳過 ica 記錄 使用 解構函式 lin 服務 print
昨天伺服器宕機,列印出的日誌非常詭異,宕在純虛函數調用處。
日誌顯示,戰鬥對象的虛函數調用,前幾次正常,某個時刻過後“喪失多態”了,直接調到父類虛函數處,引發純虛函數宕機。
且win平台下運行正常,上linux必跪,老項目linux工具不全,debug版本都編不出來,只有Log;windows下還複現不出來。
找這個bug的過程還是蠻有意思的。記錄下(*^__^*)
以往沒碰到過這種Bug,起初當然毫無頭緒。
首先想到,c++中經常的記憶體改寫,但能正常調用到普通虛函數,應該不是memset這樣的東西把對象寫壞。
進一步分析,要能正常調到父類虛函數,那對象虛指標一定指向了正確的父類虛表。
回憶c++建構函式流程:
1、假設CBase裡有幾個純虛函數 CObj 繼承它
2 、CObj 的構造順序:先構造 CBase 的部分,此時對象首地址的虛指標指向了 CBase的虛表……再接著構造 CObj 新增的部分,改寫對象首地址的虛指標,指向ClassObj的虛表
如果解構函式按對應順序反過來,容器裡儲存的 CBase* 指標,經過析構後,指向的對象,它的首地址就被改寫為指向 CBase 虛表了。
這樣就會出現日誌看到的情況。
但我不確定解構函式是不是會改虛指標,按照構造、析構對稱的思路預計是會的。
網上也沒查到資料,決定寫代碼實驗~~結果不會
…………沒啥線索了
晚上想起編譯器對拷貝建構函式的最佳化,預設產生的拷貝建構函式其實不會被調用(沒有副作用),直接最佳化為位元組拷貝即可。
寫的測試代碼裡沒顯式聲明解構函式,會不會也被編譯器跳過了。所以 delete 後,首地址的vptr還是沒變。
今天來立馬改了測試代碼,在父類裡加上解構函式聲明、實現……果然,析構後對象首地址的內容被改寫了
Obj* pB = new Obj(); printf("addr(%d) \n", *((int*)pB)); delete pB; printf("addr(%d) \n", *((int*)pB));
至此,可以肯定伺服器宕機,就是因為戰鬥對象被析構,虛指標被改寫為指向父類虛表,業務層再拿來用時就跪了。
(因為用到記憶體池,所以沒出現懸垂指標的問題)
剩下的就好查了,delete對象時某業務模組仍持有其指標,沒清理。搜搜戰鬥對象的參考關聯性,幾分鐘便找到問題所在。
戰鬥城池裡有個守衛列表,npc進入時會把自己指標放入這個列表,死亡時沒去清。 別人再來打這個城池時,跑戰鬥流程就調了純虛函數,宕機。
尾聲:
覺得這個bug挺有深度的,能扣的地方很多。
比如,為什麼在win下不會宕機呢?項目裡的戰鬥對象也是沒顯式解構函式的,應該是被vs編譯器最佳化掉了,而Linux沒有。
再比如,如果沒有記憶體池,那兩邊應該都會出現懸垂指標,直接宕機……提前暴露問題所在,反而更好分析定位Bug。
還有,win環境下,即便免去了純虛函數的宕機問題,但確將Bug隱藏的更深了。後面商務邏輯再從記憶體池取指標,拿到那箇舊的,胡亂一改,再出問題時候,你看到的就是一坨shit了,鬼知道到底是哪改壞的 ( ̄﹁ ̄)~
還是我們老大說的好:
記憶體池如果是新項目,我估計不會使用,會直接用TCMALLOC之類的。我還是想能工程化就工程化,C++開發還是要往庫的思維走。不然老挖坑填坑。
PS: 沒頭緒下班前,我幹了三件事情:
在前C++項目群裡描述問題,詢問“有誰碰到過中途調純虛函數,伺服器宕機的情況”;
在加入的技術群裡問;
在知乎提問,邀請輪子哥、R大
次天來就看到有人回複:子類析構掉的話,虛表會被改寫成iobj的虛表,析構過的指標,可以調iobj的虛函數,調其它虛函數則會掛
即使自己沒能想到“析構過程可能被編譯器最佳化掉”,也能在他們的指導之下找到問題的。
利用別人的經驗哈 b( ̄▽ ̄)d
Bug:C++運行時調用純虛函數