上窮碧落下黃泉,源碼追蹤經驗談——侯捷

剛剛把開題的準備弄完了,決定好好研究一個開源引擎。看網友評價說幾大開源引擎中,Irrlicht比較小,容易入門,所以決定先研究它了。在找Irrlicht相關資料時,看到有人推薦了侯捷老師的這篇文章,覺得此文對於研究源碼甚有幫助,唯一不妥之處在於原文是繁體的,在網上也沒找到簡體的,所以自己做了一份簡體版。

侯老師真是好人啊!不僅授人以魚,而且授人以漁!如果想要PDF的,可以在此下載。暈,CSDN怎麼不搞個直接在博客中上傳文件的方法。我只能傳到CSDN下載裏了,只要有帳號就可以下載,不用積分的。(好多人在CSDN下東西總說自己分不夠,我覺得很奇怪。只要下載後評價一下,記得要評價幾顆星,不僅下載的分數會還給你,還可以加一分。但要只有文字評價,沒有打星的話,不還分的。)再補一個愛問共享的鏈接,愛問共享零分資源是可以免費下載的,不用登錄,沒有CSDN帳號的可以到此下載

侯捷老師正文:

剖析名家源碼,是讓自己技術躍升的快捷方式。但是大系統的源碼非常龐大 Unix, Linux,Java, STL, MFC, VCL, Qt...),閱讀要有閱讀的方法。本文從動機、對象、前提、書籍、態度、工具、方法、瓶頸、價值、附加價值等方向加以討論。

讀者意見

讀者對我的印象,可能很大部份是我寫了本《深入淺出 MFC》,剖析 MFC 的運作機制並簡化模擬了它們。的確,剖析與說理是我擅長的兩個項目。偶然的情況,我起了經驗傳承的念頭,想寫這篇文章,於是在侯捷網站貼出公告,藉此獲知讀者希望看到什麼。面是幾封帶有具體提議的來信(節錄)以及我的簡單回覆:

能不能寫出系列文章?我相信您有非常非常多和好的經驗(教訓-),若只有篇短文,則不解渴。

侯捷:我只打算談原則、談基本方法,這不需要系列文章。

● (1).  如何開頭?從最開始的 .h  文件讀起?(2)  怎麼做前後連接工作?原碼前後關係不少,你是怎麼做這些工作的?做堆成山的筆記嗎?(3)  如果你自己對程序所用的算法(algorithms)並不熟悉,怎麼突破這個障礙?

侯捷:筆記定要做,稍後詳述。算法若不熟悉,比較困難,唯的辦法是一一拆解,發揮想象力,設法搞懂它。搞不懂,就是遇了瓶頸。

我也對如何閱讀大系統的源碼非常感興趣,如 Linux, GCC 以及些開放源碼的系統,所以弟便抓了 RedHat 開發的可以閱讀大系統源碼的軟件叫做Source Navigator,可是弟並沒耐性看完這套軟件的 manual,抓回來之後就丟在邊,沒有時間鑽研如何用它來看大系統的源碼(準備高考當)。…希望能夠看到您出書寫到閱讀源碼相關的心得…

侯捷:有工具幫助是很好,沒工具也有沒工具的作法。

名家的代碼﹐簡練﹑優雅﹑富有彈性,對它進行剖析確實能夠增長自己的見識﹐提升自己的功力﹐對於自己的實際工作起到很大的幫助。我覺得應該不僅僅停留在對這種已經成形的設計結果(源碼本身)的讚歎﹐更應該探討這種優美設計的進化歷程﹐因爲這些優美的源碼肯定是經過很多次的 refactoring得到的。如果僅僅剖析已經存在的優美設計﹐很容易掩蓋怎樣進行設計的正確道路﹐可能造成味的“模仿”﹐過度的 up front design

侯捷:如果能夠反推回當初設計的原貌和演化歷程,當然最好。可能沒有這樣的功力或時間。我以書寫爲目的,探究技術的同時,儘可能做到這點。技術的來龍去脈,乃我所謂之技術本質。不過我的設想與實際行動和你述所言或有差異。

● D. E. knuth在《The Art of Computer Programming》提到閱讀優秀的源代碼直是被計算器科學教育所忽略的個重要方面, Richard Stevens 也在《Unixadvanced programming書封底提到,  他認爲學習程序的最好方法就是讀程序和寫程序。再至 K&R 這樣的泰斗,也都在不同方強調過閱讀程序對於學習程序設計的重要性。…我多麼希望先生能就這主題推向縱深寫出本這方面的專著來…

侯捷:我只談原則和基本方法,這不構成本書的份量。

我瀏覽過 MFC, Linux 源代碼,深感剖析名家源碼的確是使自己技術躍升的捷徑,但此間有很大的困難,名家源代碼般極其龐大,內容涉及廣,很難把握其實質。我認爲先要對所研究的源碼有個高層抽象的把握,就像軟件工程的需求分析樣,然後再逐步進入,層具體化。

