跳到主要內容

中文試譯:How do you read source code?

原文作者:Daniel Himmelein
原文連結:http://himmele.blogspot.com/2012/01/how-do-you-read-source-code.html


如果這個世界就像Marc Andreessen與我所認為的一樣,正在被軟體吞噬,你要如何[讀|吞]原始碼?好吧,讓我們先回答究竟為何你需要很會閱讀原始碼。首先,搞懂事物是如何運作的總是樂趣無窮,透過閱讀程式碼,我們就是對有趣的軟體系統與專案在做這件事。另一個閱讀原始碼的理由是為了透過別人的經驗以及他們的錯誤,能夠更好地(並更快地)閱讀以及撰寫軟體。如果你參與一個新的軟體公司或開放原始碼計畫,你很有可能要與一個既存的巨大代碼庫奮戰,所以你需要能夠很快的熟悉它,然後實作測試以及功能,或修正錯誤。閱讀原始碼的主要目的永遠都是為了理解軟體系統的所有面貌。在這篇文章中,我提供一些閱讀時的建議與模式,這些都讓我的軟體工程師生涯輕鬆許多。 :-)

所以現在的主要問題是:你要如何閱讀原始碼?在你一頭栽進一個軟體專案的原始碼之前,你必須確認你擁有足夠的相關知識以瞭解軟體的特定區塊。因此,你需要從閱讀文件開始以獲取大的概念,以及閱讀與該軟體平台/產品相關的資訊科學,或部份領域(像是Windows apps、Mac OS X、iOS apps、Android apps、作業系統、電腦網路、瀏覽器、搜尋引擎、資料庫...)。你不需要知道該主題的所有東西,但是你必須能夠瞭解該軟體平台/產品的核心抽象概念與基本的建構元件。也就是說,在開始撰寫你自己的Linux排程演算法前,你應該要知道甚麼是processes、threads、semaphores等等(可參考Andrew S. Tanenbaum的Modern Operating Systems)。在開始之前,你也應該知道Linux特定的process管理機制(可參閱Rober Love的Linux Kernel Development以及Wolfgang Mauerer的Linux Kernel Architecture)。

不過在研究一個軟體之前,你大概就已經有這類基礎了。所以我們就從閱讀原始碼的建議與模式開始吧。你將會注意到,對於所有的軟體系統或至少所有的大型軟體子系統都有一些基本的建構元件以及核心的抽象觀念會在所有的地方出現。這些元件(類別、模組、參與者、資料結構等等)就是所謂的hubs。這些hubs會是代碼庫中的多重面相或子系統。因此,hubs連接子系統,並且使得整個代碼庫看起來像是一個小世界。Hubs形塑了軟體工程師建造軟體架構的環境。它們也實作了許多核心的功能。當軟體系統逐漸成長,越來越多其他的元件會依賴於這些hubs。(譯註:這邊所謂的hubs,有時會以隱喻的方式出現)

所以要先研究這些hubs,並且瞭解它們所擔負的責任。即使是大型軟體,通常也只有為數不多的hubs。因此,你不需要害怕數百萬行代碼,因為hubs會指引你悠遊其中。如果我們看一下Android OS,我會說以下類別是hubs:Zygote、ActivityManagerService、WindowManagerService、PackageManagerService、ConectivityService以及SurfaceFlinger。你看,才六個元件 :-)。你可以在小一點的規模重複這個遊戲,以Andorid UI framework來說,View、ViewFroup以及ViewRoot是hubs,許多UI元件都從上建構出來。這種化約的手法對於其他的軟體系統也同樣有效,像是作業系統、檔案系統、網路堆疊、網際網路後端平台等等。如果想瞭解更多hubs與網絡理論,我推薦Albert-Laszlo Barabasi的書Linked。

下一步,在識別出hubs之後,你需要試著瞭解hubs之間的互動模式。它們的互動會奠基於不同的機制,可能是純粹的API呼叫,或訊息傳遞(message queue或IPC calls)。為了瞭解hubs之間的依賴關係,我建議畫出hubs的部份圖形、它們的關係以及互動流程。舉例來說,看一下我之前的一篇關於Android架構模式的文章。在第七頁,有一張關於Andorid如何開始activities、services、content provider的圖,這些元件位於他們專屬的Linux行程中。透過了數次的ActivityManagerService、Zygote行程以及app行程之間的互動,才完成了這件事情。

如你所見,獲取大局觀的手法是透過由上而下地識別出hubs以及瞭解它們之間的互動。為了深入挖掘軟體系統特定部份,我們必須改變我們閱讀原始碼的模式。所以,我們會切換到由下而上的方式去觀察模組、類別、資料結構、方法、函式等等。然後,我們就能夠結合這兩種代碼閱讀方式。這種總結由上而下以及由下而上所獲得心得的方法就叫作downward causation。

我認為由下而上的手法最好從那些給予hubs呼吸的主動元件(threads、actors、processes)開始。這是因為要瞭解並推演一些代碼時,你必須瞭解hubs執行時的環境。所以,確認哪些主動元件運行了系統代碼的哪些部份,試著瞭解它們之間互動時是如何進行以及在何時進行。這會幫助你達成閱讀代碼的主要目的,也就是能夠思考並推演軟體系統代碼的所有面向(僅依賴你的腦袋而不用外部工具的幫忙,像除錯器 :-))。

栽進部份代碼的細節最好的方式就是試驗看看。我會透過加進一些logging代碼或在假設其行為後寫一點測試。另一個方式就是對代碼作點修改,然後看看在新的情況下行為變成如何。把系統弄掛也能幫助你學習它。 ;-)

