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