侯捷:的確,定要對研究對象先有高階理解,不能蒙着頭就栽入。

● STL 大量的變量聲明﹐感覺稀奇古怪﹑密密麻麻﹐閱讀實在成問題﹐請問有什麼好的辦法呢﹖

侯捷:可能你看的是 Visual C++ STL PJ 版本,那是可讀性最差的版本。換讀 GCC附帶的 STL SGI版本會好很多。

個人經驗

寫這篇文章,我拿什麼證明我有足夠的經驗給你參考?是這樣,我個曾經深入追蹤剖析 MFC STL 源碼,並據以寫出《深入淺出 MFC》和《STL 源碼剖析》兩本書。對於 Java Qt源碼也有點涉獵。Windows 源碼雖然沒看過(除了「那個 誰看過了) 但對Windows系統內部的數據結構 (用以管理memory, processes,modules, threads…)以及 kernel APIs,倒是有幾番深刻理解(歸功於本書,稍後詳述),亦曾經深刻剖析過 MZ/NE/PE 可執行文件格式,寫過些(自用的)分析工具。

動機

任何事情都講求動機。動機強則成功率高,動機弱則失敗率高。面幾種情況是剖析源碼的可能動機:

1.  像侯捷樣以書寫、教育爲目的。剖析源碼可以帶給我許多技術養份,又補充我退離編程第線後的實戰磨練。屬於工作的部份,和興趣結合,又能養家餬口,成功率最高,文件成果最豐碩。

2.  需要對某些 open source  進行改寫以量身訂製專用版本。這種情況最常見於Linux GCC。完全是種職場工作,壓力很大,動機超強,成功率高。但有時間壓力,很難完善其說明文件;往往在項目結束後段時間內,對如此大規模源碼的精神和實際面掌握度漸次消褪,最後煙消雲散,只留下一絲絲模糊概念。

3.  嚮往華山論劍高手招式,希望學習名家風範,對技術有強烈的追求慾望。完全不是工作,只是種學習。積極進取的學生可能是這類。自由的時間加學習的賦使命,使學生時代成爲剖析名家源碼的最佳時期,但年輕時期就具備足夠基礎與心性的不很多。

4.  工作之外偶而發心    畢竟探看核心企求醍醐灌頂是每個科技蜇居的渴望。由於本職工作的壓力,這類型通常難以持久。

大系統源碼都十分龐大,值得剖析(根據我的價值判斷)的最小規模,大約是 C++Standard Library。說它最小,你不妨親自看看有多大(就在你的 C++  編譯程序的 ”INCLUDE”  目錄)。因此,沒有強烈動機和縝密而系統化的措施,很難獲得真正有用的成果。半途而廢太可惜了,所以要嘛就決心做到相當程度,有具體成果才罷手,要嘛乾脆別動,把時間拿去看恐龍展、拿破崙展、星堆文物展、羅浮名畫展,或是和異性朋友培養感情,更有價值。

對象

取得名家源碼的機會很多。open source 不必說了,網絡可以自由載;其他諸如 classes library, framework…,多半採取白盒策略,也對使用者開放源碼(這裏所說的開放源碼,和般所謂的 open source  不同。前者只是將程序以源碼型式釋出,讓使用者得以觀察研究,或修改後用於自家產品。後者允許使用者在某種授權(例如 GPL, General Public License)之做任意用途。)。許多網絡社羣組織也大方開放他們的成果。琳琅滿目的貨架,什麼纔是值得探的寶貝呢?剖析源碼,時間與精力的投注很大,如果抱持「放進籃裏都是菜」的心態,旦遇不淑損失可就大了(別說你有的是時間)。

我個認爲,只有價值被百分之百認定的大型卓越作品,才值得剖析它,從吸取深層技術養份。樣米還養百樣,哪來被百分百認定的大型卓越作品?唔,我說的是被你百分百認定,不是被百分之百的認定。至於你認定錯誤,所學非,虛擲歲月,那是你眼力差,調查不足,怨不得

前提

剖析源碼,並非學習語言的好方法    雖然你或許可以學到很好的語言運用。剖析源碼,也不是初學 OO 的好路線  雖然你或許可以學到很好的 OO 概念和實作。要知道,你現在是單騎入風塵,飄飄無所依,迎面撲來的是成千萬如蝗蟲如夏蚊的程序代碼。剖析樣東西,必須先對它有定程度的瞭解。假設你想剖析MFC,爲什麼會有這樣的念頭?因爲你想徹底瞭解並掌握 MFC 的運行,這種需求定是因爲你想以 MFC 爲基礎開發應用程序。那麼,不先寫幾個 MFC 應用程序觸發點感覺,不宜貿貿然進入叢林深處  那兒有很多沼澤和蚊蚋。

 

