如何讀代碼

複習得鬱悶了, 休息一下. 認真讀過的大 project 代碼不多, 主要是 argo 的 telnet/web 源代碼, netbsd & freebsd kernel 源碼部分, ACM FTP Search Engine 源碼 (前面有分析報告) 等, 隨手抓來看看抄抄的就多了. 一點體會


一. 搞清楚分析源碼的目的

不 同的目的有不同的分析源碼的方法, 要做某個 project 的 maintainer, 不但要把整個 project 的文檔代碼讀透, 還要知道它的歷史源流等等. 要理解 linux 的核心算法, 當然直奔 kernel, mm 目錄而去, 但是要寫 linux 的設備驅動, 讀法可能又不同了. 所以首先要搞清楚自己想要在源代碼裏得到什麼, 然後才鑽進去, 像無頭蒼蠅一樣在代碼裏面亂轉是不會有什麼收穫的.


二. 好的工具

這裏主要講 *nix 下的了.

ctags/etags, 生成一個交叉引用的 tags 文件, 以便在源代碼各處跳轉. 比如, 生成 tags 後, 在 vim 下對着某個函數調用 ctrl+] 就能跳到該函數的實現, ctrl+t 就能跳回來, 非常方便.

需要更復雜的功能, 分析更大型的代碼的時候. 就要用到 cscope, 它不但能做到 tags 一樣的功能, 還能對某個函數 or struct 列出代碼中調用他們的地方, 以及其他更復雜的功能. vim 和 emacs 都能很好地跟 cscope 集成.

gdb, 標準的調試工具了. 調試功能很強, 調試一些網絡服務程序的時候, attach 一個進程的功能很好用.

strace, 可以跟蹤系統調用. 通過系統調用, 可以知道系統的運行情況.

還有各種 unix 標準工具啦, 如 grep 之類.


三. 一些分析代碼的基本功

首 先第一點, 就是獲得代碼外每一個你可以獲得的文檔, 並把他們發揮到極致. 這些文檔包括官方的開發者文檔, 郵件列表上的討論 (需要的時候可以 google 出來), 還有更重要的, 代碼內的註釋. 好的文檔一句話就能解決看 2000 行代碼都沒看懂的問題.

然後, 第一步應該是: 讓程序跑起來. 那種光對着代碼發呆的靜態分析方法效率是很低的, 首先應該是參照文檔, 編譯程序並讓系統正常運行. 在這之中也可以瞭解到一些系統的基本結構和工作方式, 比方說 ACM FTP Search Engine, 就有好幾個分立的程序, 他們都會自動定時運行(所以還要看 crontab 表), 讀某些其他程序生成的數據, 產生某些數據給其他程序讀. 如果不知道這些程序是什麼時候, 如何運行的, 也不知道他們有什麼效果, 就完全談不上分析代碼了.

接着就是找對入口, 這當然不完全是指程序的 main() 函數, 而是指它在 main() 初始化, 處理參數等等奇怪的工作之後所進入的真正的處理工作. 比如說對 httpd 等網絡服務程序來說, 怎麼變 daemon 綁端口 fork 等都是差不多的, 沒有分析價值, 它的入口應該是監聽進程 fork 出子進程之後開始的真正的服務處理.

入口處展現的就是真正的程序處理代碼了, 在這裏, 我們可以大致瀏覽一下, 確定這個程序的組成風格 --- 風格也由應用決定, 像 BBS 這種菜單式, 與用戶大量交互的系統, 就應該會使用一個大的類似 {"命令名", 處理例程} 的列表, 由這裏轉向各個功能項的實現, 而一個純算法程序的結構顯然會與這個不同. 通過這些能夠大概地知道下一步我們應該去哪裏找到我們想要的東西, 不會亂打亂撞 --- 聽起來有點像一個有經驗的盜墓賊進入一個大墓的做法, 嗯.

然 後, 無論是什麼類型的程序, 這時候我們都應該看數據結構了. 數據結構是一個程序的靈魂, 特別是已經確定分析某個模塊的時候, 就應該看它的數據結構, 看他們是怎麼組織起來 (鏈表? 還是其他方法?), 看誰從屬於誰. 好的程序數據結構聲明附近都有大量的註釋, 說明這是一個什麼咚咚, 是怎麼用的, 看完這些, 對整個模塊的運行機理就算不看代碼也有大概的感覺了.

至此之後, 就是讀代碼了. 不同的程序或模塊有不同的讀法. 比方說在 NetBSD 代碼分析中, 對 VFS (虛擬文件系統) 的閱讀最好是從最頂層的系統調用響應函數 (如 sys_read(), sys_write()) 開始, 一步步深入到底層, 看一個讀/寫文件的系統調用是怎麼完成的; 而讀內存管理系統的時候, 由於它有複雜的數據結構, 就應該由低至上, 從最小的數據結構單位 (vm_page) 開始, 看用什麼辦法操縱這些數據結構; 若要讀調度算法, 就應該用事件觸發的方法, 先搞清楚什麼時候會引發調度, 分哪些情況 (這好像是一道 OS 考試題), 出現這些情況時系統處於什麼樣的狀況, 知道這些背景和觸發事件, 再去讀調度代碼就會明白很多.


