亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  防御性編程技巧

防御性編程技巧

時間:2024-02-10
轉載:https://blog.csdn.net/everpenny/article/details/6316698
在防御性編程的大框架之下,有許多常識性的規則。人們在想到防御性編程的時候,通常都會想到“斷言”,這沒有錯。我們將在后面對此進行討論。但是,還是有一些簡單的編程習慣可以極大地提高代碼的安全性。
盡管看上去像是常識,但是這些規則卻往往被人們忽視,這就是為什么世界上并不缺少低質量軟件的原因。只要程序員們警惕起來,受到足夠的督促,更高的安全性和可靠的開發很容易就能夠實現。
在下面的幾頁中,將列出防御性編程的一些規則。我們將先從粗略的概覽開始,整體地描述防御的技巧、過程和步驟。隨著討論的深入,我們會加入更多的細節,進一步地逐條分析每條代碼語句。在這些防御性技巧中,有一些是與具體的編程語言相關的。這很自然——如果你的編程語言會讓你射傷到自己的腳,那么你一定要穿上防彈靴。
在閱讀這些規則時,請對你自己進行一個評估。在這些規則中,現在你遵循的有幾條?你打算采納哪些規則?

1 使用好的編碼風格和合理的設計哪

我們可以通過采用良好的編程風格,來防范大多數編碼錯誤。這與本篇的其他章節自然地吻合。很多簡單的事,如選用有意義的變量名,或者審慎地使用括號,都可以使編碼變得更加清晰明了,并減少缺陷出現的可能性。
同樣地,在投入到編碼工作中之前,先考慮大體的設計方案,這也非常關鍵。“最好的計算機程序的文本是結構清晰的。”(見參考書目Kernighan Plaugher 78)從實現一套清晰的API、一個邏輯系統結構以及一些定義良好的組件角色與責任開始入手,將使你避免以后處處頭疼的局面。

2 不要倉促地編寫代碼

閃電式的編程太常見了。使用這種編程方式的程序員會很快地開發出一個函數,馬上把這個函數交給編譯器來檢查語法,接著運行一遍看看能不能用,然后就進入下一個任務。這種方式充滿了危險。
相反,在寫每一行時都三思而后行。可能會出現什么樣的錯誤?你是否已經考慮了所有可能出現的邏輯分支?放慢速度,有條不紊的編程雖然看上去很平凡,但這的確是減少缺陷的好辦法。
關鍵概念 欲速則不達。每敲一個字,都要想清楚你要輸入的是什么。
在C語言中,有一個會使追求速度的程序員犯錯的陷阱,即將“==”錯誤地輸入為“=”。前者為相等關系測試,而后者則是變量賦值。如果你的編譯器功能不全(或者關閉了警告功能),你就不會得到相關提示,也就無從得知自己輸入了不該輸入的東西。
一定要在完成與一個代碼段相關的所有任務之后,再進入下一個環節。例如,如果你決定先編寫主體部分,再加入錯誤檢查和處理,那么一定要確保這兩項工作的完成都遵循章法。如果你要推遲錯誤檢查的編寫,而直接開始編寫超過三個代碼
段的主體部分,你一定要慎之又慎。你也許真的想隨后再回來編寫錯誤檢查,但卻一而再再而三地向后推遲,這期間你可能會忘記很多上下文,使得接下來的工作更加耗時和瑣碎。(當然,到時候你還要面臨一些人為設置的最后截止日期。)
遵循章法是一種習慣,需要牢記于心并切實貫徹。如果你不立即做正確的事,那么將來你很可能也不會再去做正確的事。現在就行動,不要等到撒哈拉沙漠下雨了才行動。晚做不如早做,因為將來再做將需要遵循更多的章法。

3 不要相信任何人