相同道理,閱讀 Qt 源碼之前,請先寫點 Qt 程序;閱讀 STL 源碼之前,請先學會使用 STL 並對 GP/template 有相當認識;閱讀 Java 源碼之前,請先學會撰寫 Java程序並對 OO 有相當體會;閱讀 Linux 源碼之前,請先在 Linux 系統陣子並對內存管理、分離地址空間、檔案管理、驅動程序等系統知識有點準備。閱讀任何窗口系統的任何 application framework,請先對該系統的訊息驅動機制做相當程度的瞭解。

書籍

永遠不要抱持「切從輪子造起」的想法。捨棄別的成果不用,走別走過的路,犯別犯過的錯,智者不爲。

名家源碼剖析心得這類書籍,屬於小衆市場,得遇本應該感。你要的書到底存不存在,自己得仔細做點功課。當然,書寫得好不好,也得你自己仔細做點功課。www.amazon.com 是最好的書籍搜尋網站,打幾個關鍵詞進去,用心鏈結瀏覽一下,花不了個半MFC 方面,《MFC Internals》、《深入淺出 MFC》都是首選,STL有《STL源碼剖析》、《The C++ Standard Template Library》,Linux方面可多了,蔚爲大觀,絕對不愁找不到。想對 Windows 操作系統有深刻認識,應該看《Undocumented Windows》、《Windows Internals》、《Windows 95 SystemProgramming SECRETs》(先前我曾說,我個 Windows 操作系統的內部結構有相當瞭解,便是得力於這本書。非常非常棒的本書  即使它們的出版年份分別是 1992,1993,1995,即使今Windows 操作系統已是 XP 當道。),印象還有本教你動手實現 Win32 操作系統的書 。想看GCC源碼, 應該先拿編譯程序原理墊墊底 再找本 Building Your Own Compilerwith C++ Crafting a Compiler with C ,能夠看看 How Debuggers Work Linkers& Loaders》當然更好。

態度

真的,剖析源碼是件大而艱鉅的工程。心理素質不好的,不要嘗試。想象這樣的情境:「我走在廣袤的熱帶雨林中,濃密的樹冠連一絲陽光也透不進來。到處是黝黑的沼澤;蛇虺魍魎,蠱毒瘴癘。撲面而來盡是蚊蚋,羣響如雷。碩大的蒼蠅毫無畏懼地在我臉上停留、舔舐我的臉孔並清理它們的腿毛。我想找一隻魔戒,傳說中載上了它就擁有超人一等的力量,足以懾服衆生。但我不知道它在哪裏,連它的長像都不知道。每個疲憊不堪的夜晚,我夢見墜入暗無天日的泥淖,手忙腳亂地尋找一隻針。呃,是的,一隻繡花針。極度疲倦中我入睡,極度無依中我醒來。日復一日。前面有三百六十五里路。每天都像行程伊始。聽說森林裏到處都是像我一樣的人…的骨骸。」

語出何處?哦,是我的即興之作,博君哂。沒有堅強信念,你走不出黑色森林。沒有適當的工具和方法,你也別想大海撈針。

工具

面是我用過的工具。由於我的最多經驗都在 MS Windows 環境,剖析對象也都是 Windows 環境的大型源碼,所以我所列的工具也就有某種侷限。然而任何應該能夠從這裏面得到些靈感。

1. grep

剖析 MFC STL 源碼時,除了般文本編輯器(我用老古董 PE2),我只使用個工具:grep,這是源自 UNIX 個小小公用程序(utility),可以在大堆檔案找出某個字符串的出現點。例如,我知道,任何 Windows 程序不可能沒有WinMain(),而 MFC 應用程序沒有它的蹤影,因此我判斷定被 MFC 包裝起來了。於是我想在茫茫大海尋找  WinMain 落於何處,如圖 1。畫面第行顯示我的動作是:

grep WinMain *.cpp           

grep 爲我找出 6個出現有 WinMain 字樣的檔案,並列出每個出現點的整行文字。從我篩選出 APPMODUL.CPP WINMAIN.CPP 做爲下一個觀察目標。這樣我便有了很好的線索。如果希望搜尋目標擴及子目錄,grep 也辦得到,如圖 2,採用選項  -d”。

我手這個 grep.exe Borland 編譯程序提供的版本。如果你沒有,可以到http://unxutils.sourceforge.net/  個同類工具(感謝 william告訴我)。

1. grep 搜尋特定字符串

 

2. 我手 grep 的全部功能選項。這是 Borland 編譯程序提供的版本。

2. windiff

拿到 MFC7 源碼的那一天,我便立刻以 windiff 觀察兩個版本的差異。Windiff VC 內附的小工具,方便觀察兩個檔案的差異,包括新增內容、刪減內容、修改內容等等,如圖 3。被觀察的兩個檔案內容置於同個大窗口,共同內容以白底黑字表現,紅色區域爲第檔案之獨特內容,黃色區域爲第檔案之獨特內容。左邊小窗口列出兩個檔案內容相異區域的映像圖,方便你掌握全局;藍點表示目前大窗口所觀察的區域在整個檔案的座落位置。其他標示及功能此處就不介紹了。利用這個工具,我輕易實證先前聽說的「MFC7 加強 Type-safe Message Maps」的實際作法:以相對安全的 static_cast<> 取代霸道的 C-style 強制轉型(如圖 3所示)。

 

