目錄
1、概述
2、引發軟體例外的常見原因
2.1、變數未初始化
2.2、死回圈
2.3、記憶體越界
2.4、記憶體泄露
2.5、空指標與野指標
2.6、執行緒堆疊溢位
2.7、函式呼叫約定不一致導致堆疊不平衡
2.8、庫與庫之間不匹配
2.9、死鎖
2.10、GDI物件接近或達到1萬個導致例外
2.11、對包含C++類成員的結構體進行memset操作
2.12、程式中拋出了例外,將部分該執行的代碼跳過去了
2.13、模塊注入到程式中導致程式出現例外
2.14、添加日志列印覆寫了lasterror的值
2.15、格式化時格式化符與引數不一致
2.16、同一個程式在不同系統中可能會有不同的表現
3、最后
你還在為開發程序中遇到的多種軟體例外崩潰而束手無策嗎?還在為趕不上專案進度而一籌莫展嗎?我能幫助到你,給你提供多種排查軟體例外的思路與方法,助你快速地解決問題,本文根據近幾年排查軟體例外的實戰經歷與經驗,詳細地總結出引發軟體例外的常見原因,給大家提供一些借鑒和參考,
1、概述

本文要講的軟體例外,是廣義上的軟體運行例外,包括軟體運行時邏輯上的例外、高CPU占用、記憶體越界、記憶體泄露、執行緒堵死、死回圈、死鎖等,本文結合近幾年來的例外排查實踐,對軟體例外的常見原因及排查思路進行一個相對完整的總結,以供參考,
希望大家了解這些內容之后,既能在寫代碼時提前感知潛在的問題,也能在出問題之后多一些排查問題的思路與手段,同時借助一些常用的軟體工具,有效地提高排查問題的效率,
本文主要以Windows下的C++開發為例進行講解,不同平臺及不同語言在多個方面是相通的,也具有很大的參考價值,
2、引發軟體例外的常見原因
下面大概地講述一下引起軟體例外的常見原因和場景,
2.1、變數未初始化
這可能是許多人容易忽略的問題,在Debug下某些編譯器會自動對變數自動進行初始化,但Release下則是分配記憶體時記憶體中的隨機值,
變數忘記初始化,可能會導致Debug和Release下運行行為的不一致,也可能會導致不同作業系統上運行行為的不一致(不同作業系統的記憶體管理機制是有差異的),
如果是回呼函式指標因為一些原因沒有及時地初始化,這將是致命的,導致沒有初始化就呼叫了,因為release下回呼函式指標中存放的是隨機值,會導致代碼“跑飛”,程式會胡亂的崩潰,

如上圖所示,對于微軟C++編譯器,在Debug下,未初始化的堆疊記憶體會被編譯器初始化為0xCCCCCCCC,未初始化的堆記憶體會被編譯器初始化為0xDDDDDDDD,
在除錯代碼時,如果遇到0xCCCCCCCC、0xDDDDDDDD和0xFEEEFEEE等典型的例外值時,就要第一時間做出反應,可能是例外碼對應的原因導致的,
可能有些人會說,我只要控制好現有的代碼,就能保證不會訪問未初始化的變數,不一定非要一上來就將變數初始化,即便是這樣,也是不可取的,因為代碼后面可能會交由其他人維護,每個人撰寫代碼的水平有所差異,你這里沒問題,別人那邊可能會出問題,
2.2、死回圈
死回圈一般會引發CPU高占用率,一般不會導致軟體崩潰,一旦發現行程占用的CPU過高,可能就是死回圈觸發的,也有可能是執行緒函式中沒有添加sleep導致的,