媽媽曾告訴過你,不要和陌生人說話。不幸的是,要想開發一個好的軟件,就需要更加憤世嫉俗,對人的天性更加不信任。即便是沒有惡意的代碼用戶,也可能會給你的程序帶來麻煩。防御意味著不能相信任何人。
下面這些情況可能是給你帶來麻煩的原因:
— 真正的用戶 意外地提供了假的輸入,或者錯誤地操作了程序;
— 惡意的用戶 故意造成不好的程序行為;
— 客戶端代碼 使用錯誤的參數調用了你的函數,或者提供了不一致的輸入;
— 運行環境 沒有為程序提供足夠的服務;
— 外部程序庫 運行失誤,不遵從你所依賴的接口協議。
你甚至可能會在編寫一個函數時犯下愚蠢的錯誤,或者錯誤地使用三年前編寫的代碼,因為你忘記了這些代碼究竟是怎樣運行的。不要設想所有的一切都運行良好,或者所有的代碼都會正確地運行。在你的程序各處都添加安全檢查。時刻注意弱點,用更多的防御性代碼防止弱點的出現。
關鍵概念 不要相信任何人毫無疑問,任何人(包括你自己)都可能把缺陷引入你的程序邏輯當中。用懷疑的眼光審視所有的輸入和所有的結果,直到你能證明它們是正確的時為止。

4 編碼的目標是清晰,而不是簡潔

如果要你從簡潔(但是有可能讓人困惑)的代碼和清晰(但是有可能比較冗長)的代碼中選擇,一定要選那些看上去和預期相符合的代碼,即使它不太優雅。例如,將復雜的代數運算拆分為一系列單獨的語句,使邏輯更清晰。
想一想,誰會是你的代碼的讀者。這些代碼也許需要一位初級程序員來進行維護,如果他不能理解代碼的邏輯,那么他肯定會犯一些錯誤。復雜的結構或不常用的語言技巧可以證明你在運算符優先級方面淵博的知識,但是這些實際上會扼殺代碼的可維護性。請保持代碼簡單。
不能維護的代碼是不安全的。舉一個極端的例子,過于復雜的表達式會使編譯器生成錯誤的代碼,許多編譯器優化的錯誤就是因此而造成的。
關鍵概念 簡單就是一種美。不要讓你的代碼過于復雜。

5 不要讓任何人做他們不該做的修補工作

內部的事情就應該留在內部。私人的東西就應該用鎖和鑰匙保管起來。不要把你的代碼初稿示于眾人。不管你多么禮貌地懇求,只要你稍不注意,別人就會篡改你的數據,然后自以為是地試著調用“僅用于執行”的例行程序。不要讓他們這樣做。
— 在面向對象的語言中,通過將屬性設為專用(private)來防止對內部類數據的訪問。在C++中,可以考慮使用Cheshire cat/pimpl idiom。(見參考書目Meyers 97)
— 在過程語言中,你仍然可以使用面向對象(oo)的打包概念,將private數據打包在不透明的類型背后,并提供可以操作它們的定義良好的公共函數。
— 將所有變量保持在盡可能小的范圍內。不到萬不得已,不要聲明全局變量。如果變量可以聲明為函數內的局部變量,就不要在文件范圍上聲明。如果變量可以聲明為循環體內的局部變量,就不要在函數范圍上聲明。
說說“何時”
何時進行防御性編程?你是否在事情不順利時才開始這樣做?或者在整理一些你不理解的代碼時才開始?
不,這是不對的,你應該從始到終地使用這些防御性編程的技巧。它們應該成為你的第二天性。成熟的程序員已經從經驗中得到教訓,在吃過不止一遍的苦頭之后,他們才明白了增加預防措施是明智的。
在開始編寫代碼時就應用防御性策略,比改進代碼時才應用要容易得多。如果你很晚才試著將這些策略強加進去,就不可能做到萬無一失。如果你在問題出現后才開始添加防御性代碼,實際上你是在調試,被動地做出反應,而不是積極地防患于未然。
然而,在調試的過程中,甚至在添加新的功能時,你將發現一些你希望驗證的情況。這常常是添加防御性代碼的好時機。

6 編譯時打開所有警告開關