3.  windiff可以讓使用者很方便觀察兩個檔案之間的差異。

3. IDE debugger

做爲技術文本書寫者,我向不喜歡使用比讀者先進太多的工具。我喜歡鶴嘴鋤、字鎬、畚箕扁擔,因爲我的讀者可能買不起昂貴的空壓機、怪手、樓蘭大吊車。如果我告訴讀者我以 BoundsChecker SoftICE 觀察到某處有塊內存泄漏,某處造成緩存器內容詭異,而我的讀者只能看着述兩個高貴的名稱乾瞪眼,這有什麼意思?

不過,研究 MFC 而計算機內沒有安裝 VC++,實在是怪事樁。VC++ 內建有除錯器,不好好利用就未免暴殄物。剖析各種大型 libraries,你應該善用各種整合開發環境(IDE的除錯器,善用其 Breakpoint, Step Into, Step Over, Step Out功能、善用其 CallStack 窗口和各種 Debug 窗口,如圖 4。這些功能讓你得以把程序的執行凍結放慢到個指令,並在任何時刻觀察任何變量的現值及函數的呼叫順序。所謂「玩弄於股掌之間」差不多也就這樣子了。這些功能非 VC 獨有,每種編譯程序的專業除錯器都有。

我模擬 MFC 做出 MFCLite3,撰寫過程曾經遇到極隱微的臭蟲,如果不是除錯器的幫忙,協助我瞭解 MFCLite3 MFC 之間的差異,單隻使用鶴嘴鋤字鎬和畚箕扁擔,我不敢想象需要花費多少額外的時間和精力。

 

4.  VC 除錯器。左窗口可觀察 classes, files, resources,右窗口可觀察源碼,右角是 CallStack 窗口,左角是 Watch 窗口(可觀察任何個你設定的變量),方正央是目前執行脈絡的局部變量窗口。將遊標移至源碼窗口內的任何變量名稱,其身旁便會出現現值,以小黃標籤框住。紅點表示斷點,執行至斷點後可選擇單步前進、進入函數、退出函數…等執行方式。每個窗口都可以隨意擺放,所以你的 VC 畫面可能和本圖不盡相同。所有這些功能並非 VC 獨有,每種專業除錯器都有這些功能。

4. Spy++

觀察任何 Windows libraries,少不了需要 SPY++  的幫忙。舉個例子,當我撰寫MFCLite3 窗口/文件關閉系統時,我從 MFC 源碼觀察到它處理了 WM_CLOSE WM_DESTROY WM_NCDESTROY,而我的 SDK 知識告訴我程序結束時還會發出WM_QUITMFC 的某些處理方式(例如 MDI 窗口管理和  ::PostMessage()同步行爲)大大超出了 MFCLite3 的設定目標,因此我必須做些簡化,繞個彎在儘量逼真的前提模擬 MFC 行爲。首先我得確定窗口/文件關閉系統的所有相關訊息,這時候就用 SPY++

SPY++ VC內附的個工具,可以偵測種東西:(1) Messages, (2) Windows, (3)Processes, (4) Threads,執行畫面如圖 5

 

5. SPY++  執行畫面。個子窗口分別展現 SPY++  所能偵測的種東西:(1)Messages, (2) Windows, (3) Processes, (4) Threads

5. TDump

TDump Borland 編譯程序內附的個工具,可用來觀察 MZ(DOS), NE(Win16),LE(VxD), PE(Win32), OMF(.OBJ & .LIB)  等文件格式,如圖 6。我在剖析 Windows可執行文件格式時,曾大量倚重它來比對《Windows 95 System Programming SECRETs》第 8 章所示的數據結構。同類工具還有 VC 所附的 DUMPBIN.EXE Matt Pietrek所寫的 PEDUMP.EXE

TDUMP 另有述其他工具沒有的特性:可以傾印(dump進位檔內容。

追蹤 MFC 源碼和撰寫 MFCLite3 時,我拿它來觀察文件格式,如圖 7

 

6  TDUMP 觀察 Win32 程序(PE格式)

 

7.  TDUMP 傾印(dump進位檔案內容。

6. Source Navigator

本文開始,讀者來函曾經提到 Source Navigator。我沒有用過這個工具,所以載了份試試。這個工具相當龐大,我還沒有足夠的動機去研究它。不過從其執行畫面(圖 8)及菜單單觀之,大概是用來追蹤分析 C++ class library。如果真是這樣,我想編譯程序所附的除錯器可以完全取代之。

 

8.  Source Navigator的執行畫面。

方法

萬事俱備,東風也有了,動手吧。首先你應該認識剖析對象的檔案組態。

檔案組態

到底你觀察的對象有哪些檔案,置於何處,首先你要掌握好。Java 程序源碼只有 .java 種型態,C++ 程序源碼有實作檔(通常擴展名爲 .CPP)和表頭檔(通常擴展名爲 .H,或者無擴展名)兩種,分放不同的磁盤目錄內。

不同的系統,對於檔案命名肯定都有某種規則。閱讀源碼的過程,對此必須留心記記。以 MFC 爲例,所有主軸核心類別的宣告放在 afxwin.h,訊息映像過程所需的訊息處理宏定義於 afxmsg_.hxxxCore.cpp 內含類別核心定義,例如appcore.cpp CWinApp , wincore.cpp CWnd , doccore.cpp CDocument , viewcore.cppCView Winxxx.cpp代表 CWnd 衍生類別的定義,例如  winfrm.cpp CFrameWnd ,winmdi.cppCMDIFrameWnd,CMDIChildWnd)…。DocXxx.cpp 表示 document 相關類別,例如 DocTempl.cppCDocTemplate,  DocMgr.cppCDocManager,DocSingl.cppCSingleDocTemplate,  DocMulti.cppCMultiDocTemplate)。這些檔名或許回生回熟,但最好你能夠做做筆記,用點心思強記來。掌握檔案的命名哲學,對你順利追蹤源碼很有幫助。

閱讀源碼的過程會涌現大量的變量名稱、函數名稱。它們也都有某種命名規則。這個也必須用點心思歸類整理記錄來。例如 MFC classes 的成員函數 On 開頭、Do 開頭、Pre 開頭、Post 開頭、Get 開頭、Set 開頭、Open 開頭、Load 開頭、Create 開頭…等各種名稱,掌握它們的命名規則能使你閱讀時印象加深,事半功倍。

爲了熟悉檔案組態,也爲了方便觀察,請熟用任何個令你舒服的文本文件快速瀏覽工具。圖 9 是我慣使的 FileCtrl.com個老掉牙的 DOS 小程序(你看它還是 .COM 呢) ,在 MS-DOS 窗口跑得很好。只要以光棒選擇左窗口的文件名,右窗口立刻顯現內容,反應極快,操作簡單,執行檔小到不行,才 8,466 bytes。把它放在 PATH 所指目錄,便可以在任何時刻任何點調用它。

 

 

9.  FileCtrl個小工具,可方便而快速觀察檔案內容。

線頭

剖析源碼,像玩拼圖遊戲。你定先拼個角落,是吧。線頭找到,抽絲剝繭就很容易。線頭在哪裏?考驗你的基礎知識。例如先前我所說,Windows 程序必定以 WinMain()爲程序進入點(entry point),當我找到其源碼所在,循序追蹤,至少就可以挖掘出application framework條重要主幹 (其他諸如msg mapping, msgrouting, document/view…還得另起爐竈)。再以 MFC msg mapping 爲例,我從來不曾在其他C++ 程序看過DECLARE_MESSAGE_MAP()BEGIN_MESSAGE_MAP(),END_MESSAGE_MAP()  這種東西,用 grep 工具找,找出其源碼,發現都是 macros,於是我就老老實實把這些 macros 的定義代入隨便個測試用的 class 內外(或利用 VC 編譯程序選項  FI 直接取得代入結果),老老實實觀察程序代碼的變化,再把這些 macros 所構築出來的數據結構畫出,輕易就破解了法老王密碼。雖然隱微的AfxSig_xxx 還有待理解(從歷史看,羅塞達石碑也是很晚才發現-),但我已能大略掌握整個設計精神。MFC 層基礎設施(Dynamic/DynCreate/Serial)也是這樣破解的。這些方法笨嗎?我做這些事情的時候(1994),世沒有任何本書篇文章能夠引導我,我的辦法是唯的辦法,不笨。

諸如 MFC 這樣的框架系統,組織龐大線頭紛歧。STL 就單純許多。STL 有六大組件,你可以任選種開始。應該會從容器開始,尤其是最簡單的 vector,畢竟它只是動態 array,而 array 是大家耳熟能詳的結構。但是當你進入 vector 的源碼看,乖乖,內存動態配置是以 STL allocator爲之,與 STL algorithm之間的橋樑則是透過 STL iterators。這時候你可以選擇先跳開研究後兩者,或是抱持「反正是那麼種東西,有那麼種功能」的心情,先解決 vector 再說。在此我可以告訴各位,破解 STL 實作奧祕的最大關鍵在 iterator traits ,因爲它不但觀念新穎, 實作手法也新穎 (對大部份 C++  程序員而言) 關鍵是 function adaptors,同樣因爲觀念新穎,實作手法新穎。至於 containers algorithms,教科書都找得到它們的詳盡說明,狠狠給它流點汗,不可能沒有收穫。

面對操作系統,線頭又在哪裏?經驗告訴我應該在數據結構;可執行文件格式尤其關鍵。連 Windows 動態聯結的奧祕都藏在可執行文件格式呢(見 PE 格式  .edata   .idata 兩個 sections;’e 代表 export,’i 代表 import)。如果你手有源碼,那麼系統的數據結構的呈現很具體,明明白白就寫在表頭檔內;(廣義的)算法比較沒那麼實象,萬缺乏良好批註,你只得步追蹤推演。然而,(廣義的)算法離不開數據結構,掌握了數據結構,你就有所依恃。稍後「瓶頸」段我另有說明。

除了以所說,另有些難以言傳的東西,答的方式或許更能傳承。面對陌生架構,不同的有不同的組織手法和觀察焦點,開始跌跌撞撞都是難免。大勢逐漸明朗後,兩岸猿聲啼不住,輕舟已過萬重山。

筆記

大系統源碼都很龐大很複雜。如果你以爲你可以像看電視連續劇邊啜咖啡邊搖頭晃腦輕鬆自在看看,旁音樂零嘴侍候,時而還要應付小傢伙的搗蛋或大傢伙的嘮叨,還可以用做點旁務,我告訴你,別作夢了。面對這大坨代碼,不論時刻長短,你必須戰戰兢兢心無旁騖,像準備大學聯考樣專心;靈光乍現、心得偶拾之際,立刻做筆記。

筆記做在計算機最好。

請熟用個文字輸入工具,個繪圖工具。請加快你的打字速度。多快?不會影響你的書寫速度就行。圖 10 是我追蹤 MFC 窗口/文件關閉系統的過程,以PowerPoint 的圖,這樣的圖我在追蹤過程產出不數百張。圖 11 是我追蹤STL RB-tree(紅黑樹)的過程,以 PowerPoint  的圖,這樣的圖也不百張。有了它們,配合少量文字,我可以自信滿滿說,20 年內,任何時候你問我關於這個系統,我可以複習個小時後便回答得頭頭是道。超過 20 年我滿 60 歲,萬得阿茲海默症(老癡呆啦)可就抱歉啦。

分析與記錄方式要規範  OO 這東西老實說複雜得很。CASE tool(如 RationalRose)很昂貴,我不能冀望買了「保時捷」才路,但至少 UMLUnified ModelingLanguage)的各種 ”diagrams” 要啃啃,要學着用用。圖 12 是我剖析 MFC 及撰寫 MFCLite3 的過程,手工繪製的 UML class diagram。嗯,富有富的辦法,窮有窮的對策。

