作者丨樸晉銹 譯者丨才璐 蓋房子時(shí),如果負(fù)責(zé)基礎(chǔ)工作的泥瓦匠手藝不精又沒(méi)有竭盡全力,房屋質(zhì)量就不可能過(guò)關(guān)。即使后期有技藝精湛的裝潢布局使得建筑物的外觀精美絕倫,根基沒(méi)有打好也會(huì)成為“豆腐渣工程”。同理,決定軟件系統(tǒng)質(zhì)量的就是編碼工作,這就要求負(fù)責(zé)編碼的程序員具有過(guò)硬的基本功。 本文將和大家分析編寫(xiě)零漏洞代碼所需的編碼準(zhǔn)則。 1 數(shù)組下標(biāo)應(yīng)從 0 開(kāi)始 C 語(yǔ)言中的數(shù)組聲明語(yǔ)句如下所示。 代碼 P280 第一行 用該語(yǔ)句聲明的數(shù)組具有如下形式。 代碼 P280 第二段 此處需要注意,數(shù)組的第一個(gè)元素的下標(biāo)為 0。也就是說(shuō),數(shù)組的第一個(gè)元素并不是 1 號(hào)元素,1 號(hào)元素實(shí)際上是數(shù)組的第二個(gè)元素。 程序員經(jīng)?;煜@一點(diǎn)。這就導(dǎo)致為第五個(gè)元素賦值時(shí),編寫(xiě)的語(yǔ)句形式如下所示。 代碼 P280 第三行 這會(huì)導(dǎo)致原本應(yīng)該賦給目標(biāo)元素的值誤賦予非目標(biāo)元素,進(jìn)而產(chǎn)生嚴(yán)重問(wèn)題。實(shí)際上,為第五個(gè)元素賦值的正確語(yǔ)句如下所示。 代碼 P281 第一行 指定數(shù)組下標(biāo)時(shí)出現(xiàn)錯(cuò)誤的原因主要在于,程序員基本功不夠扎實(shí)。要想避免這些問(wèn)題,就必須訓(xùn)練自己在指定數(shù)組下標(biāo)時(shí)能夠區(qū)分第一、第二、第三這種表示順序的序數(shù),以及一、二、三這種表示個(gè)數(shù)的基數(shù)。可以重讀自己編寫(xiě)的代碼,檢查數(shù)組下標(biāo)是否正確。 指定數(shù)組下標(biāo)時(shí)出現(xiàn)的失誤可能打亂整個(gè)數(shù)據(jù)集合。即應(yīng)該賦給 1 號(hào)元素的值被賦給了 0 號(hào)元素,而 1 號(hào)元素仍保留原值。這與程序員的本意相背離,也很難定位引起這種數(shù)據(jù)混亂的根源所在。 這種從 1 開(kāi)始對(duì)數(shù)組進(jìn)行計(jì)數(shù)引起的錯(cuò)誤又稱大小差一(off-by-one)錯(cuò)誤,顧名思義,就是少一個(gè)的意思。從 1 開(kāi)始計(jì)數(shù)可能產(chǎn)生其他錯(cuò)誤,比如無(wú)法處理數(shù)組的最后一個(gè)元素或引用無(wú)關(guān)數(shù)組,換言之,引用的數(shù)組可能始終是目標(biāo)數(shù)組的下一個(gè)數(shù)組。 特別是在 while、for、do … while 循環(huán)語(yǔ)句中,這種大小差一錯(cuò)誤更容易出現(xiàn)。 ……中略…… 計(jì)算數(shù)組所有元素總和 ……中略…… 根據(jù)注釋可知,該 while 語(yǔ)句要實(shí)現(xiàn)的功能是計(jì)算數(shù)組元素總和,那么應(yīng)該從 0 號(hào)元素開(kāi)始計(jì)算。但 counter 的值是從 1 開(kāi)始的,也就是說(shuō),是從 1 號(hào)元素開(kāi)始計(jì)算的。這就是典型的大小差一錯(cuò)誤。應(yīng)該將該語(yǔ)句改寫(xiě)如下,counter 從 0 開(kāi)始計(jì)數(shù)。 代碼 P282 第二行 隨著程序員編程經(jīng)驗(yàn)日漸豐富,這種大小差一錯(cuò)誤的出現(xiàn)率會(huì)逐漸降低。但編程菜鳥(niǎo)還是會(huì)經(jīng)常出錯(cuò),所以應(yīng)當(dāng)時(shí)刻保持警惕。特別是編寫(xiě)循環(huán)語(yǔ)句時(shí),更要多加留意。 2 置換字符串時(shí)必須使用括號(hào) 首先分析經(jīng)常被用作示例的宏函數(shù)。 代碼 P282 第三行 從代碼中可以看出,宏函數(shù) SQRT(x) 可以計(jì)算 x 的平方值。從表面看這段代碼并沒(méi)有任何問(wèn)題,運(yùn)行如下語(yǔ)句后,可以得出 y 值為 100。 代碼 P282 第四行 這是因?yàn)?,該語(yǔ)句被置換為如下形式。 代碼 P283 第一行 如果在宏函數(shù)中傳遞表達(dá)式作為參數(shù),會(huì)出現(xiàn)什么結(jié)果呢?將下列表達(dá)式傳遞到宏函數(shù),代碼還能正常運(yùn)行嗎? 代碼 P283 第二行 上述語(yǔ)句被置換為如下形式。 代碼 P283 第三行 根據(jù)該表達(dá)式的運(yùn)算符優(yōu)先級(jí),可以推測(cè)運(yùn)算順序如下,計(jì)算結(jié)果為 230。 代碼 P283 第四行 我們預(yù)期的結(jié)果應(yīng)該是 900,實(shí)際結(jié)果竟然是 230。 如何才能避免這種問(wèn)題呢?如下所示,在定義宏函數(shù)時(shí)使用括號(hào)。 代碼 P283 第五行 如上定義宏函數(shù)后,再用下列語(yǔ)句調(diào)用該函數(shù)。 代碼 P283 第六行 可以得出正確答案 900,因?yàn)樵撜Z(yǔ)句被置換為如下形式。 代碼 P283 第七行 3 文件必須有開(kāi)就有關(guān) 我編寫(xiě)過(guò)一個(gè)處理主文件的程序,該主文件一次需要保管大約 3 萬(wàn)份數(shù)據(jù)。而且主文件的特性要求,允許多個(gè)程序擁有同時(shí)使用該文件的權(quán)限。 項(xiàng)目快要收尾時(shí),我開(kāi)始進(jìn)行綜合測(cè)試。此時(shí),問(wèn)題出現(xiàn)了。所有程序中,只有我編寫(xiě)的這個(gè)程序出現(xiàn)了問(wèn)題。我從頭開(kāi)始檢查程序代碼,卻沒(méi)有發(fā)現(xiàn)任何異常。雖然在各種編譯器配置下不斷編譯、執(zhí)行,但界面始終顯示“無(wú)法打開(kāi)文件”的錯(cuò)誤信息。 這實(shí)在令人百思不得其解。我耗費(fèi)了整整一周的時(shí)間死摳代碼。當(dāng)然,整個(gè)項(xiàng)目也因此延期一周。1500 行代碼讓我眼花、惡心,最后身心交瘁,幾乎要放棄。就在這時(shí),我突然靈光乍現(xiàn):“說(shuō)不定,并不是程序有問(wèn)題?!?/p> 如果不是程序有問(wèn)題,那么是什么問(wèn)題呢?我開(kāi)始查看所有可能與該主文件有關(guān)的程序。除了我自己編寫(xiě)的程序外,還檢查了其他所有人編寫(xiě)的程序。就這樣,一周又過(guò)去了。 最終,我在一個(gè)非常小的程序里發(fā)現(xiàn)了問(wèn)題,該程序是由一個(gè)菜鳥(niǎo)程序員編寫(xiě)的。這個(gè)程序打開(kāi)相應(yīng)主文件后,沒(méi)有關(guān)閉文件。為了定位這個(gè)問(wèn)題,我浪費(fèi)了兩周。項(xiàng)目整體也因此延期兩周,最后不得不支付遲延產(chǎn)生的延期賠償金。 提示:應(yīng)該區(qū)分主文件和常見(jiàn)歷史文件。以財(cái)務(wù)工作為例,每天收到的票據(jù)單據(jù)是歷史文件,也是所有數(shù)據(jù)統(tǒng)計(jì)的基本材料。根據(jù)這些材料,可以統(tǒng)計(jì)每天累計(jì)的統(tǒng)計(jì)結(jié)果,生成資產(chǎn)平衡表和損益表,而這種文件就是主文件。 當(dāng)時(shí),軟件工程尚未普及,很多程序員的基本功并不扎實(shí)。但我認(rèn)為,即使是現(xiàn)在,總有一些地方仍然存在這種情況。當(dāng)時(shí)那個(gè)程序代碼有什么問(wèn)題呢?雖然我不能回憶所有細(xì)節(jié),但其基本形態(tài)應(yīng)該如下所示。 示例 未關(guān)閉文件 ……中略…… 該代碼試圖按照如下方式處理。 示例 未關(guān)閉文件的程序偽代碼 打開(kāi)主文件。 無(wú)限循環(huán) 讀取文件中的一行內(nèi)容。 如果讀入的行中沒(méi)有任何數(shù)據(jù), 通知“失敗”并返回。 關(guān)閉文件。 這段代碼的問(wèn)題是什么呢?問(wèn)題就在于,如果讀入的行中沒(méi)有任何數(shù)據(jù),程序會(huì)立刻返回上層程序。即使沒(méi)有讀入任何數(shù)據(jù),也應(yīng)該將已經(jīng)打開(kāi)的文件關(guān)閉后再返回。否則,文件將始終處于打開(kāi)狀態(tài),其他程序無(wú)法再次打開(kāi)該文件。因此,那個(gè)菜鳥(niǎo)程序員一開(kāi)始就應(yīng)該編寫(xiě)如下偽代碼。 示例 關(guān)閉文件的程序偽代碼 打開(kāi)主文件。 無(wú)限循環(huán) 讀取文件中的一行內(nèi)容。 如果讀入的行中沒(méi)有任何數(shù)據(jù), 關(guān)閉文件。 通知“失敗”并返回。 關(guān)閉文件。 只需在偽代碼中增加一行“關(guān)閉文件”即可。只要那個(gè)菜鳥(niǎo)程序員多寫(xiě)這么一條語(yǔ)句,就可以防止項(xiàng)目延期兩周,也就不必支付延期賠償金。當(dāng)初那個(gè)菜鳥(niǎo)程序員就應(yīng)該根據(jù)上面的偽代碼編寫(xiě)如下程序代碼! 示例 關(guān)閉文件 就因?yàn)槿鄙龠@行代碼,白白浪費(fèi)了兩周時(shí)間。 ……中略…… fclose(masterFp); 這短短一行代碼直接決定了項(xiàng)目成敗。因此,菜鳥(niǎo)程序員要牢記這一點(diǎn):絕對(duì)不能出現(xiàn)打開(kāi)文件卻沒(méi)有關(guān)閉的情況! 4 不要無(wú)視編譯器的警告錯(cuò)誤 如果程序本身有語(yǔ)法錯(cuò)誤,編譯器會(huì)在編譯過(guò)程中發(fā)現(xiàn)并提示其存在。 致命錯(cuò)誤(fatal error) 警告錯(cuò)誤(warning error) 所謂致命錯(cuò)誤通常指,如果不修復(fù)錯(cuò)誤,程序就無(wú)法運(yùn)行。但偶爾也會(huì)有些致命錯(cuò)誤并不影響程序運(yùn)行,而通常會(huì)在運(yùn)行過(guò)程中產(chǎn)生問(wèn)題,所以最好在測(cè)試過(guò)程中查找。 此處著重討論警告錯(cuò)誤。這種錯(cuò)誤可能在程序運(yùn)行過(guò)程中并不會(huì)引起什么大問(wèn)題,或者根本不會(huì)產(chǎn)生問(wèn)題。修復(fù)這種問(wèn)題就意味著修復(fù)全部程序漏洞。 這種情況下應(yīng)該怎么做呢?我在大型項(xiàng)目中做單元測(cè)試時(shí),曾遇到每次編譯都會(huì)出現(xiàn)警告錯(cuò)誤的情況。當(dāng)時(shí)同事們查看編譯器說(shuō)明文檔,想要找到這些錯(cuò)誤出現(xiàn)的原因。編譯器說(shuō)明文檔里確實(shí)記錄了相應(yīng)的修復(fù)方法。 但編譯器開(kāi)發(fā)人員和說(shuō)明文檔的作者都不是無(wú)所不能的神仙,他們也只是會(huì)犯錯(cuò)的普通人。無(wú)論我們?nèi)绾伟凑瘴臋n方法修復(fù)程序,都沒(méi)有辦法阻止警告錯(cuò)誤的出現(xiàn)。 于是,我們又嘗試用其他編譯器編譯程序。使用其他編譯器編譯時(shí),完全沒(méi)有出現(xiàn)那些警告信息。因此,我們判斷導(dǎo)致警告錯(cuò)誤出現(xiàn)的罪魁禍?zhǔn)资蔷幾g器本身。 綜合測(cè)試完成后,我們開(kāi)始進(jìn)行業(yè)務(wù)測(cè)試。在開(kāi)展手工作業(yè)的同時(shí),開(kāi)始逐漸向用電子化系統(tǒng)實(shí)現(xiàn)全自動(dòng)化工作模式轉(zhuǎn)變。之后某天,電子化系統(tǒng)突然陷入癱瘓,結(jié)果當(dāng)然引發(fā)一片混亂,訂貨公司甚至提出要完全廢止電子化系統(tǒng)的引進(jìn)。 我們整個(gè)團(tuán)隊(duì)開(kāi)始通宵分析原始代碼,但始終找不到問(wèn)題所在。突然,大家提出一個(gè)想法:不妨試試以特別不規(guī)范的數(shù)據(jù)作為輸入值。然后我們開(kāi)始嘗試輸入那些日常工作中幾乎不可能出現(xiàn)的數(shù)據(jù),從非常小的數(shù)值到非常大的數(shù)值,甚至把一些完全不是數(shù)值的字符也輸入為數(shù)值數(shù)據(jù)。經(jīng)過(guò)種種可能的嘗試,我們終于發(fā)現(xiàn)了問(wèn)題。 由于輸入了我們意料之外的非常大的數(shù)值,導(dǎo)致存儲(chǔ)位置溢出、文件混亂,最后導(dǎo)致整個(gè)系統(tǒng)癱瘓。這種現(xiàn)象稱為緩沖溢出或數(shù)據(jù)溢出。 我們最開(kāi)始使用的編譯器是如何預(yù)測(cè)并警告可能出現(xiàn)這種問(wèn)題的呢?原因無(wú)從得知。也許連編譯器開(kāi)發(fā)人員和說(shuō)明文檔的作者也并不知情吧。但可以肯定的是,如果我們當(dāng)時(shí)沒(méi)有無(wú)視編譯器的警告錯(cuò)誤,就可以事先防止系統(tǒng)癱瘓。 掌握并在編碼時(shí)防止運(yùn)行時(shí)錯(cuò)誤程序運(yùn)行過(guò)程中出現(xiàn)的錯(cuò)誤稱為運(yùn)行時(shí)錯(cuò)誤,這種錯(cuò)誤不同于編譯錯(cuò)誤和邏輯錯(cuò)誤(程序流程漏洞引起的錯(cuò)誤)。 編譯錯(cuò)誤主要是語(yǔ)法問(wèn)題引發(fā)的,邏輯錯(cuò)誤主要是程序邏輯或算法的設(shè)計(jì)缺陷引發(fā)的,而運(yùn)行時(shí)錯(cuò)誤則與運(yùn)行時(shí)環(huán)境緊密相關(guān)。 例如,典型的運(yùn)行時(shí)錯(cuò)誤——棧溢出是由于操作系統(tǒng)限制棧的大小而產(chǎn)生的。換言之,只要計(jì)算機(jī)環(huán)境發(fā)生變化,棧的大小也會(huì)發(fā)生變化,這類問(wèn)題有可能不再出現(xiàn)。只要認(rèn)清運(yùn)行時(shí)錯(cuò)誤的種類,并在編寫(xiě)代碼時(shí)注意避免這些錯(cuò)誤即可。編譯器說(shuō)明文檔詳細(xì)記錄了運(yùn)行時(shí)錯(cuò)誤,所以最好事先閱讀。下面詳細(xì)講解其中兩個(gè)最具代表性的運(yùn)行時(shí)錯(cuò)誤。這兩種錯(cuò)誤非常常見(jiàn),只要能夠避免二者的發(fā)生,就可以編寫(xiě)相當(dāng)穩(wěn)定的程序。 棧溢出計(jì)算機(jī)用棧這種數(shù)據(jù)結(jié)構(gòu)管理臨時(shí)存儲(chǔ)空間。程序中使用的自動(dòng)變量(大部分變量屬于此)在被聲明的同時(shí)就被保存到棧,而一旦脫離自動(dòng)變量使用范圍,就會(huì)被棧釋放。查看以下代碼。 示例 變量保存于棧 棧中保存變量 var1 棧中保存變量 var2 棧釋放 var2 棧中保存變量 var3 棧依次釋放 var3、var1 如上所示,變量在棧中隨時(shí)保存或釋放,所以沒(méi)有必要將棧設(shè)為無(wú)限大。以實(shí)際生活中的某個(gè)倉(cāng)庫(kù)為例思考這個(gè)問(wèn)題。如果該倉(cāng)庫(kù)中的貨物隨時(shí)進(jìn)出,而并非不間斷地堆積貨物,那么只要保證倉(cāng)庫(kù)的大小略大于貨物進(jìn)出過(guò)程中的最大堆積量即可。與之類似,棧就是變量隨時(shí)進(jìn)出的場(chǎng)所,所以大小可以受限。 而問(wèn)題正出在棧的大小受限這一點(diǎn)上。各操作系統(tǒng)都對(duì)棧的大小進(jìn)行了限制,雖然現(xiàn)在這種限制程度略有放寬,甚至一部分操作系統(tǒng)允許用戶自主控制并調(diào)整棧的大小,但處理大容量數(shù)據(jù)時(shí)仍可能發(fā)生棧溢出。例如,開(kāi)始在棧中保存自動(dòng)變量后,一旦存儲(chǔ)的變量大小超出棧的大小,就會(huì)發(fā)生棧溢出。如果出現(xiàn)棧溢出,操作系統(tǒng)會(huì)強(qiáng)制終止程序。因?yàn)槿绻麠R绯龊蟪绦蛉匀焕^續(xù)運(yùn)行,會(huì)侵犯棧之外的空間,并影響其他程序。 這種棧溢出常見(jiàn)于使用大數(shù)組或調(diào)用遞歸函數(shù)的情況。如下使用非常大的數(shù)組時(shí),該數(shù)組占用了棧的大部分空間,可供其他自動(dòng)變量存儲(chǔ)的空間相對(duì)不足。我們將對(duì)這種情況進(jìn)行單獨(dú)說(shuō)明。 示例 遞歸函數(shù) ……中略…… 如上所示,使用不斷調(diào)用函數(shù)本身的遞歸調(diào)用后,棧內(nèi)不斷累積 count 變量和 sum 變量。隨著調(diào)用次數(shù)的增加,累積變量的數(shù)量也在增加,結(jié)果可能會(huì)在某一時(shí)刻超出棧的承受范圍。如果無(wú)限調(diào)用遞歸函數(shù),一定會(huì)出現(xiàn)棧溢出。遞歸調(diào)用次數(shù)越多,出現(xiàn)棧溢出的可能性越大。 因此,使用遞歸調(diào)用時(shí),一定要細(xì)致檢查出現(xiàn)棧溢出的可能性。特別是并未準(zhǔn)確限制遞歸調(diào)用的次數(shù),而遞歸只有在滿足某個(gè)條件時(shí)才會(huì)終止的情況下,這種檢查更為必要。如下示例所示。 示例 退出條件決定遞歸調(diào)用次數(shù) ……中略…… 退出條件 該代碼的退出條件是 count 與 sum 的值相等。如果 sum 的值較小,這段代碼完全沒(méi)有問(wèn)題。也許程序員已經(jīng)假設(shè) sum 值不會(huì)太大。但假如 sum 值非常大,會(huì)出現(xiàn)什么呢?如果 sum 值是 50 000,count 是 1 呢?該函數(shù)就會(huì)被調(diào)用 50 000 次。在此期間,average 變量就會(huì)在棧中累積保存 50 000 次。不,準(zhǔn)確地說(shuō),是累積保存到棧溢出為止。 像這樣,如果遞歸調(diào)用的次數(shù)并不只是事先定好的數(shù)值,而是根據(jù)條件限制隨時(shí)可能變化的數(shù)值,那么出現(xiàn)棧溢出的可能性非常高。這種情況下,最好在函數(shù)內(nèi)部添加限制調(diào)用次數(shù)的語(yǔ)句。需要牢記,即使現(xiàn)在沒(méi)有立刻出現(xiàn)多次調(diào)用,隨著程序運(yùn)行狀況的變化,條件也可能發(fā)生變化。 除以 0 除以 0 錯(cuò)誤的原理非常簡(jiǎn)單。如下所示,將某數(shù)除以 0 的情況會(huì)觸發(fā)該錯(cuò)誤。 代碼 P292 第二段 沒(méi)有程序員會(huì)故意用 0 除某數(shù),但在非常復(fù)雜的代碼中,可能出現(xiàn)除數(shù)偶然為 0 的情況。 假定需要編寫(xiě)一個(gè)處理輸入值的程序,該程序的輸入值最小為 1。程序員經(jīng)常會(huì)忘記在程序中明確規(guī)定這一條件,一旦用戶輸入 0,并試圖用該輸入值作為除數(shù)進(jìn)行除法運(yùn)算,就會(huì)觸發(fā)錯(cuò)誤。 另一種情況常見(jiàn)于控制語(yǔ)句。試想,在 for 或 while 語(yǔ)句中,用計(jì)算循環(huán)次數(shù)的變量(計(jì)數(shù)器)作為除數(shù),與其他變量值做除法運(yùn)算。難道沒(méi)有計(jì)數(shù)器為 0 的情況嗎?倒序計(jì)數(shù)時(shí)又會(huì)怎樣呢?如下示例所示。 示例 存在除以 0 的可能性 ……中略…… 存在除以 0 的可能性 ……中略…… 該代碼中的 counter 的值遲早會(huì)變?yōu)?0,也就是說(shuō),遲早會(huì)出現(xiàn)除以 0 的情況。這種情況大多潛藏在復(fù)雜邏輯中,很難把握。這就要求程序員清醒認(rèn)識(shí)到,除以 0 的情況隨時(shí)可能出現(xiàn),并為防范這種情況的出現(xiàn)而細(xì)致檢查程序所有可能的運(yùn)行情況。 5 用靜態(tài)變量聲明大數(shù)組 C 語(yǔ)言根據(jù)變量的生存周期、影響范圍和存儲(chǔ)位置的不同,將變量分為幾類,如表 15-1 所示。 表 15-1 C 語(yǔ)言存儲(chǔ)類型 以上就是存儲(chǔ)類型。 除特別用 extern、static、register 聲明的變量外,其他所有變量均為自動(dòng)變量。因此,下列聲明語(yǔ)句 代碼 P294 第一行 與如下聲明語(yǔ)句具有相同含義。 代碼 P294 第二行 問(wèn)題就在于這個(gè)自動(dòng)變量。自動(dòng)變量存儲(chǔ)于以棧形態(tài)管理數(shù)據(jù)的內(nèi)存。嵌入式系統(tǒng)等部分常用操作系統(tǒng)中,規(guī)定的棧較小。因此,如果一個(gè)程序用到的所有自動(dòng)變量的總和超出棧的大小,就會(huì)觸發(fā)棧溢出,進(jìn)而導(dǎo)致程序異常終止。 變量最大通常不會(huì)超過(guò) 4 字節(jié),而棧大約可以存放 18 000 個(gè)變量。一個(gè)程序使用的變量個(gè)數(shù)不會(huì)超過(guò) 18 000 個(gè),最多使用幾十個(gè)到幾百個(gè)變量就足夠了。 而數(shù)組則不同。需要處理大量數(shù)據(jù)時(shí),通常會(huì)使用數(shù)組。數(shù)組包含的元素個(gè)數(shù)一般為幾十個(gè)到幾百個(gè)。隨著數(shù)組維數(shù)的增加,數(shù)組包含的元素,即變量個(gè)數(shù)以乘方形式增加。以下面聲明的三維數(shù)組為例。 代碼 P295 第一行 該數(shù)組的元素個(gè)數(shù)為 100100100=1 000 000,每個(gè)元素都是 long int 型變量,占據(jù) 4 字節(jié)。因此,該數(shù)組的總體大小為 4 字節(jié)×1 000 000 個(gè)元素 =4 000 000 字節(jié)。此時(shí),數(shù)組大小超出棧的大小,運(yùn)行過(guò)程中,程序會(huì)因?yàn)榘l(fā)生棧溢出而異常終止。 該問(wèn)題的解決方法很簡(jiǎn)單。將數(shù)組聲明為靜態(tài)變量而非自動(dòng)變量即可。 代碼 P295 第二行 提示:“用棧的形態(tài)管理”的意思是,像紙牌一樣,在上方一層層整齊疊放。因此,最后聲明的變量位于棧的最頂端,而釋放變量時(shí)同樣從最頂端開(kāi)始,逐層釋放。 如果聲明為靜態(tài)變量,那么數(shù)組的存放位置就是堆,而不是棧,也就不會(huì)出現(xiàn)棧溢出。并不只有數(shù)組大小大于棧時(shí)才能將數(shù)組聲明為靜態(tài)變量,即使數(shù)組小于棧,只要數(shù)組本身可能占據(jù)大量??臻g,就最好將數(shù)組聲明為靜態(tài)變量。這樣可以保證有足夠的空間,將數(shù)組之外的其他變量聲明為自動(dòng)變量并正常使用。 但管理靜態(tài)變量與模塊化原則相沖突,所以應(yīng)該慎重對(duì)待是否聲明靜態(tài)變量的問(wèn)題。 6 預(yù)留足夠大的存儲(chǔ)空間 我最開(kāi)始接觸計(jì)算機(jī)時(shí),RAM 容量小得可憐。具體大小記不清了,應(yīng)該在 64 KB 左右。當(dāng)時(shí),游戲開(kāi)發(fā)人員使出渾身解數(shù),只為盡可能高效利用 RAM。有人試圖用最短的機(jī)器語(yǔ)言編寫(xiě)最高效的程序,還有人用匯編語(yǔ)言編程的同時(shí),也會(huì)特意用機(jī)器語(yǔ)言編寫(xiě)其中“最耗”內(nèi)存的部分。當(dāng)時(shí)甚至還有“一行代碼”比賽,內(nèi)容就是看誰(shuí)能夠用一行代碼編寫(xiě)占內(nèi)存最少、最酷炫的程序。 隨著時(shí)間的推移,內(nèi)存價(jià)格逐漸降低,容量也在迅速擴(kuò)大,人們不必再竭盡全力節(jié)約內(nèi)存。例如,C 語(yǔ)言中用于存儲(chǔ)字符串的數(shù)組長(zhǎng)度沒(méi)有必要與字符串長(zhǎng)度相吻合。人們完全可以將數(shù)組容量定義得足夠大,更準(zhǔn)確地說(shuō),應(yīng)該是最好那么做。因?yàn)橐坏┳址L(zhǎng)度超出內(nèi)存大小,程序就會(huì)產(chǎn)生問(wèn)題。如果存儲(chǔ)的字符串超出指定內(nèi)存位置,數(shù)據(jù)之間可能發(fā)生沖突,由此導(dǎo)致程序安全大打折扣。 代碼 P296 數(shù)組定義如上所示,在該數(shù)組 inputString 存入長(zhǎng)度超過(guò) 80 個(gè)字符的字符串時(shí),會(huì)出現(xiàn)各種問(wèn)題。從根本上解決問(wèn)題的方法是,在“有效性檢查”的過(guò)程中,檢查輸入字符串是否超出對(duì)應(yīng)內(nèi)存空間的大?。涣硪环N方法是,確保定義的內(nèi)存空間遠(yuǎn)大于預(yù)期的用戶輸入值。不可否認(rèn),隨著內(nèi)存的增大,處理速度會(huì)逐漸變慢,但一般大小的內(nèi)存引起的速度降低并不會(huì)特別明顯。 例如,用戶一般輸入的字符串長(zhǎng)度為 80 個(gè)字符,那么定義時(shí)就應(yīng)該預(yù)留 2~3 倍甚至更多字符串存儲(chǔ)空間。 預(yù)留的存儲(chǔ)空間大小是預(yù)期輸入長(zhǎng)度的 10 倍。 這種做法究竟是不是資源浪費(fèi)呢?答案只有在出現(xiàn)異常情況時(shí)才能真正體現(xiàn)。一旦用戶違背了“只能輸入 80 個(gè)字符”的規(guī)定,連續(xù)幾秒內(nèi)始終按著鍵盤(pán),那么上述代碼就能發(fā)揮自己的真實(shí)價(jià)值。雖然是老生常談,但我仍要強(qiáng)調(diào),首先要驗(yàn)證輸入值的有效性,即首先檢查輸入字符串的長(zhǎng)度、內(nèi)容是否在有效范圍內(nèi)。并且為了防范省略或遺漏檢查過(guò)程的情況,應(yīng)該考慮事先預(yù)留足夠大的存儲(chǔ)空間。 7 注意信息交換引發(fā)的涌現(xiàn)效果 我聽(tīng)說(shuō)日本有一位研究機(jī)器人的科學(xué)家,他用簡(jiǎn)單的人工智能程序開(kāi)發(fā)了一些小型機(jī)器人,這些機(jī)器人之間可以進(jìn)行信息交換。有一天,突然發(fā)生了一件科學(xué)家始料未及的怪事:機(jī)器人紛紛離家出走了??茖W(xué)家不僅沒(méi)有預(yù)料到會(huì)出現(xiàn)這種事情,而且根據(jù)自己編寫(xiě)的程序邏輯看,這種事情也完全不可能發(fā)生。但事實(shí)擺在眼前,問(wèn)題是什么呢?程序有問(wèn)題嗎?可能是,也可能不是。 我偶然聽(tīng)到這個(gè)傳聞時(shí),腦海中出現(xiàn)了一個(gè)想法。當(dāng)時(shí)我正在某個(gè)公司參與編寫(xiě)安全軟件相關(guān)資料,所以萌生的想法與信息安全相關(guān):程序單元層面可能存在完全無(wú)法解決的問(wèn)題。換言之,無(wú)論怎樣強(qiáng)化個(gè)別軟件的安全級(jí)別,無(wú)論怎樣聲稱單個(gè)程序絕對(duì)不會(huì)出現(xiàn)問(wèn)題,系統(tǒng)層面總可能出現(xiàn)意想不到的狀況。 這與復(fù)雜系統(tǒng)理論中的一個(gè)現(xiàn)象非常類似。程序單元之間進(jìn)行信息交換的過(guò)程中,可能出現(xiàn)信息丟失,也可能新增冗余信息,這一點(diǎn)多少可以預(yù)見(jiàn),并成為廣為人知的安全問(wèn)題。這種問(wèn)題可以通過(guò)在各程序單元中檢查信息有效性得到解決,也就是用檢查輸入數(shù)據(jù)長(zhǎng)度的方式校驗(yàn)。 但即使如此,系統(tǒng)層面仍可能出現(xiàn)問(wèn)題。因?yàn)槌绦騿卧g進(jìn)行信息交換的過(guò)程中,可能引發(fā)涌現(xiàn)性。涌現(xiàn)性指的是能夠引起意想不到的效果的性質(zhì),是復(fù)雜系統(tǒng)相關(guān)研究領(lǐng)域非常常見(jiàn)的詞匯。程序單元之間通過(guò)交換包括數(shù)據(jù)、文字消息在內(nèi)的信息形式聯(lián)系在一起,之后,其組成的系統(tǒng)單元或上層系統(tǒng)都具有與復(fù)雜系統(tǒng)相似的特性。雖然如果現(xiàn)在主張“系統(tǒng)單元可以視為復(fù)雜系統(tǒng),并且可能產(chǎn)生涌現(xiàn)性”,會(huì)有很多人反駁說(shuō)這是無(wú)稽之談,但至少?gòu)奈夷壳暗慕?jīng)驗(yàn)看,這是可能的。 那么如何預(yù)防這種涌現(xiàn)性現(xiàn)象引起的問(wèn)題呢?目前還沒(méi)有可行的對(duì)策,但存在一種沿用至今的解決方式,即系統(tǒng)層面的綜合測(cè)試方法。例如,將包含 10 個(gè)相互聯(lián)系的程序單元稱為綜合系統(tǒng),首先在這一層級(jí)進(jìn)行綜合測(cè)試;然后將這種系統(tǒng)聚集為整個(gè)軟件系統(tǒng),在該層級(jí)再次進(jìn)行綜合測(cè)試;之后將其應(yīng)用于實(shí)際業(yè)務(wù),在人工交互階段再次進(jìn)行嚴(yán)格的綜合測(cè)試。以這樣嚴(yán)格的綜合測(cè)試為基礎(chǔ),可以在一定程度上發(fā)現(xiàn)并預(yù)防涌現(xiàn)性現(xiàn)象,但這要求在測(cè)試過(guò)程中投入與軟件開(kāi)發(fā)過(guò)程同樣多的資源。 |
|
來(lái)自: 御龍歸來(lái) > 《電腦知識(shí)》