死回圈一般是for回圈或while回圈的回圈控制條件出了問題,可能是回圈控制條件寫錯了(比如撰寫代碼時的手誤),也可能是回圈控制條件中變數出現了例外大的值(這個值可能是服務器回傳的例外值),
另外,還有一種情況是訊息觸發的函式呼叫上的死回圈,比如呼叫一個底層的介面,收到底層回應訊息后,又呼叫了該底層介面,又收到底層的這個訊息,回圈往復地執行同樣的函式呼叫,導致了死回圈的發生,
死回圈會導致所在執行緒的卡頓或堵塞,如果發生在UI執行緒中,UI界面會出現點擊沒反應或者反應很慢的情況,對于Windows程式,可以使用Windbg或者Process Explorer去定位死回圈所在執行緒及函式,
Windbg和Process Explorer是排查死回圈和CPU高占用問題的利器,都可以查看到問題執行緒的函式呼叫堆疊,相比較而言,Process Explorer要更易操作一點,但就功能而言,還是Windbg要強大很多,哪些場景下使用哪種工具,視個人喜好及問題的具體情況而定吧,
2.3、記憶體越界
記憶體越界不一定會導致崩潰,有可能會越界到附近變數的記憶體上,即篡改了附近變數的值,導致代碼的運行控制邏輯出例外,記憶體越界包含堆疊記憶體越界、堆記憶體越界以及全域記憶體越界,這些型別的越界,我們之前都遇到過,
就問題排查的難度而言,堆記憶體越界和全域記憶體越界比較難查,堆疊記憶體越界排查起來要容易很多,
其中有一種情形下的越界(被呼叫函式越界主調函式的堆疊記憶體上),很具有隱蔽性,排查起來比較困難,比如A庫依賴B庫,B庫定義了結構體Struct1,A庫呼叫了B庫的GetData介面, GetData介面是Struct1結構體作為引數的,GetData中進行了資料的memcpy操作,因為庫發布的問題,導致兩個庫不匹配,B庫中在Struct1結構體中新增了欄位,但是A庫中使用的還是老的結構體,這樣在呼叫GetData傳入結構體地址或參考,由于GetData中進行了memcpy操作導致記憶體越界,在呼叫完該介面后,因為傳入的Struct1結構體物件引數是參考,所以越界直接越到主調函式的堆疊上,即直接篡改了主調函式中其他堆疊變數的記憶體,導致代碼出現不可解釋的例外運行行為,
當時的場景對應的代碼如下所示:

呼叫pcAudEncoder->GetAudEncStatis介面,傳入了tAudEncStatis結構體物件,由于該介面引數是參考型別,該介面內部的記憶體越界越到主調函式CMiscCtrl::OnCodecStaisTimer中的區域變數tCodecStatis的堆疊記憶體上,即篡改了tCodecStatis變數記憶體中的值,導致后續代碼在訪問tCodecStatis變數的記憶體時產生了例外,
對于記憶體越界,我們可以通過分塊注釋代碼、添加資料斷點(Visual Studio中支持資料斷點)等手段進行定位,
2.4、記憶體泄露
記憶體泄漏是指程式中通過new/malloc動態申請的堆記憶體沒有釋放,長時間頻繁執行這些沒有釋放堆記憶體的代碼,導致程式的記憶體逐步被消耗,程式運行變慢,最后導致記憶體被耗盡(Out of memory),程式閃退,程式閃退時,系統會彈出類似下面的報錯提示框,

目前很多記憶體泄露檢測工具都比較陳舊,不再支持VS2017及以上版本的編譯器編譯出來的程式了,之前嘗試過使用騰訊的tMemMonitor記憶體泄露檢測工具 ,但是很多場景下的泄露該工具都檢測不出來,后來改用Windbg才將問題定位出來,
Windbg檢測記憶體泄露的步驟,可以自行搜索一下,此處不再贅述,后面可能也會寫專門的文章來介紹Windbg排查記憶體泄漏的具體用法,
2.5、空指標與野指標
這兩種是很常見的關于指標使用方面的錯誤,
對于Windows系統,訪問空指標會之所以會產生崩潰,是因為訪問了Windows系統的64KB禁止訪問的空指標記憶體區(即0-64KB這個區間的記憶體區域),這是Windows故意設定的一塊小地址記憶體區域,是為了方便程式員定位問題使用的,一旦訪問到該記憶體區就會觸發記憶體訪問違例,系統就會強制將行程強制結束掉,
關于64KB禁止訪問的記憶體區域,在《Windows核心編程》一書記憶體管理的章節,有專門的描述,相關截圖如下所示:

一旦訪問類的空指標,可能會訪問到類中的資料成員,就會訪問到64KB的小地址記憶體區,就會觸發例外,
所謂野指標,是指該指標指向的記憶體已經被釋放了,但還去訪問該指標指向的記憶體,就會導致記憶體訪問違例,導致軟體崩潰,還有一種情形是同一段堆記憶體被delete或free了兩次,也會觸發崩潰,
無論是空指標還是野指標,都可能會觸發記憶體訪問違例,都可能導致軟體的崩潰,
2.6、執行緒堆疊溢位
單個執行緒的堆疊空間是有限的,Windows上執行緒的默認堆疊空間是1MB,一旦當前執行緒的呼叫堆疊中占用的堆疊空間超過當前執行緒的堆疊空間上限,就會產生stack overflow執行緒堆疊溢位例外,導致程式崩潰,

