64位開發中去除64位平臺的內存錯誤方法
對新平臺上應用程序的開發者來說,64位平臺的穩定和可靠,是吸引他們的關鍵;而任何內存錯誤問題都會導致開發工作的失敗,內存錯誤最棘手之處在于它是難以捉摸的,找出它們非常困難且要花費大量時間。內存錯誤不會在通常意義上的測試中暴露出來,正是因為它們潛在的有害性,所以在程序定型之前,去除所有的內存問題就顯得非
|
常必要了。
目前有一些強大的內存錯誤檢測工具,它們可以在運行于雙核心處理器的應用程序中,找出導致線程內存錯誤的原因;它可在傳統測試技術找不出問題的地方,找出并修正那些難以捉摸、導致程序崩潰的"元兇"。錯誤檢測工具可幫助你在發布程序之前,找出并修正那些C/C++內存錯誤,而在移植程序之前修正這些問題,可提高在新平臺新架構上的程序質量,使移植過程更加流水線化,并且使老程序更加健壯可靠。
為何移植如此之難?
在向64位處理器或新硬件移植代碼時產生的問題當中,大多數開發者是負有主要責任的。就此來說,代碼在移植到新平臺或新架構之上時,內存問題似乎也成倍增長了。
在過渡到64位架構時最基本的問題,就是對各種不同的int和指針在比特位長度上假定。在從long轉換到int時,不管是賦值還是顯式轉換,都存在著一定的隱含限制。前者可能產生一個編譯器警告,而后者可能被無聲地接受,就此導致了運行時的各種錯誤。另一個問題就是int常量并不總是與int同樣大小,這是混淆有符號和無符號常量的問題,同時,適當地使用有關的后綴可以減少此類問題的發生。
另一些問題的主要原因是各種指針類型的不匹配。舉例來說,在多數64位架構上,指針類型不能再放入一個int中,而那些把指針值儲存在int變量中的代碼,此時當然就會出錯了。
這些問題通常會在移植過程中暴露出來,因為移植從本質上來說是一種變體測試。當你在移植代碼時,實際上是在創建一種"同等變體"(對原始代碼的小幅改動,不會影響到測試的結果),而通過這些"同等變體",可找出許多不常見的錯誤。在C/C++中,創建和運行"同等變體",可揭示出以下問題:
1、缺少拷貝構造函數或錯誤的拷貝構造函數
2、缺少或不正確的構造函數
3、初始化代碼的錯誤順序
4、指針操作的問題
5、依賴未定義的行為,如求值的順序
在準備移植應用程序時,有以下幾個相關步驟
第1步、在移植之前,要保證原始代碼中沒有諸如內存崩潰、內存泄露等問題,找出指針類型和int錯誤的最有效的一個方法是,采用平衡變體測試,來達到運行時錯誤檢測的目的。
變體測試最先是為解決無法計量測試的準確性問題而產生的,大致如下:假定已有了一個完美的測試方案,它已經覆蓋了所有的可能性,再假定已有一個完美的程序通過了這個測試,接下來修改代碼(稱之為變異),在測試方案中運行這個"變異"后的程序(稱之為變體),將會有兩個可能的情況:
一是程序會受代碼改變的影響,并且測試方案檢測到了,在此假定測試方案是完美的,這意味著它可以檢測一切改變。此時變體被稱作"已死的變體"。
二是程序沒受改變的影響,而測試方案也沒有檢測到這個變體。此時變體稱作"同等變體"。
如果拿"已死變體"和已生成的變體作對比,就會發現這個比率要小于1,這個數字表示程序對代碼改變有多敏感。事實上,完美的測試方案和完美的程序都不存在,這就說上面的兩種情況可能會有一個發生。
程序受影響的結果因個體而異,如果測試方案不適當,將無法檢測到。"已經變體"和"生成變體"的比率小于1同時也揭示了測試方案有多精確。
在實踐中,往往無法區分測試方案不精確與同等變體之間的關系。由于缺乏其他的可能性,在此我們只好把"已死變體"對所有變體的比率,看成是測試方案的精確程度。
例1(test1.c)證實了以上的說法(此處所有的代碼均在Linux下編譯),test1.c用以下命令編譯:
cc -o test1 test1.c. |
這個簡單的程序讀取輸入的參數,并打印出相關的信息。現在假定用一個測試方案來測試此程序:
Test Case 1: |
現在,對程序進行"變體",用以下簡單的改變:
Mutant 1: change line 9 to the form |
Mutants 1和3沒有改變程序的輸出,所以是同等變體,而測試方案沒有檢測
到它們。Mutant 2不是同等變體,故Test Cases 1-4將會檢測到程序的錯誤輸出,而Test Case 5在不同的電腦上可能會有不同的表現。以上表明,程序的錯誤輸出,可看作是程序可能會崩潰的一個信號。
我們統計一下,共創建了三個變體,而只被發現了一個,這說明表示測試方案的質量為1/3,正如你看到的,1/3有點低,之所以低是因為產生了兩個同等變體。這個數字應當作是測試不足的一個警告,實際上,測試方案應檢測到程序中的
|
兩個嚴重錯誤。
再回到Mutant 2,在Test Case 5中運行它,如果程序崩潰了,那這個變體測試不但計量到了測試方案的質量,還檢測到了嚴重的錯誤,這就是變體測試發現錯誤的方法。
main(argc, argv) /* line 1 */ |
在例2中的同等變體(Mutant 4),它和前一個變體的不同之處在于,Mutant 4是同等變體,這意味著它在構建時的目的,就是要使修改后的程序如同原始程序一樣運行。如果在測試方案中運行Mutant 4,那么Test Case 5大概會失敗--程序將崩潰。此處表明,通過創建一個同行變體,實際上是增強了測試方案的檢測力度,由此得出的結論是,有以下兩種方法,可提高測試方案的精確性:
·在測試方案中增加測試數量
·在測試方案中運行同等變體
這兩點是非常重要的,尤其是第二點,因為它證明了變體可提高測試的有效性。在這些例子中,是由手工創建了每一個變體,并且對每一個程序都作了單獨的修改,這個步驟費時又費力,但是自動生成同等變體是有可能的,正如例3所演示的,這個程序沒有輸入,只有一個輸出,原則上來說,它只需要一次測試:
int doublew(x) |
Test Case 1: |
試想你在例4中創建了一個同等變體,此時這個程序的結果不依賴于編譯器,實際上應是13,這也是在預料之中的。但一旦運行變體測試,就會發現錯誤了。
int doublew(x) |
在變體測試中,最讓人驚奇的是,它能找出正常看來是不可能檢測到的錯誤,通常,這些錯誤隱藏得很深,直到程序崩潰時,才可能發現,但對此,程序員經常不能理解。同等變體是找出錯誤的機會,而不是其他。但普遍來說,程序員期望同等變體能得出與原程序一樣的結果,但如果總是這樣的話,那同等變體是沒有任何作用了。
第2步:當清除最致命的錯誤之后,要把那些可能會出錯的代碼在移植之前,用靜態分析工具再確認一遍。在靜態分析時,有兩個主要的工作要做:
·找出并修正那些移植到新平臺之后可能會出錯的代碼
·找出并修正那些可能不能很好地被移植的代碼
首先,要用業界推薦的C/C++編碼標準來檢查那些可能在新平臺上出錯的代碼,以確認其編碼結構沒有問題。通過確認代碼符合編碼標準,可防止不必要的錯誤發生,還能減少在新平臺上的調試工作量,并降低在最終產品中出現bug的機率。
以下是一些可用的編碼標準:
不要返回對一個局部對象或對在函數內用"new"初始化的指針
|
的引用。對一個局部對象返回一個引用,可能會導致堆棧崩潰;而返回一個對在函數內用"new"初始化的指針的引用,可能會引起內存泄漏。
不要轉換一個常量到非常量。這可能會導致數值被改變,從而破壞數據的完整性。這也會降低代碼的可讀性,因為你不能再假定常量不被改變。
如果某個類有虛擬成員函數,它最好也帶有一個虛擬析構函數。這能在繼承類中防止內在泄漏。帶有任何虛擬成員函數的類,通常被用作基類,此時它應有一個虛擬析構函數,以保證繼承類通過一個指向基類的指針來引用時,相應的析構函數會被調用。
公共成員函數必須為成員數據返回常量句柄。當把一個非常量的句柄提供給成員數據時,此時調用者可在成員函數之外修改成員數據,這就破壞了類的封裝性。
不要把指向一個類的指針,轉換成指向另一個類的指針,除非它們之間有繼承關系。這種無效的
轉換將導致不受控的指針、數據崩潰等問題,或者其他錯誤。
不要從一個構造函數中直接訪問一個全局變量。C++語言的定義之中,沒有規定在不同的代碼單元中定義的靜態對象初始化的順序。因此,在從一個構造函數中訪問一個全局變量時,這個變量可能還沒有初始化。
當找到并修正有錯誤的代碼之后,從那些在當前平臺上運行良好的代碼中再繼續找,因為它們可能不能被很好地移植。以下是一些對大多數64位移植項目都適用的規則:
盡量使用標準類型。比如說,使用size_t而不是int。如果想要一個無符號的64位int,那么請使用uint64_t。這個習慣不但有助于找出和防止代碼中的bug,還能在將來向128位處理器移植程序時,幫上大忙。
檢查現有代碼中long數據類型的用法。如果變量、域、參數中數值的變化范圍,只在2Gig-1到-2Gig或4Gig到0之間,那么最好分別使用int32_t或uint32_t。
檢查所有的"窄向"賦值。應該避免這種情況出現,因為把一個long賦值給一個int,在64位數值上會導致截斷。
找出"窄向"轉換。應只在表達式中使用窄向轉換,而不是在操作數中。
找出那些把long*轉換成int*,或把int*轉換成long*的地方。在32位環境下,這也許是可交替的,但在64位中不行,并檢查所有的不匹配指針賦值。
找出那些在乘法符號的兩端,沒有long操作數的表達式。要使int型表達式將產生64位結果,至少其中的一個操作數是long或unsigned long。
找出long型值用int初始化的地方。應避免這種類型的初始化,因為甚至在64位類型的表達式中,int常量也可能只是代表一個32位類型。
找出那些對int進行移位操作,又把結果賦給long的地方。如果結果是64位值,最好使用64位乘法。
找出那些64位表達式中的int常量。在64位表達式中應使用64位值。
找出把指針轉換成int的地方。涉及指針與int互轉換的代碼,應仔細檢查。
檢查內聯匯編語句。因為它不可能被很好地移植。
第3步:重復一遍運行時錯誤檢測,以確認所有的修改都沒有引入新的運行時錯誤。
第4步:此時,你可選擇進行更多的測試步驟,以保證在移植之前,所有的代碼都完全正確。這個額外的步驟是單元測試,單元測試是在每一個軟件單元完成之后進行的傳統測試,它在開發階段的后期,也是有益的。因為在單元級別,很容易設計出每個函數的輸入,它將有助于更快地找出那些在應用級別測試中無法發現的錯誤。
找出64位處理器上的問題
也許64位處理器本身就有問題,如果是這樣的話,下面的步驟應該有用: 第1步:在64位處理器上重新編譯應用程序。在編譯中如果有問題,應考慮是不是因編譯器的不同而產生的。
第2步:一旦重新編譯代碼,應進行代碼檢查,以確保新代碼都遵循適當的編碼標準。在這一點上,任何人都不希望每一次修改都帶來一個錯誤,此時解決好過在程序運行時才發現。
第3步:鏈接并生成應用程序。
第4步:應試著運行程序。如果在64位處理器上,運行程序時發現了問題,應使用單元測試方法一個函數一個函數地去找,這樣能確定哪些代碼沒有正確地被移植;修正這些問題直到程序可以運行。
第5步:重復運行時錯誤檢測。
一旦程序可以運行,一定要重復一遍運行時錯誤檢測,因為移植過程很可能導致新的問題產生,例如新的內存崩潰或程序工作方式有所不同。如果運行時錯誤檢測發現了錯誤,那么此時趕快修正它。
結論
遵循此文中提及的方法,可在程序發布之前,找到并修正C/C++內存錯誤,并可以節省下數周的調試時間,使用戶免受"災難"之苦。
posted on 2009-09-08 00:35 肥仔 閱讀(2328) 評論(6) 編輯 收藏 引用 所屬分類: Windows開發