我的成果可用於書寫與出版,當然我寫字畫圖起來就格外帶勁兒。你的情況不同,不必像我樣畫得那麼精美漂亮。我要強調的是,你得勤做筆記,份量要足,不能偷懶。曾經走過的路,再走遍真令不耐,曾經理解的知識,重新推演遍真令懊惱。

 

10. 追蹤 MFC 的窗口-文件關閉系統時,我以 PowerPoint 的圖。

 

11. 追蹤 STL RB-tree(紅黑樹)源碼時,我以 PowerPoint 的圖。

 

12. 追蹤MFC及撰寫MFCLite3 ,我以PowerPoint  UML class diagram

Design Patterns

先有雞還是先有蛋?,請回答。

答不出來是吧。

先有 design還是先有 design patters

在自然演化的世界,當然是先有 design 纔有 patterns。後者是前者的淬鍊與分類。但我們希望予程序員以訓練,讓他們在還未能完成那麼多設計之前,先獲得前的加持灌頂。如果他們心有了 design patterns,他們就可以在適當時機運用前的經驗完成最理想(或足夠理想)的設計。

源碼追蹤和 design patterns 關係幾何?是否定先要熟透那些名聞遐邇的 designpatterns,追蹤與剖析纔有依據?不,具備 design patterns 知識,你在分析源碼時感觸會更敏銳,文字說明或總結時可以更言簡意賅,但即使不知道 design patterns 也不會影響你的追蹤與學習。《深入淺出 MFC2e,p82(這裏說的是繁體版頁次。簡體版出現於 p68 央和 p69 。)最後行說:『我要在這裏說明虛擬函數另個極重要的行爲模式』,p84 段第行說:『這種行爲模式非常頻繁出現在 application framework 』。1996 年我寫下上述文字時,並不知道它就是如今大名鼎鼎的 Template Method(詳見《Design Patternsby Gamma, etc. 1995, Addison Wesley。”Define the skeleton of analgorithm in an operation, deferring some steps to subclasses. Template Method letssubclasses redefine certain steps of an algorithms without changing the algorithm’sstructure.”)。但這不影響我的認識和我的體會。當然,如果當初我就讀過 GOF的名著,可能對我的剖析和書寫更有幫助。