從以往的排查這類問題的經驗來看,導致執行緒堆疊溢位的原因主要有以下幾種:
1)函式的遞回呼叫過深,導致函式中占用的堆疊空間一直未被釋放;
2)作為函式引數的結構體定義比較大,超過了當前堆疊空間的上限(在堆記憶體上定義結構體變數);
3)因為某些機制的存在,導致兩個函式不斷相互呼叫,陷入函式的死回圈呼叫(掐斷死回圈呼叫的機制);
4)switch陳述句中的case分支過多(將部分case分支封裝到新的函式中),
此處要特別說明一下switch-case陳述句中case分支過多的場景,可能這些case分支是用來處理服務器給過來的多個訊息,每個case分支對應一個訊息處理分支,我們會在case分支中定義生命周期在此case分支中的區域變數,
雖然代碼執行到case分支中這些變數才有“生命”,但其實這些變數已經在所在函式入口處幾句分配好堆疊記憶體了,可以撰寫C++測驗代碼進入除錯狀態查看一下匯編代碼,就能看出來的,這點我特別驗證過!
2.7、函式呼叫約定不一致導致堆疊不平衡
函式的呼叫約定不僅決定著函式引數壓入堆疊中的先后順序,還決定了應該由誰來釋放給被調函式傳遞的引數所占用的堆疊空間,
比如,我們常用的stdcall標準呼叫約定和C呼叫約定,如果被呼叫函式是標準約定,則由被調函式釋放傳給被調函式的引數的堆疊空間,如果被調函式是C呼叫,則由主調函式去釋放堆疊空間,
關于誰來負責釋放引數占用的堆疊空間,大家很容易混淆,給大家一個容易記住的辦法,比如我們經常用到的C函式printf:

該函式支持多個可變引數的格式化,設定的是C呼叫約定,因為被調函式無法知道傳入了哪些引數,只有主調函式才知道傳入了哪些引數,才知道傳入引數占用的堆疊記憶體的大小,才好去釋放引數占用的堆疊記憶體,
這個問題在設定回呼函式時比較常見,特別是跨語言設定回呼函式時,因為呼叫約定的不一致,可能會導致引數堆疊空間多釋放了一次,會直接影響主調函式的ebp堆疊基址出錯,導致主調函式中的記憶體地址錯亂出現例外或崩潰,比如C++的SDK給C#程式呼叫,因為C++語言中默認使用C呼叫約定,C#默認使用標準呼叫,如果回呼函式沒有顯式地指明呼叫約定,在實際使用時就會出問題,
在Debug下,Visual Studio默認開啟了/RTC運行時檢測,一旦檢測到堆疊不平衡,一般都會彈出如下的提示:

默認情況下,/RTC編譯選項只在Debug下是開啟的,Release下該選項是關閉的,有的模塊為了方便排查問題,在Release版本中開啟了該編譯選項,開啟該選項會向代碼添加很多額外的跟蹤代碼,會對程式的執行效率產生一定的影響,
2.8、庫與庫之間不匹配
因為一些原因,導致庫與庫之間的版本不一致或不匹配,從而導致程式運行例外或崩潰,比如底層的庫只發布了debug版本的庫,忘記發布release版本,導致Debug版本庫與Release庫混用,因為Debug于Release下的記憶體管理機制的不同導致崩潰,
再比如,底層庫的API頭檔案發生了改動(比如結構體中新增或刪減了若干欄位),但是只發布了庫檔案,忘記發布頭檔案;還比如,我們修改了頭檔案,但是發布時有若干關聯的庫沒有編譯或者編譯失敗,導致這些庫用的還是老版本,
一般這類不匹配會觸發記憶體上的問題,使程式出現例外或崩潰,比如Debug下彈出如下的提示框:

處理這類問題的辦法是,查看svn上的庫發布記錄,可能要重新發布庫,也可能需要將相關的模塊重新編譯一下,
2.9、死鎖
死鎖一般發生在多執行緒同步的時候,比如執行緒1占用了鎖A,在等待獲取鎖B,而執行緒2占用了鎖B,在等待獲取鎖A,兩個執行緒各不相讓,在沒有獲取到自己要獲取的鎖之前,都不會釋放另一個鎖,這樣就導致了死鎖,我們需要做好多個執行緒間協調,避免死鎖問題的出現,很多時候我們能夠根據現象及相關的列印日志,初步估計出可能發生死鎖的地方,
如果UI執行緒出現堵塞,或者是底層業務模塊出現擁堵,業務出現例外,可能就是死鎖引起的,可以將windbg掛在到目標行程上,查看所有執行緒的函式呼叫堆疊,確定發生死鎖的是哪些執行緒,發生死鎖的執行緒一般都會停留在WaitForSingleObject這個函式的呼叫或者類似函式的呼叫上,比如這樣的截圖:

如圖所示,當前執行緒卡在了WaitForSingleObject的函式呼叫上,通過函式呼叫堆疊,可以確定是呼叫了哪個函式觸發的,
對于使用臨界區的死鎖,使用Windbg排查比較容易分析,因為臨界區是屬于用戶態的,我們只需要使用為Windbg進行用戶態的除錯即可,如果是信號量等其他的鎖,則要使用Windbg進行內核態的除錯,內核態的除錯則要復雜很多,
此外,可以使用《Windows核心編程》一書中附帶的、有原始碼的工具LockCop來檢測一下,但該工具有局限性,只適用于部分內核物件的死鎖檢測,有些內核的死鎖檢測不到,后面會有專門的文章來介紹該工具的使用詳情,
2.10、GDI物件接近或達到1萬個導致例外
通過查看任務管理器就可以看到目標行程的GDI占用情況:

當程式中有GDI物件泄露時,程式長時間拷機運行,可能就會出現GDI物件接近或達到1萬個,導致GDI繪圖函式呼叫出現例外,出現視窗繪制不出來等問題,比如下圖中的視窗繪制不出來的例外:

左側的工具列視窗和右上角的關閉等按鈕都繪制不出來了,緊接著程式就會出現崩潰,很多Windows老程式員可能都遇到過類似的問題,
Windows系統中,行程中的GDI物件總數不能超過10000個,當行程的GDI物件總數接近或超過10000個時就會導致GDI繪圖出現例外,API函式呼叫回傳失敗,甚至出現閃退崩潰,
除了GDI泄漏會導致GDI總數達到系統上限,打開程式的多個視窗可能也會導致這個問題,比如之前我們用MFC做UI界面時,每個控制元件都是一個視窗,每個視窗都包含若干個GDI物件,這樣導致一個聊天視窗就占用了200多個GDI物件,這樣在測驗同事做極限測驗時,同時打開了好幾十個聊天視窗,就出現了GDI物件達到或接近上萬個的問題,這也是當時我們要將MFC界面庫換成duilib界面庫的原因之一,
在MFC框架中,每個控制元件都是一個真實的視窗,都會占用若干個GDI物件,而一個程式視窗中可能會包含多個控制元件,這樣單個視窗占用的GDI物件就不少了,
在duilib框架中,視窗中的dui控制元件都是繪圖繪制出來的,控制元件本身并不是視窗,所以dui視窗相對MFC視窗,占用的GDI物件可能要少很多,

對于GDI物件泄露,可以使用GDIView工具去實時查看目標行程中的GDI物件的占用及增長情況,就能確定哪種GDI物件數比較高了,這樣就能有針對性地去排查代碼了,在我們的專案中,我們已經多次使用GDIView工具去排查GDI物件泄露的問題,
2.11、對包含C++類成員的結構體進行memset操作
大家在使用結構體物件時,在使用結構體之前,都會習慣性地對結構體物件進行memset操作,但如果結構體中包含C++類時,是不能進行memset操作的,我們需要在建構式中對結構體物件成員進行初始化,
對包含C++類的結構體物件進行memset操作導致的崩潰問題,我們已經遇到過幾次了,特別是新人容易犯這樣的錯誤,有的C++類除了有存放資料的成員,還有維護內部記憶體結構的欄位,比如string類、CString類、stl類等,如果對結構體物件進行memset操作,則會破壞維護內部記憶體結構的欄位的記憶體,會導致不可預期的錯誤,例如CString類中就包含了一個額外的用于維護類內部記憶體結構的類:

具體的問題實體,比如在以前遇到的一個問題場景中,除錯時發現,傳給C++類物件的資料是對的,但是用到這個C++類物件時獲取的值卻是有問題的,
再比如,幾年前同事在開發新功能時,遇到了一個很詭異的問題,代碼是順序執行的,按順序執行下來肯定是沒問題的,但是程式跑下來,邏輯卻有明顯的例外,于是我過去看了一下,發現他定義的結構體中包含有stl串列:

他在使用該結構體物件之前,對結構體物件進行了memset操作,問題就出在這個memset操作上了,memset操作破壞了stl串列內部的記憶體結構,在我們讀取這個串列中的資料時,stl內部拋出了例外,直接將當前函式余下的代碼給跳過去了,導致本該執行到的代碼沒有執行,導致了程式邏輯上的例外,
2.12、程式中拋出了例外,將部分該執行的代碼跳過去了
程式中執行了非法的操作,拋出了例外,但程式并沒有崩潰,直接將當前函式余下的代碼跳過去了, 導致部分該執行的代碼沒有被執行到,導致后續的代碼邏輯出現了問題,進而導致程式出現例外,
這個問題我們遇到過幾次,比如給MFC中的CTime類的建構式傳入了非法的引數值,觸發CTime類介面拋出了例外,再比如,我們把一個包含stl串列vector成員的結構體給memset了,破壞了該結構體中的vector成員的記憶體結構,引發了vector內部拋出了例外,
2.13、模塊注入到程式中導致程式出現例外
我們之前遇到過很多次這樣的問題,輸入法的庫注入到我們軟體的行程中,導致了我們軟體的崩潰,通過分析發現,崩潰是發生在輸入法的模塊中,但因為這個模塊是注入到我們的行程中的,所以直接導致了我們軟體的崩潰,此外,輸入法注入到我的行程后還出現了軟體卡頓、CPU占用不間斷跳高的問題,
除了輸入法,還有一些安全軟體也會注入到我們的行程中,導致我們的軟體出現例外,這樣的問題我們也遇到過幾次,比如前段時間我們就遇到這樣一個問題,有個客戶的機器上安裝了多個安全防護軟體,運行我們的軟體一段時間后就會崩潰閃退,經遠程到客戶的機器上查看到任務管理器中的行程的記憶體在持續增長,估計是軟體中發生了記憶體泄露,增長到一定程度后就導致記憶體耗盡,發生了閃退,最終定位到記憶體泄露發生在安全軟體的注入庫中,
再比如幾年前遇到的一個客戶問題,他們的Windows系統中安裝了VPN軟體,注入到我們的行程中,hook了網路通信的相關介面,以監控軟體的網路資料包的收發,其中hook的recvfrom介面實作有bug,我們代碼中有處呼叫recvfrom介面的地方傳入了NULL引數(對于系統API函式recvfrom,傳入NULL值是允許的),結果直接導致該注入模塊產生了崩潰,進而導致了我們軟體的崩潰,
查看微軟MSDN上對套接字API函式recvfrom的說明,函式的最后兩個引數是可選的,可以不傳入,直接設定NULL就可以了,如下所示:

但客戶VPN軟體注入模塊,將系統的recvfrom函式hook成了他們實作的recvfrom函式,在實作他們自己的recvfrom函式時,直接訪問了recvfrom最后的兩個引數,因為我們的代碼直接傳入了NULL值:

這樣在他們的recvfrom內部沒有做空指標的判斷,訪問了NULL指標,觸發了記憶體訪問違例,導致VPN軟體的注入模塊發生崩潰,從而導致了我們整個程式的崩潰,對于這個問題,我們有臨時的規避辦法,我們只要傳入兩個有效的引數即可,當然在對應的代碼中,我們并不關心這兩個引數在函式呼叫完成之后的回傳值,不再傳入兩個NULL引數,
像這類出在第三方安全軟體中的問題,必須要拿出足夠的證據,證明問題是出在安全軟體上,客戶才會認可排查的結論,客戶才會找第三方安全軟體開發商去反饋問題,
2.14、添加日志列印覆寫了lasterror的值
最近在開發新版本的需求時,發現我們的客戶端軟體始終連不上某個業務服務器,抓包看到客戶端發起了三次握手的流程,客戶端發出了SYN包,服務器收到SYN包后給出了ACK回應,但是客戶端始終沒給出ACK報應答,導致建鏈失敗,

最開始我們懷疑是客戶端所在的系統的防火墻攔截了服務器的資料包,導致客戶端應用層無法收到服務器的資料包,于是將程式添加到防火墻的信任串列中,允許通過防火墻進行通信,但還是有問題,于是分別將防火墻和殺毒軟體都關閉掉,但是問題還是照舊,
后來通過排查代碼得知,建鏈時用的是非阻塞式套接字,發起connect之后會檢測connect設定的lasterror值,結果在檢查該lasterror之前,添加了一句列印:

