一、為什麼要閱讀源代碼?
很多作家成名之前都閱讀過大量的優秀文學作品,經過長期的閱讀和寫作積累,慢慢的才有可能寫出一些好的、甚至是優秀的文學作品。 而程式員與此類似,很多程式員也需要閱讀大量的優秀程式或産品,經過不斷閱讀和實踐積累,然後可能寫出好的程式或産品。養成閱讀高品質代碼的習慣 可以提高編寫代碼的能力。
第一個好處是可以學習到很多程式設計的方法,看好的源代碼,對于提高自己的程式設計水準,比自己寫源代碼的幫助更大。當然不是說不用自己寫,而是說,自己寫代碼的同時,可以從别人寫的好的源代碼中間學習到更多的程式設計方法和技巧。
第二個好處是,可以提高自己把握大規模源代碼的能力。一個比較大型的程式,往往都是經過了很多個版本很長的時間,有很多人參與開發,修正錯誤,添加功能而發展起來的。是以往往源代碼的規模都比較大,少則10-100多k, 多的有好幾十個MB. 在閱讀大量源代碼的時候,能夠提高自己對大的軟體的把握能力,快速了解脈絡,熟悉細節,不僅僅是程式設計技巧,還能在程式的架構,設計方面提高自己的能力。(這裡說一句題外話,<<設計模式>>這本書相信很多人都看過,而且很多人對它推崇備至,奉為經典。現在也出了不少書,都是冠以"設計模式"這一名稱。在書中就提到,設計模式并不是一本教材,不是教你如何去程式設計式,而是把平時程式設計中一些固定的模式記錄下來,加以不斷的測試和改進,分發給廣大程式員的一些經驗之談。我在看這本書的時候,有一些地方一些設計方法往往讓我有似曾相識的感覺,另外一些則是我以前就常常用到的。而這些經驗的獲得,一部分得益于自己的編碼過程,另外一個很重要的來源就是閱讀别人寫的源代碼。)
閱讀源代碼第三個好處,就是獲得一些好的思想。比如,有很多人在開始一個軟體項目之前都喜歡到sourceforge.net上去找一下,是否有人以前做過相同或者相似的軟體,如果有,則拿下來讀一讀,可以使自己對這個軟體項目有更多更深的認識。我以前曾經想找一本關于如何閱讀源代碼的書來看看,卻沒有找到。相反,倒是找到了不少分析源代碼的書,比如Linux kernel, Apache source, 等等。
第四個好處是,修複、檢查、改進代碼、當程式出現bug時或半截接手别人的項目時,需要閱讀源代碼後更改bug或推進項目。帶着目的閱讀代碼
第五個好處是,閱讀的源代碼多了,發現了解開源軟體的運作機理,提取可重用的材料加以利用。他山之石,可以攻玉,閱讀源代碼進而從現有的優秀代碼、算法、設計、架構中汲取營養,提高自身的開發與設計能力
二、如何有效的閱讀源代碼?
(1)摘自《代碼閱讀方法與實踐》書籍的知識點總結
(原書目錄從微觀到宏觀,個人習慣先宏觀:文檔、架構、代碼閱讀工具、應對大型項目、一個完整的例子......微觀,實際閱讀代碼的流程)
第一章 導論
1. 要養成一個習慣,經常花時間閱讀别人編寫的高品質代碼。 (讀好書、多讀書)
2. 要有選擇地閱讀代碼,同時,還要有自己的目标。您是想學習新的模式、編碼風格、還是滿足某些需求的方法? (搞清楚為什麼要閱讀代碼? 學習架構、學習業務、學習模式、學習編碼風格、學習類庫... ... 制定目标,對自己要有要求)
3. 要注意并重視代碼中特殊的非功能性需求,這些需求也許會導緻特定的實作風格。(如 可維護性、可伸縮性、性能、)
4. 在現有的代碼上工作時,請與作者或維護人員進行必須的協調,以避免重複勞動或是以而産生厭惡情緒。
5. 請将從開放源碼軟體中得到的益處看作是一項貸款,盡可能地尋找各種方式來回報開放源碼社團。(比如、報告bug,提出問題,解決bug,添加功能貢獻代碼及文檔... ... 人人為我,我為人人)
6. 多數情況下,如果您想要了解“别人會如何完成這個功能呢?”,除了閱讀代碼以外,沒有更好的方法。
7. 在尋找 BUG時,請從問題的表現形式到問題的根源來分析代碼。不要沿着不相關的路徑(誤入岐途) (先搞清楚問題,然後找與問題相關部分代碼閱讀,記住帶有目的性的閱讀)
8. 我們要充分利用排程器,編譯器給出的警告或輸出的符号代碼,系統調用跟蹤器,資料庫結構化查詢語言的日志機制、包轉儲工具和Windows的消息偵查程式,定出BUG的位置。(要充分利用線索。 偵探破案、醫生診斷 都是利用線索或現象的)
9. 對于那些大型且組織良好的系統,您隻需要最低限度地了解它的全部功能,就能夠對它做出修改。(一個系統可能有幾十個子產品,隻需要了解與問題相關的子產品,就有可能把問題解決,但首先必須子產品設計合理)
10. 當向系統中增加新功能時,首先的任務就是找到實作類似特性的代碼,将它作為待實作功能的模闆。(做一個新功能首先考慮到的是有沒有類似的 模闆或例子,有類似模闆或例子可以提供借鑒,讓你如何實作該功能有更加深入的認識)
11. 從特性的功能描述到代碼的實作,可以按照字元串消息,或使用關鍵詞來搜尋代碼。
12. 在移植代碼或修改接口時, 您可以通過編譯器直接定位出問題涉及的範圍,進而減少代碼閱讀的工作量。
13. 進行重構時,您從一個能夠正常工作的系統開始做起,希望確定結束時系統能夠正常工作。一套恰當的測試用例可以幫助您滿足此項限制。(測試用例很關鍵)
14. 閱讀代碼尋找重構機會時,先從系統的構架開始,然後逐漸細化,能夠獲得最大的效益。(由宏觀到微觀,由上到下一種方法)
15. 代碼的可重用性是一個誘人的,但難以掌握的思想;降低期望就不會感到失望。
16. 如果您希望重要的代碼十分棘手,難以了解與分離,可以試着尋找粒度更大一些的包,甚至其他代碼。
17. 在複查軟體系統時,要注意,系統是由很多部分組成的,不僅僅隻是執行語句。還要注意分析以下内容:檔案和目錄結構、生成和配置過程、使用者界面和系統的文檔。
18. 可以将軟體複查作為一個學習、講授、援之以手和接受幫助的機會。
第二章 文檔(原書:第八章:文檔)
150.閱讀代碼時, 應該盡可能地利用任何能夠得到的文檔.
151.閱讀一小時代碼所得到的資訊隻不過相當于閱讀一分鐘文檔.
152.使用系統的規格說明文檔, 了解所閱讀代碼的運作環境.
153.軟體需求規格說明是閱讀和評估代碼的基準.
154.可以将系統的設計規格說明作為認知代碼結構的路線圖, 閱讀具體代碼的指引.
155.測試規格說明文檔為我們提供可以用來對代碼進行預演的資料.
156.在接觸一個未知系統時, 功能性的描述和使用者指南可以提供重要的背景資訊,進而更好地了解閱讀的代碼所處的上下文.
157.從使用者參考手冊中, 我們可以快速地擷取, 應用程式在外觀與邏輯上的背景知識, 從管理者手冊中可以得知代碼的接口|檔案格式和錯誤消息的詳細資訊.
158.利用文檔可以快捷地擷取系統的概況, 了解提供特定特性的代碼.
159.文檔經常能夠反映和提示出系統的底層結構.
160.文檔有助于了解複雜的算法和資料結構.
161.算法的文字描述能夠使不透明(晦澀, 難以了解)的代碼變得可以了解.
162.文檔常常能夠闡明源代碼中辨別符的含義.
163.文檔能夠提供非功能性需求背後的理論基礎.
164.文檔還會說明内部程式設計接口.
165.由于文檔很少像實際的程式代碼那樣進行測試, 并受人關注, 是以它常常可能存在錯誤|不完整或過時.
166.文檔也提供測試用例, 以及實際應用的例子.
167.文檔常常還會包括已知的實作問題或bug.
168.環境中已知的缺點一般都會記錄在源代碼中.
169.文檔的變更能夠标出那些故障點.
170.對同一段源代碼重複或互相沖突的更改, 常常表示存在根本性的設計缺陷, 進而使得維護人員需要用一系列的修補程式來修複.
171.相似的修複應用到源代碼的不同部分, 常常表示一種易犯的錯誤或疏忽, 它們同樣可能會在其他地方存在.
172.文檔常常會提供不恰當的資訊, 誤導我們對源代碼的了解.
173.要警惕那些未歸檔的特性: 将每個執行個體歸類為合理|疏忽或有害, 相應地決定是否應該修複代碼或文檔.
174.有時, 文檔在描述系統時, 并非按照已完成的實作, 而是系統應該的樣子或将來的實作.
175.在源代碼文檔中, 單詞gork的意思一般是指”了解”.
176.如果未知的或特殊用法的單詞阻礙了對代碼的了解, 可以試着在文檔的術語表(如果存在的話)|New Hacker’s Dictionary[Ray96]|或在Web搜尋引擎中查找它們.
177.總是要以批判的态度來看待文檔, 注意非傳統的來源, 比如注釋|标準|出版物|測試用例|郵件清單|新聞討論區|修訂日志|問題跟蹤資料庫|營銷材料|源代碼本身.
178.總是要以批判的态度來看待文檔; 由于文檔永遠不會執行, 對文檔的測試和正式複查也很少達到對代碼的同樣水準, 是以文檔常常會誤導讀者, 或者完全錯誤.
179.對于那些有缺陷的代碼, 我們可以從中推斷出它的真實意圖.
180.在閱讀大型系統的文檔時, 首先要熟悉文檔的總體結構和約定.
181.在對付體積龐大的文檔時, 可以使用工具, 或将文本輸出到高品質輸出裝置上, 比如雷射列印機, 來提高閱讀的效率.
第三章 系統構架(原書:第九章: 系統構架)、設計模式、編碼慣例
182.一個系統可以(在重大的系統中也确實如此)同時出多種不同的構架類型. 以不同的方式檢查同一系統|分析系統的不同部分|或使用不同級别的分解, 都有可能發現不同的構架類型.
183.協同式的應用程式, 或者需要協同通路共享資訊或資源的半自治程序, 一般會采用集中式儲存庫構架.
184.黑闆系統使用集中式的儲存庫, 存儲非結構化的鍵/值對, 作為大量不同代碼元件之間的通信集線器.
185.當處理過程可以模組化|設計和實作成一系列的資料變換時, 常常會使用資料流(或管道—過濾器)構架.
186.在批量進行自動資料處理的環境中, 經常會采用資料流構架, 在對資料工具提供大量支援的平台上尤其如此.
187.資料流構架的一個明顯征兆是: 程式中使用臨時檔案或流水線(pipeline)在不同程序間進行通信.
188.使用圖示來模組化面向對象構架中類的關系.
189.可以将源代碼輸入到模組化工具中, 逆向推導出系統的構架.
190.擁有大量同級子系統的系統, 常常按照分層構架進行組織.
191.分層構架一般通過堆疊擁有标準化接口的軟體元件來實作.
192.系統中每個層可以将下面的層看作抽象實體, 并且(隻要該層滿足它的需求說明)不關心上面的層如何使用它.
193.層的接口既可以是支援特定概念的互補函數族, 也可以是一系列支援同一抽象接口不同底層實作的可互換函數.
194.用C語言實作的系統, 常常用函數指針的數組, 表達層接口的多路複用操作.
195.用面向對象的語言實作的系統, 使用虛方法調用直接表達對層接口的多路複用操作.
196.系統可以使用不同的|獨特的層次分解模型跨各種坐标軸進行組織.
197.使用程式切片技術, 可以将程式中的資料和控制之間依賴關系集中到一起.
198.在并發系統中, 一個單獨的系統元件起到集中式管理器的作用, 負責啟動|停止和協調其他系統程序和任務的執行.
199.許多現實的系統都會博采衆家之長. 當處理此類系統時, 不要徒勞地尋找無所不包的構架圖; 應該将不同構架風格作為獨立但相關的實體來進行定位|識别并了解.
200.狀态變遷圖常常有助于理清狀态機的動作.
201.在處理大量的代碼時, 了解将代碼分解成單獨單元的機制極為重要.
202.大多數情況下, 子產品的實體邊界是單個檔案|組織到一個目錄中的多個檔案或擁有統一字首的檔案的集合.
203.C中的子產品, 由提供子產品公開接口的頭檔案和提供對應實作的源檔案組成.
204.對象的構造函數經常用來配置設定與對象相關的資源, 并初始化對象的狀态. 函數一般用來釋放對象在生命期中占用的資源.
205.對象方法經常使用類字段來存儲控制所有方法運作的資料(比如查找表或字典)或維護類運作的狀态資訊(例如, 賦給每個對象一個辨別符的計數器).
206.在設計良好的類中, 所有的字段都應在聲明為private, 并用公開的通路方法提供對它們的通路.
207.在遇到friend聲明時, 要停下來分析一下, 看看繞過類封裝在設計上的理由.
208.可以有節制地用運算符增強特定類的可用性, 但用運算符重載, 将類實作為擁有内建算術類型相關的全部功能的類實體, 是不恰當的.
209.泛型實作不是在編譯期間通過宏替換或語言所支援的功能(比如C++模闆和Ada的泛型包)來實作, 就是在運作期間通過使用資料元素的指針和函數的指針|或對象的多态性實作.
210.抽象資料類型經常用來封裝常用的資料組織方案(比如樹|清單或棧), 或者對使用者隐藏資料類型的實作細節.
211.使用庫的目的多種多樣: 重用源代碼或目标代碼, 組織子產品集合, 組織和優化編譯過程, 或是用來實作應用程式各種特性的按需載入.
212.大型的|分布式的系統經常實作為許多互相協作的程序.
213.對于基于文本的資料儲存庫, 可以通過浏覽存儲在其中的資料, 破譯出它的結構.
214.可以通過查詢資料字典中的表, 或使用資料庫專有的SQL指令, 比如show table, 來分析關系型資料庫的模式.
215.識别出重用的構架元素後, 可以查找其最初的描述, 了解正确地使用這種構架的方式, 以及可能出現的誤用.
216.要詳細分析建立在某種架構之上的應用程式, 行動的最佳路線就是從研究架構自身開始.
217.在閱讀向導生成的代碼時, 不要期望太高, 否則您會感到失望.
218.學習幾個基本的設計模式之後, 您會發現, 您檢視代碼構架的方式會發生改變: 您的視野和詞彙将會擴充到能夠識别和描述許多通用的形式.
219.頻繁使用的一些模式, 但并不顯式地指出它們的名稱, 這是由于構架性設計的重用經常先于模式的形成.
220.請試着按照底層模式來了解構架, 即使代碼中并沒有明确地提及模式.
221.大多數解釋器都遵循類似的處理構架, 圍繞一個狀态機進行建構, 狀态機的操作依賴于解釋器的目前狀态|程式指令和程式狀态.
222.多數情況下, 參考構架隻是為應用程式域指定一種概念性的結構, 具體的實作并非必須遵照這種結構.
第四章 代碼閱讀工具 (原書:第十章: 代碼閱讀工具)
223.詞彙工具可以高效地在一個大代碼檔案中或者跨多個檔案查找某種模式.
224.使用程式編輯器和正規表達式查找指令, 浏覽龐大的源代碼檔案.
225.以隻讀方式浏覽源代碼檔案.
226.使用正規表達式 ^function name 可以找出函數的定義.
227.使用正規表達式的字元類, 可以查找名稱遵循特定模式的變量.
228.使用正規表達式的否定字元類, 可以避免非積極比對.
229.使用正規表達式 symbol-1. *symbol-2, 可以查找出現在同一行的符号.
230.使用編輯器的 tags 功能, 可以快速地找出實體的定義.
231.可以用特定的 tag 建立工具, 增加編輯器的浏覽功能.
232.使用編輯器的大綱視圖, 可以獲得源代碼結構的鳥瞰圖.
233.使用您的編輯器來檢測源代碼中圓括号|方括号和花括号的比對.
234.使用 grep 跨多個檔案查找代碼模式.
235.使用 grep 定位符号的聲明|定義和應用.
236.當您不能精确地表述要查找的内容時, 請使用關鍵單詞的詞幹對程式的源代碼進行查找.
237.用 grep 過濾其他工具生成的輸出, 分離出您要查找的項.
238.将 grep 的輸出輸送到其他工具, 使複雜處理任務自動化.
239.通過對 grep 的輸出進行流編輯, 重用代碼查找的結果.
240.通過選取與噪音模式不比對的輸出行(grep-v), 過濾虛假的 grep 輸出.
241.使用 fgrep 在源代碼中查找字元串清單.
242.查找注釋, 或辨別符大小寫不敏感的語言編寫的代碼時, 要使用大小寫不敏感的模式比對(grep -i).
243.使用 grep –n 指令行開關, 可以建立與給定正規表達式比對的檔案和行号的檢查表.
244.可以使用 diff 比較檔案或程式不同版本之間的差别.
245.在運作 diff 指令時, 可以使用 diff –b, 使檔案比較算法忽略結尾的空格, 用 –w 忽略所有空白區域的差異, 用 –i 使檔案比較對大小寫不敏感.
246.不要對建立自己的代碼閱讀工具心存畏懼.
247.在建構自己的代碼閱讀工具時: 要充分利用現代快速原型語言所提供的能力; 從簡單開始, 根據需要逐漸改進; 使用利用代碼詞彙結構的各種試探法; 要允許一些輸出噪音或寂靜(無關輸出或缺失輸出); 使用其他工具對輸入進行預處理, 或者對輸出進行後期處理.
248.要使編譯器成為您的: 指定恰當級别的編譯器警告, 并小心地評估生成的結果.
249.使用C預處理器理清那些濫用預處理器特性的程式.
250.要徹底地了解編譯器如何處理特定的代碼塊, 需要檢視生成的符号(彙編)代碼.
251.通過分析相應目标檔案中的符号, 可以清晰地了解源檔案的輸入和輸出.
252.使用源代碼浏覽器浏覽大型的代碼集合以及對象類型.
253.要抵制住按照您的編碼規範對外部代碼進行美化的誘惑; 不必要的編排更改會建立不同的代碼, 并妨礙工作的組織.
254.優美列印程式和編輯器文法着色可以使得程式的源代碼為易讀.
255.cdecl 程式可以将難以了解的C和C++類型聲明轉換成純英語(反之亦然).
256.實際運作程式, 往往可以更深刻地了解程式的動作.
257.系統調用|事件和資料包跟蹤程式可以增進對程式動作的了解.
258.執行剖析器可以找出需要着重優化的代碼, 驗證輸入資料的覆寫性, 以及分析算法的動作.
259.通過檢查從未執行的代碼行, 可以找出測試覆寫的弱點, 并據此修正測試資料.
260.要探究程式動态動作時的每個細節, 需要在調試器中運作它.
261.将您覺得難以了解的代碼列印到紙上.
262.可以繪制圖示來描繪代碼的動作.
263.可以試着向别人介紹您在閱讀的代碼, 這樣做一般會增進您對代碼的了解.
264.了解複雜的算法或巧妙的資料結構, 要選擇一個安靜的環境, 然後聚精會神地考慮, 不要借助于任何計算機化或自動化的幫助.
第五章 應對大型項目 (第六章: 應對大型項目)
116.我們可以通過浏覽項目的源代碼樹—包含項目源代碼的層次目錄結構, 來分析一個項目的組織方式. 源碼樹常常能夠反映出項目在構架和軟體過程上的結構.
117.應用程式的源代碼樹經常是該應用程式的部署結構的鏡像.
118.不要被龐大的源代碼集合吓倒; 它們一般比小型的專門項目組織得更出色.
119.當您首次接觸一個大型項目時, 要花一些時間來熟悉項目的目錄樹結構.
120.項目的源代碼遠不隻是編譯後可以獲得可執行程式的計算機語言指令; 一個項目的源碼樹一般還包括規格說明|最終使用者和開發人員文檔|測試腳本|多媒體資源|編譯工具|例子|本地化檔案|修訂曆史|安裝過程和許可資訊.
121.大型項目的編譯過程一般聲明性地借助依賴關系來說明. 依賴關系由工具程式, 如make/ant/Maven及其派生程式, 轉換成具體的編譯行動.
122.大型項目中, 制作檔案常常由配置步驟動态地生成; 在分析制作檔案之前, 需要先執行項目特定的配置.
123.檢查大型編譯過程的各個步驟時, 可以使用make程式的-n開關進行預演.
124.修訂控制系統提供從儲存庫中擷取源代碼最新版本的方式.
125.可以使用相關的指令, 顯示可執行檔案中的修訂辨別關鍵字, 進而将可執行檔案與它的源代碼比對起來.
126.使用修訂日志中出現的bug跟蹤系統内的編号, 可以在bug跟蹤系統的資料庫中找到有關的問題的說明.
127.可以使用修訂控制系統的版本儲存庫, 找出特定的變更是如何實作的.
128.定制編譯工具用在軟體開發過程的許多方面, 包括配置|編譯過程管理|代碼的生成|測試和文檔編制.
129.程式的調試輸出可以幫助我們了解程式控制流程和資料元素的關鍵部分.
130.跟蹤語句所在的地點一般也是算法運作的重要部分.
131.可以用斷言來檢驗算法運作的步驟|函數接收的參數|程式的控制流程|底層硬體的屬性和測試用例的結果.
132.可以使用對算法進行檢驗的斷言來證明您對算法運作的了解, 或将它作為推理的起點.
133.對函數參數和結果的斷言經常記錄了函數的前置條件和後置條件.
134.我們可以将測試整個函數的斷言作為每個給定函數的規格說明.
135.測試用例可以部分地代替函數規格說明.
136.可以使用測試用例的輸入資料對源代碼序列進行預演.
第六章 一個完整的例子 (第十一章: 一個完整的例子)
265.模仿軟體的功能時, 要依照相似實體的線路(類|函數|子產品). 在相似的現有實體中, 為簡化對源代碼庫的文本查找, 應選取比較罕見的名稱.
266.自動生成的檔案常常會在檔案的開關有一段注釋, 說明這種情況.
267.如果試圖精确地分析代碼, 一般會陷入數量衆多的類|檔案和子產品中, 這些内容會很快将我們淹沒; 是以, 我們必須将需要了解的代碼限定在絕對必需的範圍之内.
268.采用一種廣度優先查找政策, 從多方攻克代碼閱讀中存在的問題, 進到找出克服它們的方法為止.
第七章基本程式設計元素 (第二章:基本程式設計元素)
19.第一次分析一個程式時, main是一個好的起始點.
20.層疊if-else if-...-else序列可以看作是由互斥選擇項組成的選擇結構.
21.有時, 要想了解程式在某一方面的功能, 運作它可能比閱讀源代碼更為恰當.(運作,并debug可以降低了解動态流程的難度)
22.在分析重要的程式時, 最好首先識别出重要的組成部分. (打蛇打三寸,抓住關鍵部分,可以快速了解)
23.了解局部的命名約定, 利用它們來猜測變量和函數的功能用途.(類名用名詞或代詞或動名詞,最好不要用動詞,方法用動詞)
24.當基于猜測修改代碼時, 您應該設計能夠驗證最初假設的過程. 這個過程可能包括用編譯器進行檢查|引入斷言|或者執行适當的測試用例.
25.了解了代碼的某一部分, 可能幫助你了解餘下的代碼.
26.解決困難的代碼要從容易的部分入手.
27.要養成遇到庫元素就去閱讀相關文檔的習慣; 這将會增強您閱讀和編寫代碼的能力.
28.代碼閱讀有許多可選擇的政策: 自底向上和自頂向下的分析|應用試探法和檢查注釋和外部文檔, 應該依據問題的需要嘗試所有這些方法.
29.for (i=0; i<n; i++)形式的循環執行n次; 其他任何形式都要小心.
30.涉及兩項不等測試(其中一項包括相等條件)的比較表達式('0' <= c && c <= '9')可以看作是區間成員測試(0 <= c <= 9).
31.我們經常可以将表達式應用在樣本資料上, 借以了解它的含義. (用用例去驗證表達式)
32.使用De Morgan法則簡化複雜的邏輯表達式. (?)
33.在閱讀邏輯乘表達式(&&)時, 問題可以認為正在分析的表達式以左的表達式均為true; 在閱讀邏輯和表達式(||)時, 類似地, 可以認為正在分析的表達式以左的表達式均為false.
34.重新組織您控制的代碼, 使之更為易讀.
35.将使用條件運作符? :的表達式了解為if代碼.
36.不需要為了效率, 犧牲代碼的易讀性.
37.高效的算法和特殊的優化确實有可能使得代碼更為複雜, 進而更難了解, 但這并不意味着使代碼更為緊湊和不易讀會提高它的效率.
38.創造性的代碼布局可以用來提高代碼的易讀性.
39.我們可以使用空格|臨時變量和括号提高表達式的易讀性.
40.在閱讀您所控制的代碼時, 要養成添加注釋的習慣. (在閱讀中比較複雜或難懂的地方要加注釋備注)
41.我們可以用好的縮進以及對變量名稱的明智選擇, 提高編寫欠佳的程式的易讀性.(養成好的編碼習慣)
42.用diff程式分析程式的修訂曆史時, 如果這段曆史跨越了整體重新縮排, 常常可以通過指定-w選項, 讓diff忽略空白差異, 避免由于更改了進層次而引入的噪音.(SVN/Git是否也有這個功能呢?)
43.do循環的循環體至少執行一次.
44.執行算術運算時, 當b=2n-1時, 可以将a&b了解為a%(b+1). (?)
45.将a<<n了解為a*k, k=2n.
46.将a>>n了解為a/k, k=2n.
47.每次隻分析一個控制結構, 将它的内容看作是一個黑盒.
48.将每個控制結構的控制表達式看作是它所包含代碼的斷言.
49.return, goto, break和continue語句, 還有異常, 都會影響結構化的執行流程. 由于這些語句一般都會終止或重新開始正在進行的循環,是以要單獨推理它們的行為.
50.用複雜循環的變式和不變式, 對循環進行推理.
51.使用保持含義不變的變換重新安排代碼, 簡化代碼的推理工作.
第八章 進階C資料類型 (第三章: 進階C資料類型)
52.了解特定語言構造所服務的功能之後, 就能夠更好地了解使用它們的代碼.
53.識别并歸類使用指針的理由.
54.在C程式中, 指針一般用來構造鍊式資料結構|動态配置設定的資料結構|實作引用調用|通路和疊代資料元素|傳遞數組參數|引用函數|作為其他值的别名|代表字元串|以及直接通路系統記憶體.
55.以引用傳遞的參數可以用來傳回函數的結果, 或者避免參數複制帶來的開銷.
56.指向數組元素位址的指針, 可以通路位于特定索引位置的元素.
57.指向數組元素的指針和相應的數組索引, 作用在二者上的運算具有相同的語義.
58.使用全局或static局部變量的函數大多數情況都不可重入(reentrant).
59.字元指針不同于字元數組.
60.識别和歸類應用結構或共用體的每種理由.
61.C語言中的結構将多個資料元素集合在一起, 使得它們可以作為一個整體來使用, 用來從函數中傳回多個資料元素|構造鍊式資料結構|映射資料在硬體裝置|網絡連結和存儲媒體上的組織方式|實作抽象資料類型|以及以面向對象的方式程式設計.
62.共用體在C程式中主要用于優化存儲空間的利用|實作多态|以及通路資料不同的内部表達方式.
63.一個指針, 在初始化為指向N個元素的存儲空間之後, 就可以作為N個元素的數組來使用.
64.動态配置設定的内在塊可以電焊工地釋放, 或在程式結束時釋放, 或由垃圾回收器來完成回收; 在棧上配置設定的記憶體塊當配置設定它的函數退出後釋放.
65.C程式使用typedef聲明促進抽象, 并增強代碼的易讀性, 進而防範可移植性問題, 并模拟C++和Java的類聲明行為.
66.可以将typedef聲明了解成變量定義: 變量的名稱就是類型的名稱; 變量的類型就是與該名稱對應的類型.
第九章 C資料結構 (第四章: C資料結構)
67.根據底層的抽象資料類型了解顯式的資料結構操作.
68.C語言中, 一般使用内建的數組類型實作向量, 不再對底層實作進行抽象.
69.N個元素的數組可以被序列for (i=0; i<N; i++)完全處理; 所有其他變體都應該引起警惕.
70.表達式sizeof(x)總會得到用memset或memcpy處理數組x(不是指針)所需的正确位元組數.
71.區間一般用區間内的第一個元素和區間後的第一個元素來表示.
72.不對稱區間中元素的數目等于高位邊界與低位邊界的差.
73.當不對稱區間的高位邊界等于低位邊界時, 區間為空.
74.不對稱區間中的低位邊界代表區間的第一個元素; 高位邊界代表區間外的第一個元素.
75.結構的數組常常表示由記錄和字段組成的表.
76.指向結構的指針常常表示通路底層記錄和字段的遊标.
77.動态配置設定的矩陣一般存儲為指向數組列的指針或指向元素指針的指針; 這兩種類型都可以按照二維數組進行通路.
78.以數組形式存儲的動态配置設定矩陣, 用自定義通路函數定位它們的元素.
79.抽象資料類型為底層實作元素的使用(或誤用)方式提供一種信心的量度.
80.數組用從0開始的順序整數為鍵, 組織查找表.
81.數組經常用來對控制結構進行高效編碼, 簡化程式的邏輯.
82.通過在數組中每個位置存儲一個資料元素和一個函數指針(指向處理資料元素的函數), 可以将代碼與資料關聯起來.
83.數組可以通過存儲供程式内的抽象機(abstract machine)或虛拟機(virtual machine)使用的資料或代碼, 控制程式的運作.
84.可以将表達式sizeof(x) / sizeof(x[0])了解為數組x中元素的個數.
85.如果結構中含有指向結構自身|名為next的元素, 一般說來, 該結構定義的是單向連結清單的結點.
86.指向連結清單結點的持久性(如全局|靜态或在堆上配置設定)指針常常表示連結清單的頭部.
87.包含指向自身的next和prev指針的結構可能是雙向連結清單的結點.
88.了解複雜資料結構的指針操作可以将資料元素畫為方框|指針畫為箭頭.
89.遞歸資料結構經常用遞歸算法來處理.
90.重要的資料結構操作算法一般用函數參數或模闆參數來參數化.
91.圖的結點常常順序地存儲在數組中, 連結到連結清單中, 或通過圖的邊連結起來.
92.圖中的邊一般不是隐式地通過指針, 就是顯式地作為獨立的結構來表示.
93.圖的邊經常存儲為動态配置設定的數組或連結清單, 在這兩種情況下, 邊都錨定在圖的結點上.
94.在無向圖中, 表達資料時應該将所有的結點看作是等同的, 類似地, 進行處理任務的代碼也不應該基于它們的方向來區分邊.
95.在非連通圖中, 執行周遊代碼應該能夠接通孤立的子圖.
96.處理包含回路的圖時, 周遊代碼應該避免在處理圖的回路進入循環.
97.複雜的圖結構中, 可能隐藏着其他類型的獨立結構.
第十章 進階控制流程 (第五章: 進階控制流程)
98.采用遞歸定義的算法和資料結構經常用遞歸的函數定義來實作.
99.推理遞歸函數時, 要從基準落伍測試開始, 并認證每次遞歸調用如何逐漸接近非遞歸基準範例代碼.
100.簡單的語言常常使用一系列遵循該語言文法結構的函數進行文法分析.
101.推理互遞歸函數時, 要基于底層概念的遞歸定義.
102.尾遞歸調用等同于一個回到函數開始處的循環.
103.将throws子句從方法的定義中移除, 然後運作Java編譯器對類的源代碼進行編譯, 就可以容易地找到那些可能隐式地生成異常的方法.
104.在多處理器計算機上運作的代碼常常圍繞程序或線程進行組織.
105.工作群并行模型用于在多個處理器間配置設定工作, 或者建立一個任務池, 然後将大量需要處理标準化的工作進行配置設定.
106.基于線程的管理者/勞工并行模型一般将耗時的或阻塞的操作配置設定給勞工子任務, 進而維護中心任務的響應性.
107.基于程序的管理者/勞工并行模型一般用來重用現有的程式, 或用定義良好的接口組織和分離粗粒度的系統子產品.
108.基于流水線的并行進行中, 每個任務都接收到一些輸入, 對它們進行一些處理, 并将生成的輸出傳遞給下一個任務, 進行不同的處理.
109.競争條件很難捉摸, 相關的代碼常常會将競争條件擴散到多個函數或子產品; 因而, 很難隔離由于競争條件導緻的問題.
110.對于出現在信号處理器中的資料結構操作代碼和庫調用要保持高度警惕.
111.在閱讀包含宏的代碼時, 要注意, 宏既非函數, 也非語句.
112.do…while(0)塊中的宏等同于控制塊中的語句.
113.宏可以通路在它的使用點可見的所有局部變量.
114.宏調用可改變參數的值
115.基于宏的标記拼接能夠建立新的标記符.
第十一章 編碼規範和約定 (第七章: 編碼規範和約定)
137.了解了給定代碼庫所遵循的檔案組織方式後, 就能更有效率地浏覽它的源代碼.
138.閱讀代碼時, 首先要確定您的編輯器或優美列印程式的tab設定, 與代碼遵循的風格規範一緻.
139.可以使用代碼塊的縮進, 快速地掌握代碼的總體結構.
140.對編排不一緻的代碼, 應該立即給予足夠的警惕.
141.分析代碼時, 對标記為XXX, FIXME和TODO的代碼序列要格外注意: 錯誤可能就潛伏在其中.
142.常量使用大寫字母命名, 單詞用下劃線分隔.
143.在遵循Java編碼規範的程式中, 包名(package name)總是從一個頂級的域名開始(例如, org, com), 類名和接口名由大寫字母開始, 方法
和變量名由小寫字母開始.
144.使用者界面控件名稱之前的匈牙利記法的字首類型标記可以幫助我們确定它的作用.
145.不同的程式設計規範對可移植構造的構成有不同的主張.
146.在審查代碼的可移植性, 或以某種給定的編碼規範作為指南時, 要注意了解規範對可移植性需求的界定與限制.
147.如果GUI功能都使用相應的程式設計結構來實作, 則通過代碼審查可以輕易地驗證給定使用者界面的規格說明是否被正确地采用.
148.了解項目編譯過程的組織方式與自動化方式之後, 我們就能夠快速地閱讀與了解對應的編譯規則.
149.當檢查系統的釋出過程時, 常常可以将相應發行格式的需求作為基準.
(2)如何閱讀linux源代碼
随着linux的逐漸普及,現在有不少人對于Linux的安裝已經比較熟悉了。與Linux的蓬勃發展相适應,想深入了解Linux的也越來越多。而要想深入了解Linux,就需要閱讀和分析linux核心的源代碼。
Linux的核心源代碼可以從很多途徑得到。一般來講,在安裝的linux系統下,/usr/src/linux目錄下的東西就是核心源代碼。另外還可以從互連網上下載下傳,解壓縮後檔案一般也都位于linux目錄下。核心源代碼有很多版本,目前最新的穩定版是3.0.3。
許多人對于閱讀Linux核心有一種恐懼感,其實大可不必。當然,象Linux核心這樣大而複雜的系統代碼,閱讀起來确實有很多困難,但是也不像想象的那麼高不可攀。隻要有恒心,困難都是可以克服的。也不用擔心水準不夠的問題,事實上,有很多事情我們不都是從不會到會,邊幹邊學的嗎?再就是根據自己的工作需要針對某一部分進行深入了解,畢竟現在linux核心太龐大了,全面了解的困難不小。
任何事情做起來都需要有方法和工具。正确的方法可以指導工作,良好的工具可以事半功倍。對于Linux核心源代碼的閱讀也同樣如此。下面我就把自己閱讀核心源代碼的一點經驗介紹一下,最後介紹Window平台下的一種閱讀工具。
對于源代碼的閱讀,要想比較順利,事先最好對源代碼的知識背景有一定的了解。對于linux核心源代碼來講,我認為,
基本要求是:a、作業系統的基本知識;b、對C語言比較熟悉,最好要有彙編語言的知識和GNU C對标準C的擴充的知識的了解。另外在閱讀之前,還應該知道Linux核心源代碼的整體分布情況。我們知道現代的作業系統一般由程序管理、記憶體管理、檔案系統、驅動程式、網絡等組成。看一下Linux核心源代碼就可看出,各個目錄大緻對應了這些方面。
Linux核心源代碼的組成如下(假設相對于linux目錄):
arch 這個子目錄包含了此核心源代碼所支援的硬體體系結構相關的核心代碼。如對于X86平台就是i386。
include 這個目錄包括了核心的大多數include檔案。另外對于每種支援的體系結構分别有一個子目錄。
init 此目錄包含核心啟動代碼。
mm 此目錄包含了所有的記憶體管理代碼。與具體硬體體系結構相關的記憶體管理代碼位于archkernel目錄下。
net 核心的網絡部分代碼。裡面的每個子目錄對應于網絡的一個方面。
lib 此目錄包含了核心的庫代碼。與處理器結構相關庫代碼被放在arch
epoch=jdate(1,1,1970);
add_nlist("index.",&index_alias);
這兩個函數暫時不用仔細看,後面會提到,略過。
sprintf(tmp_buf,"%s/webalizer.conf",ETCDIR);
if (!access("webalizer.conf",F_OK))
get_config("webalizer.conf");
else if (!access(tmp_buf,F_OK))
get_config(tmp_buf);
從注釋和程式本身可以看出,這是查找是否存在一個叫做webalizer.conf的配置檔案,如果目前目錄下有,則用get_config來讀入其中内容,如果沒有,則查找ETCDIR/webalizer.conf是否存在。如果都沒有,則進入下一部分。(注意:ETCDIR = @ETCDIR@在makefile中有定義)
opterr = 0;
while ((i=getopt(argc,argv,"a:A:c:C:dD:e:E:fF:
g:GhHiI:l:Lm:M:n:N:o:pP:qQr:R:s:S:t:Tu:U:vVx:XY"))!=EOF)
{
switch (i)
{
case 'a': add_nlist(optarg,&hidden_agents); break;
case 'A': ntop_agents=atoi(optarg); break;
case 'c': get_config(optarg); break;
case 'C': ntop_ctrys=atoi(optarg); break;
case 'd': debug_mode=1; break;
case 'D': dns_cache=optarg; break;
case 'e': ntop_entry=atoi(optarg); break;
case 'E': ntop_exit=atoi(optarg); break;
case 'f': fold_seq_err=1; break;
case 'F': log_type=(optarg[0]=='f')?
LOG_FTP:(optarg[0]=='s')?
LOG_SQUID:LOG_CLF; break;
case 'g': group_domains=atoi(optarg); break;
case 'G': hourly_graph=0; break;
case 'h': print_opts(argv[0]); break;
case 'H': hourly_stats=0; break;
case 'i': ignore_hist=1; break;
case 'I': add_nlist(optarg,&index_alias); break;
case 'l': graph_lines=atoi(optarg); break;
case 'L': graph_legend=0; break;
case 'm': visit_timeout=atoi(optarg); break;
case 'M': mangle_agent=atoi(optarg); break;
case 'n': hname=optarg; break;
case 'N': dns_children=atoi(optarg); break;
case 'o': out_dir=optarg; break;
case 'p': incremental=1; break;
case 'P': add_nlist(optarg,&page_type); break;
case 'q': verbose=1; break;
case 'Q': verbose=0; break;
case 'r': add_nlist(optarg,&hidden_refs); break;
case 'R': ntop_refs=atoi(optarg); break;
case 's': add_nlist(optarg,&hidden_sites); break;
case 'S': ntop_sites=atoi(optarg); break;
case 't': msg_title=optarg; break;
case 'T': time_me=1; break;
case 'u': add_nlist(optarg,&hidden_urls); break;
case 'U': ntop_urls=atoi(optarg); break;
case 'v':
case 'V': print_version(); break;
case 'x': html_ext=optarg; break;
case 'X': hide_sites=1; break;
case 'Y': ctry_graph=0; break;
}
}
if (argc - optind != 0) log_fname = argv[optind];
if ( log_fname && (log_fname[0]=='-')) log_fname=NULL;
if (log_fname) if (!strcmp((log_fname+strlen(log_fname)-3),".gz"))
gz_log=1;
這一段是分析指令行參數及開關。(getopt()的用法我在另外一篇文章中講過,這裡就不再重複了。)可以看到,這個軟體雖然功能不太複雜,但是開關選項還是不少。大多數的unix/linux程式的開頭部分都是這個套路,初始化配置檔案,并且讀入分析指令行。在這段程式中,我們需要注意一個函數:add_nlist(). print_opts(), get_config()等等一看就明白,就不用多講了。這裡我們已經是第二次遇到add_nlist這個函數了,就仔細看看吧。
$ grep add_nlist *.h
linklist.h:extern int add_nlist(char *, NLISTPTR *);
可以發現它定義在linklist.h中。
在這個h檔案中,當然會有一些資料結構的定義,比如:
struct nlist { char string[80];
struct nlist *next; };
typedef struct nlist *NLISTPTR;
struct glist { char string[80];
char name[80];
struct glist *next; };
typedef struct glist *GLISTPTR;
這是兩個連結清單結構。還有
extern GLISTPTR group_sites ;
extern GLISTPTR group_urls ;
extern GLISTPTR group_refs ;
這些都是連結清單, 太多了,不用一一看得很仔細,因為目前也看不出來什麼東西。當然要注意它們是extern的, 也就是說,可以在其他地方(檔案)看到它們的數值(類似于C++中的public變量)。這裡還定義了4個函數:
extern char *isinlist(NLISTPTR, char *);
extern char *isinglist(GLISTPTR, char *);
extern int add_nlist(char *, NLISTPTR *);
extern int add_glist(char *, GLISTPTR *);
注意,這些都是extern的,也就是說,可以在其他地方見到它們的調用(有點相當于C++中的public函數)。再來看看linklist.c,
NLISTPTR new_nlist(char *);
void del_nlist(NLISTPTR *);
GLISTPTR new_glist(char *, char *);
void del_glist(GLISTPTR *);
int isinstr(char *, char *);
這5個函數是内部使用的(相當于C++中的private), 也就是說,這些函數隻被isinlist(NLISTPTR, char *), isinglist(GLISTPTR, char *), add_nlist(char *, NLISTPTR *), add_glist(char *, GLISTPTR *)調用,而不會出現在其他地方。是以,我們先來看這幾個内部函數。舉例來說,
add_nlist(char *)
NLISTPTR new_nlist(char *str)
{
NLISTPTR newptr;
if (sizeof(newptr->string) < strlen(str))
{
if (verbose)
fprintf(stderr,"[new_nlist] %s ",msg_big_one);
}
if (( newptr = malloc(sizeof(struct nlist))) != NULL)
{strncpy(newptr->string, str, sizeof(newptr->string));
newptr->next=NULL;}
return newptr;
}
這個函數配置設定了一個struct nlist, 并且把其中的string指派為str, next指派為NULL.這實際上是建立了連結清單中的一個節點。verbose是一個全局變量,定義了輸出資訊的類型,如果verbose為1,則輸出很詳細的資訊,否則輸出簡略資訊。這是為了調試或者使用者詳細了解程式情況來用的。不是重要内容,雖然我們常常可以在這個源程式的其他地方看到它。另外一個函數:
void del_nlist(NLISTPTR *list)
{
NLISTPTR cptr,nptr;
cptr=*list;
while (cptr!=NULL)
{
nptr=cptr->next;
free(cptr);
cptr=nptr;
}
}
這個函數删除了一個nlist(也可能是list所指向的那一個部分開始知道連結清單結尾),比較簡單。看完了這兩個内部函數,可以來看
int add_nlist(char *str, NLISTPTR *list)
{
NLISTPTR newptr,cptr,pptr;
if ( (newptr = new_nlist(str)) != NULL)
{
if (*list==NULL) *list=newptr;
else
{
cptr=pptr=*list;
while(cptr!=NULL) { pptr=cptr; cptr=cptr->next; };
pptr->next = newptr;
}
}
return newptr==NULL;
}
這個函數是建立了一個新的節點,把參數str指派給新節點的string, 并把它連接配接到list所指向連結清單的結尾。另外的三個函數:new_glist(), del_glist(), add_glist()完成的功能和上述三個差不多,所不同的隻是它們所處理的資料結構不同。看完了這幾個函數,我們回到main程式。接下來是,
init_counters();
我們所閱讀的這個軟體是用來分析日志并且做出統計的,那麼這個函數的名字已經告訴了我們,這是一個初始化計數器的函數。簡略的看看吧!
$ grep init_counters *.h
webalizer.h:extern void init_counters();
在webalizer.c中找到:
void init_counters()
{
int i;
for (i=0;i
根據在最開始讀過的README檔案,這個page_type是用來定義處理的頁面的類型的。在README檔案中,
-P name Page type. This is the extension of files you consider to
be pages for Pages calculations (sometimes called 'pageviews').
The default is 'htm*' and 'cgi' (plus whatever HTMLExtension
you specified if it is different). Don't use a period!
我們在程式中也可以看到,如果沒有在指令行中或者config檔案中指定,則根據處理的日志檔案的類型來添加預設的檔案類型。比如對于CLF檔案(WWW日志),處理html, htm, cgi檔案
if (log_type == LOG_FTP)
{
ntop_entry=ntop_exit=0;
ntop_search=0;
}
else
.....
這一段是對于FTP的日志格式,設定搜尋清單。
for (i=0;i
清空哈西表,為下面即将進行的排序工作做好準備。關于哈西表,這是資料結構中常用的一種用來快速排序的結構,如果不清楚,可以參考相關書籍,比如清華的<<資料結構>>教材或者<<資料結構的C++實作>>等書。
if (verbose>1)
{
uname(&system_info);
printf("Webalizer V%s-%s (%s %s) %s ",
version,editlvl,system_info.sysname,
system_info.release,language);
}
這一段,是列印有關系統的資訊和webalizer程式的資訊(可以參考uname的函數說明)。
#ifndef USE_DNS
if (strstr(argv[0],"webazolver")!=0)
{
printf("DNS support not present, aborting... ");
exit(1);
}
#endif
這一段,回憶我們在看README檔案的時候,曾經提到過可以在編譯的時候設定選項開關來設定DNS支援,在源代碼中可以看到多次這樣的代碼段出現,如果不指定DNS支援,這些代碼段則會出現(ifdef)或者不出現(ifndef).下面略過這些代碼段,不再重複。
if (gz_log)
{
gzlog_fp = gzopen(log_fname,"rb");
if (gzlog_fp==Z_NULL)
{
fprintf(stderr, "%s %s ",msg_log_err,log_fname);
exit(1);
}
}
else
{
if (log_fname)
{
log_fp = fopen(log_fname,"r");
if (log_fp==NULL)
{
fprintf(stderr, "%s %s ",msg_log_err,log_fname);
exit(1);
}
}
}
這一段,回憶在README檔案中曾經讀到過,如果log檔案是gzip壓縮格式,則用gzopen函數打開(可以猜想gz***是一套針對gzip壓縮格式的實時解壓縮函數),如果不是,則用fopen打開。
if (out_dir)
{
if (chdir(out_dir) != 0)
{
fprintf(stderr, "%s %s ",msg_dir_err,out_dir);
exit(1);
}
}
同樣,回憶在README檔案中讀到過,如果參數行有-o out_dir, 則将輸出結果到該目錄,否則,則輸出到目前目錄。在這一段中,如果輸出目錄不存在(chdir(out_dir) != 0)則出錯。
#ifdef USE_DNS
if (strstr(argv[0],"webazolver")!=0)
{
if (!dns_children) dns_children=5;
if (!dns_cache)
{
fprintf(stderr,"%s ",msg_dns_nocf);
exit(1);
}
}
......
在上面曾經提到過,這是DNS解析的代碼部分,可以略過不看,不會影響對整個程式的了解。
if (!hname)
{
if (uname(&system_info)) hname="localhost";
else hname=system_info.nodename;
}
這一段繼續處理參數做準備工作。如果在指令行中指定了hostname(機器名)則采用指定的名稱,否則調用uname查找機器名,如果沒有,則用localhost來作為機器名。(同樣在README中說得很詳細)
if (ignore_hist) {if (verbose>1) printf("%s ",msg_ign_hist); }
else get_history();
如果在指令行中指定了忽略曆史檔案,則不讀取曆史檔案,否則調用get_history()來讀取曆史資料。在這裡,我們可以回想在README檔案中同樣說過這一細節,在指令行或者配置檔案中都能指定這一開關。需要說明的是,我們在這裡并不一定需要去看get_history這一函數,因為從函數的名稱,README檔案和程式注釋都能很清楚的得知這一函數的功能,不一定要去看代碼。而如果要猜想的話,也可以想到,history是webalizer在上次運作的時候記錄下來的一個檔案,而這個檔案則是去讀取它,并将它的資料包括到這次的分析中去。不信,我們可以來看看。
void get_history()
{
int i,numfields;
FILE *hist_fp;
char buffer[BUFSIZE];
for (i=0;i<12;i++)
{
hist_month=hist_year=hist_fday=hist_lday=0;
hist_hit=hist_files=hist_site=hist_page=hist_visit=0;
hist_xfer=0.0;
}
hist_fp=fopen(hist_fname,"r");
if (hist_fp)
{
if (verbose>1) printf("%s %s ",msg_get_hist,hist_fname);
while ((fgets(buffer,BUFSIZE,hist_fp)) != NULL)
{
i = atoi(buffer) -1;
if (i>11)
{
if (verbose)
fprintf(stderr,"%s (mth=%d) ",msg_bad_hist,i+1);
continue;
}
numfields = sscanf(buffer,"%d %d %lu %lu %lu %lf %d %d %lu %lu",
&hist_month,
&hist_year,
&hist_hit,
&hist_files,
&hist_site,
&hist_xfer,
&hist_fday,
&hist_lday,
&hist_page,
&hist_visit);
if (numfields==8)
{
hist_page = 0;
hist_visit = 0;
}
}
fclose(hist_fp);
}
else if (verbose>1) printf("%s ",msg_no_hist);
}
void put_history()
{
int i;
FILE *hist_fp;
hist_fp = fopen(hist_fname,"w");
if (hist_fp)
{
if (verbose>1) printf("%s ",msg_put_hist);
for (i=0;i<12;i++)
{
if ((hist_month != 0) && (hist_hit != 0))
{
fprintf(hist_fp,"%d %d %lu %lu %lu %.0f %d %d %lu %lu ",
hist_month,
hist_year,
hist_hit,
hist_files,
hist_site,
hist_xfer,
hist_fday,
hist_lday,
hist_page,
hist_visit);
}
}
fclose(hist_fp);
}
else
if (verbose)
fprintf(stderr,"%s %s ",msg_hist_err,hist_fname);
}
在preserve.c中,這兩個函數是成對出現的。get_history()讀取檔案中的資料,并将其記錄到hist_開頭的一些數組中去。而put_history()則是将一些資料記錄到同樣的數組中去。我們可以推測得知,hist_數組是全局變量(在函數中沒有定義),也可以查找源代碼驗證。同樣,我們可以找一找put_history()出現的地方,來驗證剛才的推測是否正确。在webalizer.c的1311行,出現:
month_update_exit(rec_tstamp);
write_month_html();
write_main_index();
put_history();
可以知道,推測是正确的。再往下讀代碼,
if (incremental)
{
if ((i=restore_state()))
{
fprintf(stderr,"%s (%d) ",msg_bad_data,i);
exit(1);
}
......
}
同樣,這也是處理指令行和做資料準備,而且和get_history(), put_history()有些類似,讀者可以自己練習一下。下面,終于進入了程式的主體部分, 在做完了指令行分析,資料準備之後,開始從日志檔案中讀取資料并做分析了。
while ( (gz_log)?(our_gzgets(gzlog_fp,buffer,BUFSIZE) != Z_NULL):
(fgets(buffer,BUFSIZE,log_fname?log_fp:stdin) != NULL))
我看到這裡的時候,頗有一些不同意作者的這種寫法。這一段while中的部分寫的比較複雜而且效率不高。因為從程式推斷和從他的代碼看來,作者是想根據日志檔案的類型不同來采用不同的方法讀取檔案,如果是gzip格式,則用our_gzgets來讀取其中一行,如果是普通的文本檔案格式,則用fgets()來讀取。但是,這段代碼是寫在while循環中的,每次讀取一行就要重複判斷一次,明顯是多餘的而且降低了程式的性能。可以在while循環之前做一次這樣的判斷,然後就不用重複了。
total_rec++;
if (strlen(buffer) == (BUFSIZE-1))
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_rec);
if (debug_mode) fprintf(stderr,": %s",buffer);
else fprintf(stderr," ");
}
total_bad++;
while ( (gz_log)?(our_gzgets(gzlog_fp,buffer,BUFSIZE)!=Z_NULL):
(fgets(buffer,BUFSIZE,log_fname?log_fp:stdin)!=NULL))
{
if (strlen(buffer) < BUFSIZE-1)
{
if (debug_mode && verbose) fprintf(stderr,"%s ",buffer);
break;
}
if (debug_mode && verbose) fprintf(stderr,"%s",buffer);
}
continue;
}
這一段代碼,讀入一行,如果這一行超過了程式允許的最大字元數(則是錯誤的日志資料紀錄),則跳過本行剩下的資料,忽略掉(continue進行下一次循環)。同時把total_bad增加一個。如果沒有超過程式允許的最大字元數(則是正确的日志資料紀錄),則
strcpy(tmp_buf, buffer);
if (parse_record(buffer))
将該資料拷貝到一個緩沖區中,然後調用parse_record()進行處理。我們可以同樣的推測一下,get_record()是這個程式的一個主要處理部分,分析了日志資料。在parse_record.c中,有此函數,
int parse_record(char *buffer)
{
memset(&log_rec,0,sizeof(struct log_struct));
#ifdef USE_DNS
memset(&log_rec.addr,0,sizeof(struct in_addr));
#endif
switch (log_type)
{
default:
case LOG_CLF: return parse_record_web(buffer); break;
case LOG_FTP: return parse_record_ftp(buffer); break;
case LOG_SQUID: return parse_record_squid(buffer); break;
}
}
可以看到,log_rec是一個全局變量,該函數根據日志檔案的類型,分别調用三種不同的分析函數。在webalizer.h中,找到該變量的定義,從結構定義中可以看到,結構定義了一個日志檔案所可能包含的所有資訊(參考CLF,FTP, SQUID日志檔案的格式說明)。
struct log_struct { char hostname[MAXHOST];
char datetime[29];
char url[MAXURL];
int resp_code;
u_long xfer_size;
#ifdef USE_DNS
struct in_addr addr;
#endif
char refer[MAXREF];
char agent[MAXAGENT];
char srchstr[MAXSRCH];
char ident[MAXIDENT]; };
extern struct log_struct log_rec;
先看一下一個parser.c用的内部函數,然後再來以parse_record_web()為例子看看這個函數是怎麼工作的,parse_record_ftp, parse_record_squid留給讀者自己分析作為練習。
void fmt_logrec(char *buffer)
{
char *cp=buffer;
int q=0,b=0,p=0;
while (*cp != '')
{
switch (*cp)
{
case ' ': if (b || q || p) break; *cp=''; break;
case '"': q^=1; break;
case '[': if (q) break; b++; break;
case ']': if (q) break; if (b>0) b--; break;
case '(': if (q) break; p++; break;
case ')': if (q) break; if (p>0) p--; break;
}
cp++;
}
}
從parser.h頭檔案中就可以看到,這個函數是一個内部函數,這個函數把一行字元串中間的空格字元用''字元(結束字元)來代替,同時考慮了不替換在雙引号,方括号,圓括号中間的空格字元以免得将一行資料錯誤的分隔開了。(請參考WEB日志的檔案格式,可以更清楚的了解這一函數)
int parse_record_web(char *buffer)
{
int size;
char *cp1, *cp2, *cpx, *eob, *eos;
size = strlen(buffer);
eob = buffer+size;
fmt_logrec(buffer);
cp1 = cpx = buffer; cp2=log_rec.hostname;
eos = (cp1+MAXHOST)-1;
if (eos >= eob) eos=eob-1;
while ( (*cp1 != '') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_host);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
while ( (*cp1 != '') && (cp1 < eob) ) cp1++;
if (cp1 < eob) cp1++;
cpx = cp1;
cp2 = log_rec.ident;
eos = (cp1+MAXIDENT-1);
if (eos >= eob) eos=eob-1;
while ( (*cp1 != '[') && (cp1 < eos) )
{
if (*cp1=='') *cp1=' ';
*cp2++=*cp1++;
}
*cp2--='';
if (cp1 >= eob) return 0;
if (*cp1 != '[')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_user);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while ( (*cp1 != '[') && (cp1 < eob) ) cp1++;
}
while (*cp2==' ') *cp2--='';
cpx = cp1;
cp2 = log_rec.datetime;
eos = (cp1+28);
if (eos >= eob) eos=eob-1;
while ( (*cp1 != '') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_date);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
if ( (log_rec.datetime[0] != '[') ||
(log_rec.datetime[3] != '/') ||
(cp1 >= eob)) return 0;
cpx = cp1;
cp2 = log_rec.url;
eos = (cp1+MAXURL-1);
if (eos >= eob) eos = eob-1;
while ( (*cp1 != '') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_req);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
if ( (log_rec.url[0] != '"') ||
(cp1 >= eob) ) return 0;
log_rec.resp_code = atoi(cp1);
while ( (*cp1 != '') && (cp1 < eob) ) cp1++;
if (cp1 < eob) cp1++;
if (*cp1<'0'||*cp1>'9') log_rec.xfer_size=0;
else log_rec.xfer_size = strtoul(cp1,NULL,10);
if (cp1>=eob) return 1;
while ( (*cp1 != '') && (*cp1 != ' ') && (cp1 < eob) )
cp1++;
if (cp1 < eob) cp1++;
cpx = cp1;
cp2 = log_rec.refer;
eos = (cp1+MAXREF-1);
if (eos >= eob) eos = eob-1;
while ( (*cp1 != '') && (*cp1 != ' ') && (cp1 != eos) )
*cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_ref);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
cpx = cp1;
cp2 = log_rec.agent;
eos = cp1+(MAXAGENT-1);
if (eos >= eob) eos = eob-1;
while ( (*cp1 != '') && (cp1 != eos) )
*cp2++ = *cp1++;
*cp2 = '';
return 1;
}
該函數,一次讀入一行(其實是一段日志資料中間的一個域,因為該行資料已經被fmt_logrec分開成多行資料了。根據CLF中的定義,檢查該資料并将其拷貝到log_rec結構中去,如果檢查該資料有效,則傳回1。回到主程式,
for (i=4;i<7;i++)
log_rec.datetime=tolower(log_rec.datetime);
for (i=0;i<12;i++)
{
if (strncmp(log_month,&log_rec.datetime[4],3)==0)
{ rec_month = i+1; break; }
}
rec_year=atoi(&log_rec.datetime[8]);
rec_day =atoi(&log_rec.datetime[1]);
rec_hour=atoi(&log_rec.datetime[13]);
rec_min =atoi(&log_rec.datetime[16]);
rec_sec =atoi(&log_rec.datetime[19]);
....
在parse_record分析完資料之後,做日期的分析,把日志中的月份等資料轉換成機器可讀(可了解)的資料,并存入到log_rec中去。
if ((i>=12)||(rec_min>59)||(rec_sec>59)||(rec_year<1990))
{
total_bad++;
if (verbose)
{
fprintf(stderr,"%s: %s [%lu]",
msg_bad_date,log_rec.datetime,total_rec);
......
如果日期,時間錯誤,則把total_bad計數器增加1,并且列印錯誤資訊到标準錯誤輸出。
good_rec = 1;
req_tstamp=cur_tstamp;
rec_tstamp=((jdate(rec_day,rec_month,rec_year)-epoch)
*86400)+
(rec_hour*3600)+(rec_min*60)+rec_sec;
if (check_dup)
{
if ( rec_tstamp <= cur_tstamp )
{
total_ignore++;
continue;
}
else
{
check_dup=0;
if (cur_month != rec_month)
{
clear_month();
cur_sec = rec_sec;
cur_min = rec_min;
cur_hour = rec_hour;
cur_day = rec_day;
cur_month = rec_month;
cur_year = rec_year;
cur_tstamp= rec_tstamp;
f_day=l_day=rec_day;
}
}
}
if (rec_tstamp/3600 < cur_tstamp/3600)
{
if (!fold_seq_err && ((rec_tstamp+SLOP_VAL)
/3600
如果該日期、時間沒有錯誤,則該資料是一個好的資料,将good_record計數器加1,并且檢查時間戳,和資料是否重複資料。這裡有一個函數,jdate()在主程式一開頭我們就遇到了,當時跳了過去沒有深究,這裡留給讀者做一個練習。(提示:該函數根據一個日期産生一個字元串,這個字元串是惟一的,可以檢查時間的重複性,是一個通用函數,可以在别的程式中拿來使用)
cp1 = cp2 = log_rec.url;
if (*++cp1 == '-') { *cp2++ = '-'; *cp2 = ''; }
else
{
while ( (*cp1 != ' ') && (*cp1 != '') ) cp1++;
if (*cp1 != '')
{
while ((*cp1 == ' ') && (*cp1 != '')) cp1++;
if (( *cp1=='/') && (*(cp1+1)=='/')) cp1++;
while ((*cp1 != ' ')&&(*cp1 != '"')&&(*cp1 != ''))
*cp2++ = *cp1++;
*cp2 = '';
}
}
unescape(log_rec.url);
if ( (cp2=strstr(log_rec.url,"://")) != NULL)
{
cp1=log_rec.url;
while (cp1!=cp2)
{
if ( (*cp1>='A') && (*cp1<='Z')) *cp1 += 'a'-'A';
cp1++;
}
}
cp1 = log_rec.url;
while (*cp1 != '')
if (!isurlchar(*cp1)) { *cp1 = ''; break; }
else cp1++;
if (log_rec.url[0]=='')
{ log_rec.url[0]='/'; log_rec.url[1]=''; }
lptr=index_alias;
while (lptr!=NULL)
{
if ((cp1=strstr(log_rec.url,lptr->string))!=NULL)
{
if ((cp1==log_rec.url)||(*(cp1-1)=='/'))
{
*cp1='';
if (log_rec.url[0]=='')
{ log_rec.url[0]='/'; log_rec.url[1]=''; }
break;
}
}
lptr=lptr->next;
}
unescape(log_rec.refer);
......
這一段,做了一些URL字元串中的字元轉換工作,很長,我個人認為為了程式的子產品化,結構化和可複用性,應該将這一段代碼改為函數,避免主程式體太長,造成可讀性不強和沒有移植性,和不夠結構化。跳過這一段乏味的代碼,進入到下面一個部分---後處理。
if (gz_log) gzclose(gzlog_fp);
else if (log_fname) fclose(log_fp);
if (good_rec)
{
tm_site[cur_day-1]=dt_site;
tm_visit[cur_day-1]=tot_visit(sd_htab);
t_visit=tot_visit(sm_htab);
if (ht_hit > mh_hit) mh_hit = ht_hit;
if (total_rec > (total_ignore+total_bad))
{
if (incremental)
{
if (save_state())
{
if (verbose) fprintf(stderr,"%s ",msg_data_err);
unlink(state_fname);
}
}
month_update_exit(rec_tstamp);
write_month_html();
write_main_index();
put_history();
}
end_time = times(&mytms);
if (time_me' '(verbose>1))
{
printf("%lu %s ",total_rec, msg_records);
if (total_ignore)
{
printf("(%lu %s",total_ignore,msg_ignored);
if (total_bad) printf(", %lu %s) ",total_bad,msg_bad);
else printf(") ");
}
else if (total_bad) printf("(%lu %s) ",total_bad,msg_bad);
temp_time = (float)(end_time-start_time)/CLK_TCK;
printf("%s %.2f %s", msg_in, temp_time, msg_seconds);
if (temp_time)
i=( (int)( (float)total_rec/temp_time ) );
else i=0;
if ( (i>0) && (i<=total_rec) ) printf(", %d/sec ", i);
else printf(" ");
}
這一段,做了一些後期的處理。接下來的部分,我想在本文中略過,留給感興趣的讀者自己去做分析。原因有兩點:
1、這個程式在前面結構化比較強,而到了結構上後面有些亂,雖然代碼效率還是比較高,但是可重用性不夠強, 限于篇幅,我就不再一一解釋了。 2、前面分析程式過程中,也對後面的代碼做了一些預測和估計,也略微涉及到了後面的代碼,而且讀者可以根據上面提到的原則來自己分析代碼,也作為一個實踐吧。
最後,對于在這篇文章中提到的分析源代碼程式的一些方法做一下小結,以作為本文的結束。
分析一個源代碼,一個有效的方法是:
1、閱讀源代碼的說明文檔,比如本例中的README, 作者寫的非常的詳細,仔細讀過之後,在閱讀程式的時候往往能夠從README檔案中找到相應的說明,進而簡化了源程式的閱讀工作。
2、如果源代碼有文檔目錄,一般為doc或者docs, 最好也在閱讀源程式之前仔細閱讀,因為這些文檔同樣起了很好的說明注釋作用。
3、從makefile檔案入手,分析源代碼的層次結構,找出哪個是主程式,哪些是函數包。這對于快速把握程式結構有很大幫助。
4、從main函數入手,一步一步往下閱讀,遇到可以猜測出意思來的簡單的函數,可以跳過。但是一定要注意程式中使用的全局變量(如果是C程式),可以把關鍵的資料結構說明拷貝到一個文本編輯器中以便随時查找。
5、分析函數包(針對C程式),要注意哪些是全局函數,哪些是内部使用的函數,注意extern關鍵字。對于變量,也需要同樣注意。先分析清楚内部函數,再來分析外部函數,因為内部函數肯定是在外部函數中被調用的。
6、需要說明的是資料結構的重要性:對于一個C程式來說,所有的函數都是在操作同一些資料,而由于沒有較好的封裝性,這些資料可能出現在程式的任何地方,被任何函數修改,是以一定要注意這些資料的定義和意義,也要注意是哪些函數在對它們進行操作,做了哪些改變。
7、在閱讀程式的同時,最好能夠把程式存入到cvs之類的版本控制器中去,在需要的時候可以對源代碼做一些修改試驗,因為動手修改是比僅僅是閱讀要好得多的讀程式的方法。在你修改運作程式的時候,可以從cvs中把原來的代碼調出來與你改動的部分進行比較(diff指令), 可以看出一些源代碼的優缺點并且能夠實際的練習自己的程式設計技術。
8、閱讀程式的同時,要注意一些小工具的使用,能夠提高速度,比如vi中的查找功能,模式比對查找,做标記,還有grep,find這兩個最強大最常用的文本搜尋工具的使用。
對于一個Unix/Linux下面以指令行方式運作的程式,有這麼一些套路,大家可以在閱讀程式的時候作為參考。
1、在程式開頭,往往都是分析指令行,根據指令行參數對一些變量或者數組,或者結構指派,後面的程式就是根據這些變量來進行不同的操作。
2、分析指令行之後,進行資料準備,往往是計數器清空,結構清零等等。
3、在程式中間有一些預編譯選項,可以在makefile中找到相應部分。
4、注意程式中對于日志的處理,和調試選項打開的時候做的動作,這些對于調試程式有很大的幫助。
5、注意多線程對資料的操作。(這在本例中沒有涉及)
結束語:
當然,在這篇文章中,并沒有闡述所有的閱讀源代碼的方法和技巧,也沒有涉及任何輔助工具(除了簡單的文本編輯器),也沒有涉及面向對象程式的閱讀方法。我想把這些留到以後再做讨論。也請大家可以就這些話題展開讨論。
三、選一些比較優秀的開源産品作為源代碼閱讀對象?
優秀的産品及項目:
1、大型産品:
1.1 C/C++/Object-C:
Linux核心(C):《Linux核心代碼》《深入了解LINUX核心》 《Linux核心完全剖析》 《Linux核心源代碼情景分析(上冊)》
《萊昂氏UNIX源代碼分析》《Linux核心設計與實作》 《Linux核心設計的藝術》《自己動手寫嵌入式作業系統》
《自己動手寫作業系統》 《Orange'S:一個作業系統的實作》
TCP/IP協定棧 《TCP/IP詳解》
JVM(C/C++)、OpenJDK7or8:OpenJDK源碼閱讀導航:http://rednaxelafx.iteye.com/blog/1549577 《深入Java虛拟機(原書第2版)》
《Java Virtual Machine Specification》 《深入了解Java虛拟機》 ----14
GCC 《垃圾收集》 《An Introduction to GCC》
DB: MySQL (C/C++) MySQL JDBC Driver (mysql-connector-java-5.1.13) 《MySQL技術内幕》, PostgreSQL代碼結構清晰
Nginx 《實戰Nginx》《深入了解Nginx:子產品開發與架構解析》 --- 14
LVS:
Apache(C++) 《Apache源代碼全景分析第1卷》《Apache Server源代碼分析》
Android
1.2 Java/C#:
JDK類庫(尤其util(concurrent)包集合容器等) --- 14
Eclipse
OSGI
Servlet規範
JSP規範
Spring 《Spring揭秘》《Spring技術内幕—深入解析Spring架構與設計原理》
Struct2,JDBC連接配接池,Ibatis
Tomcat 《深入剖析Tomcat》《How Tomcat Works》
消息中間件:ActiveMQ
Hadoop 《Hadoop實戰》
1.3 Other:
PHP
Python
Linux工具集/Shell/Perl
Erlang編譯器
ruby
人機互動、函數式語言、并發語言、邏輯語言
2、中小型産品:
2.1 C/C++/Object-C:
STL 《C++标準程式庫》《标準模闆庫自修教程與參考手冊(STL進行C++程式設計第2版)》《Effective STL中文版》《STL源碼剖析》
《泛型程式設計與STL》《C++ Templates》
Boost 《超越C++标準庫:Boost庫導論》 《Boost程式庫完全開發指南》
ACE 《C++網絡程式設計(卷1)》《C++網絡程式設計(卷2)》《ACE技術内幕》 libevent
Cache : Redis、 Memcached
安全: OpenSSL
2.2 Java/C#:
Netty(I/O)
Jetty
JDBC
Hibernate
Ibatis iBATIS架構源碼剖析 《iBatis in Action》《iBATIS架構源碼剖析》
Struts(MVC): 《Struts 2實戰》
WebWork
Velocity1.6
cache:OSCache
2.3 Other:
C#/Flex用戶端
JQuery(JS) 《jQuery基礎教程》
Python 《Python源碼剖析》
3、小型産品:
3.1 C/C++/Object-C:
接入、分發、ptypes
3.2 Java/C#:
log4j
httpclient
JUnit4
MegaClient/MegaServer
資料庫連接配接池:
3.3 Other:
V8 Node
四、參考
[1] 摘自代碼閱讀方法與實踐書籍的知識點總結
[2] 如何閱讀linux源代碼
[3] 如何閱讀源代碼(1)
[4] 如何閱讀源代碼Linux開發者:http://wenku.baidu.com/view/56270184b9d528ea81c77968.html