大多數語言的編譯器都會在你“傷了它們感情的時候”給出一大堆錯誤信息。當這些編譯器碰到潛在的有缺陷代碼時(如在賦值之前使用C或C++變量)[3],它們也會給出各種各樣的警告。通常情況下,這些警告可以有選擇地啟用或禁用。
如果你的代碼中充滿了危險的構造,你將會得到數頁的警告信息。糟糕的是,通常的反應是禁用編譯器的警告功能,或者干脆不理會這些信息。這兩種做法都不可取。
在任何情況下都要打開你的編譯器的警告功能。如果你的代碼產生了任何的警告信息,立即修正代碼,讓編譯器的報錯聲停下來。在啟用了警告功能之后,不要對不能安靜地完成編譯的代碼感到滿意。警告的出現總是有原因的。即使你認為某個警告無關緊要,也不要置之不理。否則,總有一天這個警告會隱藏一個確實重要的警告。
關鍵概念 編譯器的警告可以捕捉到許多愚蠢的編碼錯誤。在任何情況下都啟用它們。確保你的代碼可以安安靜靜地完成編譯。

7 使用靜態分析工具

編輯器警告是對代碼的一次有限的靜態分析(即在程序運行之前執行的代碼檢查)的結果。
還有許多獨立的靜態分析工具可供使用,如用于C語言的lint(以及更多新出的衍生工具)和用于.NET匯編程序的FxCop。你的日常編程工作,應該包括使用這些工具來檢查你的代碼。它們會比你的編譯器挑出更多的錯誤。

8 使用安全的數據結構

如果你做不到,那么就安全地使用危險的數據結構。
最常見的安全隱患大概是由緩沖溢出引起的。緩沖溢出是由于不正確地使用固定大小的數據結構而造成的。如果你的代碼在沒有檢查一個緩沖的大小之前就寫入這個緩沖,那么寫入的內容總是有可能會超過緩沖的末尾的。
這種情況很容易出現,如下面這一小段C語言代碼所示:
char *unsafe_copy(const char *source){char *buffer = new char[10];strcpy(buffer, source);return buffer;}
如果source中數據的長度超過10個字符,它的副本就會超出buffer所保留內存的末尾。隨后,任何事都可能會發生。數據出錯是最好情況下的結果——一些其他數據結構的內容會被覆蓋。而在最壞的情況下,惡意用戶會利用這個簡單的錯誤,把可執行代碼加入到程序堆棧中,并使用它來任意運行他自己的程序,從而劫持了計算機。這類缺陷常常被系統黑客所利用,后果極其嚴重。
避免由于這些隱患而受到攻擊其實很簡單:不要編寫這樣的糟糕代碼!使用更安全的、不允許破壞程序的數據結構——使用類似C++的string類的托管緩沖。或者
對不安全的數據類型系統地使用安全的操作。通過把strcpy更換為有大小限制的字符串復制操作strncpy,就可以使上面的C代碼段得到保護。
char *safer_copy(const char *source){    char *buffer = new char[10];    strncpy(buffer, source, 10);    return buffer;}


9 檢查所有的返回值

如果一個函數返回一個值,它這樣做肯定是有理由的。檢查這個返回值。如果返回值是一個錯誤代碼,你就必須辨別這個代碼并處理所有的錯誤。不要讓錯誤悄無聲息地侵入你的程序;忍受錯誤會導致不可預知的行為。
這既適用于用戶自定義的函數,也適用于標準庫函數。你會發現:大多數難以察覺的錯誤都是因為程序員沒有檢查返回值而出現的。不要忘記,某些函數會通過不同的機制(例如,標準C庫的errno)返回錯誤。不論何時,都要在適當的級別上捕獲和處理相應的異常。

10 審慎地處理內存(和其他寶貴的資源)

對于在執行期間所獲取的任何資源,必須徹底釋放。內存是這類資源最常提到的一個例子,但并不是唯一的一個。文件和線程鎖也是我們必須小心使用的寶貴資源。做一個好的“管家”。
不要因為覺得操作系統會在你的程序退出時清除程序,就不注意關閉文件或釋放內存。對于你的代碼還會執行多長時間,是否會耗盡所有的文件句柄或占用所有的內存,其實你一無所知。你甚至不能肯定操作系統是否會完全釋放你的資源,有的操作系統就不是這樣的。
有一個學派說:“在確定你的程序可以運行之前,不要擔心內存的釋放;只有在能夠確定之后再添加所有相關的釋放操作。”這種觀點大錯特錯,是一種荒謬而且危險的做法。它會使你在使用內存時出現許許多多的錯誤;你將不可避免地在某些地方忘記釋放內存。
關鍵概念 重視所有稀有的資源。審慎地管理它們的獲取和釋放。
Java和.NET使用垃圾回收器來執行這些繁重的清潔工作,所以你可以“忘記”釋放資源。讓它們進入工作狀態,這樣在運行時將會不時地進行清掃。這真是一種享受,不過,不要因此而對安全性抱有錯誤的想法。你仍然需要思考。你必須顯式地終止對那些不再需要,或不會被自動清除的對象的引用;不要意外地保留對對象的引用。不太先進的垃圾回收器也很容易會被循環引用蒙蔽(例如,A引用B,B又引用A,除此之外沒有對A和B的引用)。這會導致對象永遠不會被清除;這是一種難以發現的內存泄漏形式。