瓶頸

當你的知識水平和你所閱讀的對象差距太遠,你也只好暫時放,補齊必要的基礎。舉個例子,當你追蹤 STL allocator,研究它的內存配置策略時,如果不知道什麼是 memory pool,源碼又無法讓你參悟,你只好先去了解 memory pool 是何方神聖。如果你不知道什麼是 Red Black tree,你也絕不可能剖析 STL map set兩種容器,因爲它們的底層機制都是 Red Black tree。不瞭解 Hash table?先去看看數據結構教科書;不解 QuickSort Insertion Sort?先去看看算法教科書。

沒有哪份名家源碼是易與之輩。它們都是大系統,包羅萬象。面對操作系統源碼或編譯程序源碼,需要的基礎知識就更多更底層更艱澀了。斷、迂迴、定點攻堅是你常遇到的情況和必要措施,頹喪和興奮是你情緒輪迴。每項知識都有其基礎知識,每項基礎知識又有其更基礎知識。地中斷、轉換、挫折,難以行雲流水,大概是源碼追蹤工程的最大失敗潛因。

先前讀者來信問到,如果對程序所用的算法不熟悉,怎麼突破障礙?我必得告訴你,你只好以修復古蹟的態度,瓦重建整個脈絡。然而經驗告訴我,演算法和數據結構脫不了干係,把數據結構摸清楚,再耐心步進追蹤,終有水落石出的一天。剖析 STL deque 時我有類似經驗。我對 deque 實作技術的唯理解是,個分段連續空間。Deque 的實作碼相較於其他序列式容器如 vector, list 龐大很多,但當我耐心 class deque 的所有 data member畫出來,如圖 13,再實際放些元素進去(特別注意邊界狀態),我就可以輕鬆觀察數據結構的內容變化。有了這些認識,再搭配 deque member functions 源碼,疑惑迎刃而解。