當閱讀代碼時,總是要問自己:"它是如何運作的?"以及"為何開發者要這麼作?"這很可能讓你好幾晚都睡不好,不過也會讓你變成更好的軟體工程施以及架構師。任何讓自己對於閱讀代碼變得更好的方法都會幫助你開發出更強健的除錯與分析技術,然後你就可以實作新功能,修正臭蟲,或作一些重構。

透過思考與反思你正在閱讀的原始碼,你將會學到許多撰寫軟體系統與平台的技術。除此以外,你也可以從糟糕的軟體學到如何在開發系統時避免同樣的錯誤。此外,這邊有兩篇文章,是關於如何撰寫出好的代碼與軟體系統。Rich Hickey在InfoQ的演講"Simple made easy",以及Erlang programming rules and conventions。不論你所使用的編程語言為何,這兩篇文章都非常傑出有幫助。所以,閱讀原始碼是非常好玩的。也許下次不要讀又一本的軟體書籍,而是閱讀一些原始碼(GitHub是一個很好的集散地)。

由於需要一些能夠堅持下去的動力才能夠跳進巨大的代碼庫,我建議挑選能提供一些樂趣以及目的的專案 :-)。或許下面的列表包含了一個對你有趣的專案...

Software projects

留言

這個網誌中的熱門文章

誰在呼叫我?不同的backtrace實作說明好文章

今天下班前一個同事問到:如何在Linux kernel的function中主動印出backtrace以方便除錯? 寫過kernel module的人都知道,基本上就是用dump_stack()之類的function就可以作到了。但是dump_stack()的功能是如何作到的呢?概念上其實並不難,慣用手法就是先觀察stack在function call時的變化(一般OS或計組教科書都有很好的說明,如果不想翻書,可以參考 這篇 ),然後將對應的return address一層一層找出來後,再將對應的function名稱印出即可(透過執行檔中的section去讀取函式名稱即可,所以要將KALLSYM選項打開)。在userspace的實作可參考Jserv介紹過的 whocallme 或對岸好手實作過的 backtrace() ,都是針對x86架構的很好說明文章。 不過從前面兩篇文章可以知道,只要知道編譯器的calling convention,就可以實作出backtrace,所以是否GCC有提供現成的機制呢?Yes, that is what __builtin_return_address() for!! 可以參考這篇 文章 。該篇文章還提到了其他可以拿來實作功能更齊全的backtrace的 程式庫 ,在了解了運作原理後,用那些東西還蠻方便的。 OK,那Linux kernel是怎麼做的呢?就是用頭兩篇文章的方式啦~ 每個不同的CPU架構各自手工實作一份dump_stack()。 為啥不用GCC的機制?畢竟...嗯,我猜想,除了backtrace以外,開發者還會想看其他register的值,還有一些有的沒的,所以光是GCC提供的介面是很難印出全部所要的資訊,與其用半套GCC的機制,不如全都自己來~ arm的實作 大致上長這樣,可以看到基本上就只是透過迭代fp, lr, pc來完成: 352 void unwind_backtrace (struct pt_regs * regs , struct task_struct *tsk) 353 { 354 struct stackframe frame ; 355 register unsigned long current_sp asm ( "...

淺讀Linux root file system初始化流程

在Unix的世界中,file system佔據一個極重要的抽象化地位。其中,/ 所代表的rootfs更是所有後續新增file system所必須依賴前提條件。以Linux為例,黑客 Jserv 就曾經詳細說明過 initramfs的背後設計考量 。本篇文章不再重複背景知識,主要將追蹤rootfs初始化的流程作點整理,免得自己日後忘記。 :-) file system與特定CPU架構無關,所以我觀察的起點從init/main.c的start_kernel()開始,這是Linux作完基本CPU初始化後首先跳進的C function(我閱讀的版本為 3.12 )。跟root file system有關的流程羅列如下: start_kernel()         -> vfs_caches_init_early()         -> vfs_caches_init()                 -> mnt_init()                         -> init_rootfs()                         -> init_mount_tree()         -> rest_init()                 -> kernel_thread(kernel_init,...) 其中比較重要的是mnt_int()中的init_rootfs()與init_mout_tree()。init_rootfs()實作如下: int __init init_root...

中文試譯:Load-time relocation of shared libraries

作者: eliben 原文連結: http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries/ 共享庫的載入時重定位 這篇文章的目的在解釋現代作業系統如何讓共享庫在載入時的連結動作發生。我們聚焦在32位元的x86 Linux上頭,但相同的原則在其他的作業系統與CPUs上一樣適用。 要注意的是,共享庫有許多不同的名字 - 共享庫(shared libraries),共享物件( shared objects),動態共享物件( dynamic shared objects (DSOs)),動態連結庫( dynamically linked libraries(如果你習慣的是Windows的環境,就知道這是所謂的DLL))。為了一致性,我在整篇文章中會使用"共享庫"這個字眼。 載入可執行檔 如同其他支援虛擬記憶體的作業系統,Linux將可執行檔載入到一個固定的記憶體位址。如果我們檢視任意一個可執行檔的ELF header,我們將可發現一個Entry point的位址: $ readelf -h /usr/bin/uptime ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 [...] some header fields Entry point address: 0x8048470 [...] some header fields 這是由linker所放置的,可告訴作業系統要從何處開始執行此執行檔[1]。如果我們用GDB載入程式並觀察0x8048470的位址,我們將可看到.text segment的第一個被執行的指令。 這代表的是,當linker連結可執行檔時,會把全部的內部符號引用的位址都確定下來(function以及data),確定它們的固定的、最終的位址。Linker本身會對自己進行一些relocation[2],但最終的結果不會有任何的relocation。 講啥啊?注意,我在上段文中特別強...