11 在聲明位置初始化所有變量

這是一個顯而易見的問題。如果你初始化了每個變量,它們的用途就會是明確的。依靠像“如果我不初始化它,我就不關心初始值”的經驗主義是不安全的。代碼將會發展。未初始化的值以后可能隨時都會變成問題。
C和C++使這個問題更加復雜化。如果你意外地使用了一個沒有初始化的變量,那么你的程序在每次運行的時候都將得到不同的結果,這取決于當時內存中的垃圾信息是什么。在一個地方聲明一個變量,隨后再對它進行賦值,在這之后再使用它,這樣會為錯誤打開一個窗口。如果賦值的語句被跳過,你就會花費大量的時間來尋找程序隨機出現各種行為的原因。在聲明每個變量的時候就對它進行初始化,就可以把這個窗口關上,因為即使初始化時賦的值是錯誤的,至少出現的錯誤行為也是可以預知的。
比較安全的語言(如Java和C#)通過為所有變量定義初始值,回避了這個易犯的錯誤。在聲明變量的時候對它進行初始化仍然是一種好的做法,這樣可以提高代碼的明確性。

12 盡可能推遲一些聲明變量

盡可能推遲一些聲明變量,可以使變量的聲明位置與使用它的位置盡量接近,從而防止它干擾代碼的其他部分。這樣做也使得使用變量的代碼更加清晰。你不再需要到處尋找變量的類型和初始化,在附近聲明使這些都變得非常明顯。
不要在多個地方重用同一個臨時變量,即使每次使用都是在邏輯上相互分離的區域中進行的。變量重用會使以后對代碼重新完善的工作變得異常復雜。每次都創建一個新的變量——編譯器會解決任何有關效率的問題。

13 使用標準語言工具

在這方面,C和C++都是一場噩夢。它們的規范有許多不同的版本,使得許多情況成為了其他實現的未定義行為。現如今有很多種編譯器,每個編譯器都有一些與其他編譯器稍有不同的行為。這些編譯器大部分是相互兼容的,但是仍然存在大量的繩索會套住你的脖子。
明確地定義你正在使用的是哪個語言版本。除非你的項目要求你(最好是有一個好的理由),否則不要將命運交給編譯器,或者對該語言的任何非標準的擴展。如果該語言的某個領域還沒有定義,就不要依賴你所使用的特定編譯器的行為(例如,不要依賴你的C編譯器將char作為有符號的值對待,因為其他的編譯器并不是這樣的)。這樣做會產生非常脆弱的代碼。當你更新了編譯器之后,會發生什么?一位新的程序員加入到開發團隊中,如果他不理解那些擴展,會發生什么?依賴于特定編譯器的個別行為,將導致以后難以發現的錯誤。

14 使用好的診斷信息日志工具

當你編寫新的代碼時,常常會加入很多診斷信息,以確定程序的運行情況。在調試結束后是否應該刪除這些診斷信息呢?保留這些信息對以后再次訪問代碼會帶來很多方便,特別是如果在此期間可以有選擇地禁用這些信息。
有很多診斷信息日志系統可以幫助實現這種功能。這些系統中很多都可以使診斷信息在不需要的時候不帶來任何開銷;可以有選擇地使它們不參加編譯。

15 審慎地進行強制轉換

大多數語言都允許你將數據從一種類型強制轉換(或轉換)為另一種類型。這種操作有時比其他操作更成功。如果試著將一個64位的整數轉換為較小的8位數據類型,那么其他的56位會怎么樣呢?你的執行環境可能會突然拋出異常,或者悄悄地使你數據的完整性降級。很多程序員并不考慮這類事情,所以他們的程序就會表現出不正常的行為。
如果你真的想使用強制轉換,就必須對之深思熟慮。你所告訴編譯器的是:“忘記類型檢查吧:我知道這個變量是什么,而你并不知道。”你在類型系統中撕開了一個大洞,并直接穿越過去。這樣做很不可靠。如果你犯了任何一種錯誤,編譯器將只會靜靜地坐在那里小聲嘀咕道:“我告訴過你的。”如果你很幸運(例如使用Java或C#),運行時可能會拋出異常以讓你了解發生了錯誤,但這完全依賴于你要進行的是什么轉換。
C和C++對于數據類型的精度并不明確,所以對于數據類型的可互換性不要做任何假設。不要假設int和long的大小相同并且可以相互賦值,即使你在你的平臺上僥幸可以這樣做。代碼可以在平臺之間移植,但是糟糕的代碼可移植性很差。

16 細則

低級別防御性代碼的編寫技巧有很多。這些技巧是日常編程工作的組成部分,包含在對現實世界的一種健康的懷疑當中。下面的幾條細則值得考慮:
提供默認的行為
大多數語言都提供了一條switch語句;這些語言都將碰到default case的執行情況。如果default case是錯誤的,在代碼中將錯誤情況明示出來。如果一切都正常,也要在代碼中明示順利執行的情況,只有這樣維護代碼的程序員才會理解程序的執行情況。
同樣地,如果你要編寫一條不帶else子句的if語句,停下來想一想,你是否應該處理這個邏輯上的默認情況。
遵從語言習慣
這條簡單的建議將確保你的讀者可以明白你所編寫的所有代碼。他們做出的錯誤設想會更少。
檢查數值的上下限
即使是最基本的計算,也會使數值型變量上溢或下溢。對此要非常注意。語言規范或核心庫提供了一些機制,用來確定各個標準類型的大小——別忘了使用這些機制。確保你了解所有可用的數值類型,以及每種類型最適合的情況。
檢查并確保每一次運算都是可靠穩定的。例如,確保自己一定不要使用可能會造成除0錯誤的值。
正確設置常量
C或C++語言的程序員真的應該對常量的設置保持高度警惕,這會讓日子好過很多。盡可能將所有可以設置成常量的都設為常量。這樣做有兩個好處:首先,常量的限制條件可以充當代碼記錄;其次,常量使編譯器可以找到你所犯下的愚蠢錯誤。這樣,你就可以避免修改超出上下限的數據了。


往期推薦



四萬字長文,這是我見過最好的模板元編程文章!

如何正確的理解指針和結構體指針、指針函數、函數指針這些東西?

C++為什么要弄出虛表這個東西?

研究了一波RTTI,再介紹軟件開發的201個原則,文末再送6本書

【性能優化】高效內存池的設計與實現

這么多家公司都裁員?辟謠了,我該何去何從?

分享大廠的一些筆試題目

網傳阿里裁員2萬人…

騰訊 C++ 筆試/面試題及答案

介紹一個C++中非常有用的設計模式

研究了一下Android JNI,有幾個知識點不太懂。

Effective c++

60 張圖詳解 98 個常見網絡概念

哪家互聯網公司一周工作時間最長??太卷了!!!

沒辦法,基因決定的!

C++的lambda是函數還是對象?


主站蜘蛛池模板: 贺兰县| 柘荣县| 佛坪县| 灵武市| 马龙县| 武邑县| 湖南省| 施秉县| 东乌珠穆沁旗| 商水县| 新平| 南皮县| 饶阳县| 双峰县| 新丰县| 临清市| 滦南县| 龙井市| 工布江达县| 阳山县| 诸暨市| 无棣县| 余庆县| 民乐县| 大厂| 德惠市| 庄河市| 五家渠市| 桦川县| 时尚| 思南县| 桃江县| 襄垣县| 高要市| 双鸭山市| 军事| 尤溪县| 大宁县| 明溪县| 松原市| 张家港市|