四. 一些技巧.

這些技巧能夠幫你迅速地定位想要的咚咚的位置, 或者幫你迅速分析它的運行情況.

利用系統輸出.
系 統輸出包括標準/錯誤輸出輸出中的結果, log 文件等. 可以知道, 一些不尋常的系統輸出 ("沒有那個文件或目錄" 的那些就應該去看 strace 的結果了) 肯定會在代碼裏出現, 雖然會不那麼直接 (比如與一個常量關聯). 總之, 遇到一個奇怪的輸出, 用多幾次 grep 就能大概找到輸出這個信息的代碼位置, 閱讀它的上下文, 就能確定出現這樣的信息的原因. 有時系統的輸入是難以猜測的, 只有 log 文件能清楚地描述系統的運行狀況, 從而推導系統的輸入. 在進行 argo 的維護時, 往往唯一能夠讓人相信的就是系統的 log.

我 們還可以利用 strace 的結果, 特別是那些莫名其妙掛掉的程序, 他們往往掛在系統調用上 (因自身邏輯而掛的通常會有自己的提示). 所以第一件要做的事並不是在代碼裏大海撈針, 而是讀 strace 的結果, 看它到底做了哪些事, 打開了什麼文件, 最後在哪裏掛掉. 即使沒仔細讀過代碼, 也能建立一些基本的印象, 這時候再來讀代碼, 目的性就明確很多了, 你可以知道哪些東西才能產生 strace 結果中出現的系統調用, 這樣就能迅速定位有問題的地方了. 我移植過 zebra 到一塊 arm 的板子上, 結果出現了一些 PC 機上從未出現過的問題. 我在只是大概閱讀過其代碼的情況下, 沒有用複雜的遠程 gdb 調試方法, 僅僅用 strace 就定位了問題並解決了.

利用調試語句.
這 是一個非常傳統的技術, 在分析源代碼的時候也特別有用. 特別是在程序邏輯非常複雜, 程序輸入難以確定等情況下, 只要知道少數幾個關鍵的點 (比方說若前面的錯誤檢測都正常, 就應該能到這一點) 加上一些打印語句, 我們就能迅速確定代碼的運行路徑, 從而理解其運行機理. 比方說分析 libnids 代碼時, 我發現一個奇怪的無法跟蹤 TCP 三次握手的問題 (其實根本就是自己的粗心), 使用了加入關鍵點調試語句的方法, 很快就找到了問題所在.

以上幾種都是在動態的程序運行中瞭解程序運行機理的方法. 不但適用於代碼分析, 也適用於大型項目開發中的 debug 等. 總之, 我的觀點是, 不到最後時刻, 不使用 gdb 等源代碼級調試器. 細心, 聰明的觀察, 就能發現很多東西.

具體化策略.
好 的程序往往使用了一些通用而抽象的例程. 他們能讓程序變得優美, 卻往往由於過於抽象而增加了分析的難度. 這時候可以採用將一個具體的對通用例程的調用的參數代入分析的方法, 就很容易理解程序的真實運行方式了. 例如 ARGO telnet 端代碼使用了 i_read() 作爲版面文章列表, 信件列表, 精華區文章列表的通用列表操作例程, 光讀 i_read() 是很難知道一些功能的工作方式的, 而把如閱讀文章時的參數代入, 就可以知道一個操作的真實執行路徑, 理解就簡單很多. 有如 ARGO http 端使用了通用的 script.c 生成頁面, 處理模塊向它傳入一些值, 作一些簡單的控制, 再結合一個 pattern 文件, 就能生成具體頁面. 只要找一個具體的功能實現, 結合相關的 pattern 文件, 具體地看一個頁面是怎麼生成的, 就會對它有很好的理解.

比較相似的代碼.
比 較相似的代碼能找到他們的相同和不同之處, 瞭解他們在策略上的區別乃至採用某策略的原因. 比方說freebsd 的傳統 VM (虛擬內存管理) 和 netbsd 的 UVM對比來讀, 就能發現對相似的數據結構的不同的處理策略, 再來閱讀那篇描述 UVM 的論文, 就能對 netbsd 的 UVM 有個更深刻的瞭解. 對各個 telnet-based BBS 版本的對比分析, 再結合它們的源流關係, 也能發現很多東西.


五. 好習慣

最 後, 分析源代碼的一個好習慣就是做筆記. 我在源代碼分析的時候一般使用紙做筆記. 因爲這樣可以亂塗亂畫, 可以用各種自己喜歡的形式去記錄關鍵的運行原理. 通常我會一個功能模塊用一張 A4 紙記錄. (在這裏插播一句, 請珍惜用紙, 我們常常會單面打印很多資料, 用完後它們的反面就是最好的草稿紙, 用完後的草稿紙也不要亂扔, 小部分可以拿來墊飯盒, 剩下的大部分還是可以回收的)

代碼分析完之後, 應該將紙面的筆記整理一次. 這個整理過程也是再一次的代碼閱讀分析過程. 紙面筆記的整理就可以是電子版的並和大家共享的. 比如我的 NetBSD 核心源代碼分析. 閱讀代碼是很好的學習方法, 也希望各位牛人能夠共享自己的源代碼分析筆記和分析心得~ 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章