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