14是我的另個經驗。我從 SGI STL allocator的源碼變量名稱隱約知道它實作有 memory pool,同樣我把數據結構畫出來,塞幾個元素進去,觀察內容的變化和指標的移動,疑惑迎刃而解。

 

13. SGI STL deque 的實作手法。

 

14.  SGI STL allocator所實作的16memory pool 分別應付8, 16, 24, 32, 128

bytes 的小塊內存索求。

當然也有些情況非常複雜,不那麼容易對付。奉勸句,不要硬鑽牛角尖!就算不是牛角尖,也不能硬鑽。不懂還是不懂,硬鑽也是不懂,那就放吧(還能怎樣)。幸運的話,在偶然的時機裏,也許貴相助,也許心有靈犀,也許觸類旁通,你就手到擒來得之不費功夫了。

我有個切身實例。1997年我完成《深入淺出 MFC》,其第八章剖析 document檔案結構,當時我已經搞清楚 Serialization 的來龍去脈,也可以解釋許多 document進位內容,但對於爲什麼有些 tag 8001,有些 tag 8003,我不瞭解。當時我認爲我已經達到了我設定的目標,對於更進步剖析已無興趣(沒興趣和遇不易突破的障礙多少有點因果循環),而且我認爲《深入淺出 MFC》的讀者最終目標是要撰寫 MFC 應用程序,未能把屬於極內部機制的 tag 編碼(encode)方式搞清楚,無關宏旨甚至連理解 document 存檔格式在我認爲都已是「機了」。

晃就是五年,直到最近我開始撰寫《多型與虛擬》2e 第六章的 MFCLite3  個模擬 MFC  的輕量級文本模式 application framework。由於我對述主題的認識只及某個層次,與真正的 MFC 還有段距離,造成 MFCLite3 在某種情況出錯。甫自浙江大學電子系畢業的肖翔先生來信給了我份錯誤報告(全文見侯捷網站「汗如雨」),以爲來函摘要:

psqr1 psqr2 指向同對象﹐寫入文件時應該只有份﹐但是在您的實現卻寫了兩次﹗導致讀出時﹐psqr1 psqr2 指向了不同的對象。顯然這是不正確的。我覺得對於 C++  對象持久性而言﹐最重要的問題﹕個是如何保存相關的類信息﹐另個就是如何解決述問題﹗在您的兩本着作《多形與虛擬》﹑《深入淺出 MFC對前者都有很精闢的論述﹐唯獨後者點也沒有提及﹐不能不說是個很大的瑕疵。對於如何解決這個問題也不是很困難﹐只要先實現 CMapPtrToPtr CPtrArray﹐在寫入時先查 map 如果已寫過﹐就只把輸出序號寫入文件﹐如果沒有就把對象的址和輸出序號插入 map﹐再把數據寫入文件。讀出時﹐遇到第種情況(即文件有實際數據,而非只是序號)﹐就先創建個對象把數據讀出﹐接着再把新建對象的址加到數組 array 尾端﹐遇到第種情況﹐就以輸出序號爲索引直接從數組得到對象(由於寫入和讀出的順序樣﹐僅用輸出序號就可以完全解決問題)。

 