就是這句列印引起的,列印介面中呼叫了系統API函式或者C函式,覆寫了呼叫connect時設定的lasterror,導致后續判斷connect設定的lasterror值出錯了:

然后客戶端直接關閉了套接字,結束了三次握手的流程,所以客戶端始終連接不上服務器,
類似的問題我們遇到過幾次了,本例中的問題出在開源的libjingle代碼(XMPP客戶端代碼)中,開源代碼中做了多層函式封裝,在多個函式回傳后才去判斷connect設定的lasterror值,所以該問題具有很強的隱蔽性,在我們不太熟悉代碼的情況下,添加了一句列印導致了這個問題,
2.15、格式化時格式化符與引數不一致
在資料格式化時,如果格式化符與引數型別不一致,可能會引發記憶體訪問違例,導致軟體崩潰,比如一個整型的變數(保存了一個很小的整數),結果錯誤的對應了%s格式化符,這樣格式化函式內部會把這個整型值當成一個字串首地址去對待,會讀取這個記憶體地址中的字串,但是這個地址是個很小的地址,即程式訪問了64KB范圍內的小地址存盤區,這個區域是禁止訪問的,會觸發記憶體訪問違例,系統會直接將行程終止掉,
幾年前,我們遇到這么一個看似很奇怪的格式化時的崩潰,崩潰的那行代碼如下所示:

參與格式化的兩個引數是duilib中的CStdString字串類物件,既然是字串,上面的代碼看上去是不會有問題的,但為啥運行時會產生崩潰呢?百思不得其解!后來我們才發現,CStdString字串類中有兩個資料成員,正是這兩個資料成員導致的問題,
CStdString類的成員如下所示:

在呼叫格式化函式時,待格式化的引數會將記憶體中的內容壓到堆疊上,格式化函式內部正是從堆疊上讀取待格式化資料的,這里就涉及到函式呼叫時的堆疊分布的知識了,
對于CStdString類物件,兩個資料成員都會壓入到堆疊上,第一個引數m_pstr,壓入的是字串的首地址(正是要格式化的字串),第二個資料成員m_szBuffer壓入堆疊的也是記憶體地址中的內容,不是m_szBuffer陣列的首地址,這樣格式化函式會把m_szBuffer陣列中的內容作為%s對應的字串的首地址,這明顯是有問題的,m_szBuffer陣列中保存的是字串各個字符的ASCII碼,根本不是什么字串的首地址,硬要作為字串地址去決議,可能地址是不可訪問的,會觸發記憶體訪問違例,導致崩潰,
下面給出呼叫格式化函式的堆疊分布圖,便于理解上面的問題:


2.16、同一個程式在不同系統中可能會有不同的表現
當前用的最多的系統是win7和win10,也有少部分人還在用XP,不同的系統底層的實作會有一定的差異,比如說記憶體管理模塊,同一個程式在win7上運行可能是沒問題的,但是拿到win10系統上跑可能會有問題,這個我們之前就遇到過,
即便是同一個型號的系統,比如都是win7或win10的系統,不同時期的版本也可能會有一定的差異,比如最近我們還遇到過這么一個問題,在安裝2015版win10系統(微軟surface平板)上有CPU占用過高的情況,但在2019版的win10系統中則沒有問題,
2017年的時候,我們也遇到過不同系統上有不同表現的實體,測驗同事無意發現一個掩藏很深的崩潰,并且是必現的,在win7系統上將windbg附加到行程上后,竟然不再崩潰,但是直接運行程式就有例外,這個著實有點詭異!我們將程式拿到另一臺裝有win8的電腦上運行,用windbg附加到行程上除錯,竟然是能抓到例外資訊的,這個案例也給了我們一個啟示,當遇到問題時可以到不同的系統上去跑一跑,說不定有意外的識訓,特別是在軟體發版本之前,一定要在多個版本的系統上做測驗,
不同系統中程式的表現可能會有差異,同一型別的系統在不同時期的版本中也可能會有一定的差異,所以我們在測驗的時候,要把程式拿到多個系統中去跑一跑,去測一測!
3、最后
上面簡單地介紹了一下引發軟體例外的常見原因,緊接著下一篇文章我們將講述定位這些例外的方法,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/342206.html
標籤:其他
上一篇:Qt開發經驗小技巧181-185
下一篇:(C語言)手撕資料結構之——佇列
