常見的軟體異常場景分析與總結
根據最近一年多的排查軟體異常問題的經歷和經驗,簡單的總結一下軟體異常的場景和原因,以供參考。
1、野指標問題
可能是指標沒初始化就使用。也有可能是指標指向的記憶體已經被釋放,但是指標沒置為NULL,一旦訪問這樣的指標就會出問題。在很多情況(包括訪問空指標的情況)下可能會訪問64KB以內的系統禁止訪問的NULL指標記憶體區,系統直接將程式終止掉。此處是某個變數沒有初始化,也有可能某個dll庫沒初始化,就呼叫庫中的介面了。
2、空指標問題
有可能記憶體已經釋放,指標已經置為NULL,但是後面還是訪問了空指標。比如在某個庫已經unInitialize之後還呼叫庫中的介面或者庫中執行的程式碼訪問了空指標。也有可能是呼叫介面返回了空指標,呼叫者沒有新增指標是否為空判斷。
3、堆記憶體被釋放兩次
已經釋放了,但是又呼叫free或delete釋放了一次,即釋放了兩次。對於空指標,釋放多次也沒問題。
4、棧記憶體被當做堆記憶體來釋放
比如在類的函式中自動釋放當前類物件的記憶體,即delete this,但是在用類定義的物件時,使用的棧記憶體,使用delete釋放棧記憶體就有問題了。
5、記憶體訪問越界
將相鄰的記憶體中的內容破壞了,即將相鄰的記憶體中的內容篡改了,會出現各種意想不到的問題。比如就會觸發訪問訪問NULL指標記憶體區的異常、後續記憶體拷貝因為長度被篡改,導致另一個記憶體越界操作。不管是堆記憶體越界,還是棧記憶體越界,都可能導致異常。最直接的異常可能就是在越界的時候發生記憶體訪問違例。
6、函式呼叫約定不一致引起的棧不平衡的問題
比如在給dll庫中設定回撥函式,回撥函式沒指定呼叫約定,使用VS預設的C呼叫。但是當dll被C#呼叫時,包含dll的標頭檔案,C#中預設使用標準呼叫。回撥函式在C#層實現時被設定為標準呼叫,函式內部負責清理棧,但是在dll中呼叫到回撥函式,dll認為函式是C呼叫,dll中在呼叫回撥函式時會在函式外部清理棧,這樣在回撥函式被呼叫後,多清理了一次棧,這樣棧就不平衡了。
7、呼叫虛擬函式時的二次定址
C++的虛擬函式存放在虛擬函式表中。如果要呼叫一個類物件的虛擬函式,需要通過類物件首地址,得到虛擬函式表首地址,然後根據呼叫的虛擬函式名稱,到虛擬函式中找到虛擬函式的地址,然後call這個地址。在彙編程式碼中,能看到二次定址的詳細過程。
8、debug和release庫混用的問題
可能dll匯出介面在dll內部申請的堆記憶體,需要呼叫者在外部釋放。如果該dll是release版本的,而呼叫者是debug版本的,這個在呼叫者釋放記憶體時就會出現異常。因為debug和release下的記憶體管理是不同的。debug下包含除錯資訊,申請的記憶體較大。
9、死迴圈問題
死迴圈一般會導致系統的CPU佔用會比較高。如果是UI主執行緒,則比較好辦,就是0號執行緒,切換到0號執行緒,然後多go幾次,檢視堆疊是否一樣。如果一樣可以使用bp設定堆疊中的多個斷點,看到底死迴圈發生在哪個函式中。一般是迴圈體中出現了較大的迴圈次數導致的,或者是迴圈條件設定有問題,一直為TRUE。可以看一下堆疊中的與迴圈條件相關的區域性變數的值,從而定位問題。如果是遠端傳過來的值作為迴圈次數,可能要新增上限保護,放置傳過來的是異常值。如果是底層的執行緒,可以藉助Process Explorer檢視一下堆疊,找到執行緒id和堆疊,到windbg中切換到目標執行緒中設定斷點,判斷是否有死迴圈。也可以直接在windbg中使用!runaway命令檢視哪個執行緒佔用CPU較高,有必要時可能還要看看是核心態的佔用多,還是使用者態的佔用的多。
10、死鎖問題排查
目前用windbg排查關鍵程式碼的死鎖相對簡單,因為關鍵程式碼段是使用者態的物件。如果是核心物件的鎖,則需要進行核心態的除錯,不過核心態的除錯比較複雜。
11、資料型別使用違規引起的問題
比如沒有搞清楚某類的約束條件,隨意使用其介面,導致使用違規,觸發異常。
12、丟擲異常後導致部分程式碼被跳過的問題
類中遇到異常資料丟擲異常,程式仍在執行,但是有部分程式碼被跳過,導致程式碼邏輯出現異常。比如初始化CTime物件時傳入了異常值報出異常。還比如新人對stl中的vetctor等類物件執行了memset的操作,破壞了stl內部的結構資料,導致stl內部產生異常。當然還有一種場景,直接抵CString進行memset操作,破壞了類中的維護結構的資料,一般都會觸發異常。一般memset是對結構體操作的,但如果結構體中包含非基本資料型別,比如類物件就要注意了,只能在建構函式中初始化,不能直接進行memset。所以在使用memset時,遇到類物件就要注意了。
13、類物件沒有初始化就直接訪問問題
比如臨界區物件,沒呼叫初始化介面,就直接enter了,就會出異常。有可能程式碼中跳出異常,將初始化的程式碼跳過去。