※原函之大陸術語,對臺灣讀者十分陌生。以修改爲臺灣術語以利臺灣讀者閱讀。謹此。

psqr1 psqr2 指向同對象﹐寫入文件時應該只有份﹐但是在您的實作卻寫了兩次﹗導致讀出時﹐psqr1 psqr2 指向了不同的物件。顯然這是不正確的。我覺得對於 C++ 對象永續性而言﹐最重要的問題﹕個是如何保存相關的類別資訊﹐另個就是如何解決述問題﹗在您的兩本着作《多形與虛擬》﹑《深入淺出 MFC對前者都有很精闢的論述﹐唯獨後者點也沒有提及﹐不能不說是個很大的瑕疵。對於如何解決這個問題也不是很困難﹐只要先實現 CMapPtrToPtr CPtrArray﹐在寫入時先查 map 如果已寫過﹐就只把輸出序號寫入文件﹐如果沒有就把對象的址和輸出序號插入 map﹐再把數據寫入文件。讀出時﹐遇到第種情況(即文件有實際數據,而非只是序號)﹐就先產生個對象把數據讀出﹐接着再把新建對象的址加到數組 array 尾端﹐遇到第種情況﹐就以輸出序號爲索引直接從數組得到對象(由於寫入和讀出的順序樣﹐僅用輸出序號就可以完全解決問題)。

看這幾句話,我就知道它的價值。高手過招需要真正發力嗎?比個招式就夠了。這些提示有如醍醐灌頂,我的興奮難以言傳。這些年來我對 STL 有了很多認識,所以我以 std::map 取代 CMapPtrToPtr,以 std::vector 取代 CPtrArray,快速而成功模擬出完完整整的 MFC document 精確文件格式。這是我寫作生涯以來和讀者互動的個最精彩實例。

價值

源碼之前,了無祕密!

閱讀源碼,猶如私淑大師儀採,親炙大師風範。大師往前站,淵停嶽峙,大師往後退,瀟灑從容。誰不向往做大師物?看多了大師身手,舉手投足自然也就有了樣子。

追蹤名家源碼,歷經震撼與洗禮,你將有如脫胎換骨。說白點,個談吐思想眼界的檔次都會高出不少,當然前提是你受教。

常有詢問,編程需要賦嗎?哦,任何事情走往極致,都需要賦。任何個軟件產品的極致成功,都需要創意賦、編程賦、管理賦、營銷賦…。然而,只需用心模仿,再加點匠心獨具,任何都能夠把編程路走得穩當順遂。能讀千賦則善賦,能觀千劍則曉劍,巧者不過習者之門也。你把名家源碼融爲己用,別也會讚歎聲『你有編程賦』。

我個認爲,剖析大系統源碼的最大價值不在於編程技術的小枝小節,而在於宏觀視野與大格局的陶養。看過 MFC 源碼、STL源碼、Windows 內核結構和 kernel APIs 假碼 pseudo code)(Windows 源碼並未開放。這些都是 Andrew Schulman Matt Pietrek的勞動成果,載於 Undocumented Windows Windows Internals Windows 95 System Programming SECRETs本書。我站在他們的肩膀。),使我對於 Large Scale Object Oriented SystemApplicationFrameworkGeneric ProgrammingOperating System kernel 成竹在胸,從容自在。我雖沒有開發類似產品(我比較喜歡大刀闊斧修剪番,寫些 “lite” 版本,如 MFCLite, STLLite,做爲教育之用。),但胸丘壑已成,自有番風景。

附加價值

計算器前輩大師們開放源碼,山高水長,典範長存。這些大系統源碼固然是寶,對而言猶如際明星,只能瞻仰。若有智慧言語,引領衆認識這些寶貝,不啻亦如寶貝。

千辛萬苦窺探這些寶藏並獲得了具體成果,你會不會希望讓別也分享你的成果和喜悅?懷寶迷世,聖不許,相信 100%  都願意分享。把你的心得整理出版,立言立功,不但爲後學鋪路,對自己也有省思反芻的技術效益和版稅的經濟價值。

不過,自己理解是回事,讓別理解又是回事。思想是回事,文字表達又是回事。這正是爲什麼得道者不乏其,善書卻少得可憐的原因。要立言立功,首先,追蹤剖析的過程筆記要記得勤、記得足。其次,繁複如斯的架構該如何起頭說明,起承轉合該如何設計,使讀者循序漸進而不至於愈來愈迷糊,有賴良好的組織能力。寫這樣本書,規模、難度、力時間的規劃,和做項目沒什麼兩樣,該有的準備樣也不能少。至於以圖馭文,文圖並茂,那已是書寫功力了,不在討論之列,怕也準備不來。

無論如何,解脫之味不獨飲,開心之果不獨證,我鼓勵曾經用功並得到具體收穫的你,留足跡,把心得寫成文字,化爲圖形,以文章或書籍或其他任何型式,讓衆分享你的成果。一人得道,雞犬升,何樂如之。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章