寫在前面的話:
自從我在Linuxaid.com.cn上發表一些文章開始,就不斷的有網友發來電子郵件,或者是就其中某些問題進行探討,或者是查詢其他文章的地址(往往這些網友看的是其他網站轉載的我的文章),我很高興自己寫出的文章有這麼多人回應,因為這是對我最好的贊賞,也很高興有這麼多人對我的文章感興趣。但是常常因為工作關系。有很多郵件是詢問我的其他文章在哪裡能夠找到,我不一定能夠及時回復,也覺得回復同樣的問題比較麻煩,所以在這裡重復一下,我為linuxaid.com.cn寫的文章都能在www.linuxaid.com.cn的應用開發欄目中找到,我的一部分文章收集在bambi.may10.ca/~ariesram/articles下面(這是一個很簡陋的網頁,只有文本格式的文章,也歡迎有興趣的網友幫我設計一下網頁),我的郵件地址:[email protected], 或者[email protected]。請轉載文章的網站保留這一說明,歡迎網友寫email給我探討問題,雖然我不能保證能及時回復。
正文:
由於工作的關系,我常常需要讀一些源代碼,並在上面做一些修改並且拿來使用,或者是借鑒其中的某些部分。可以說,open source對於程序員來說,是很有意義的事情。根據我的經驗,讀源代碼,至少有3個好處。第一個好處是可以學習到很多編程的方法,看好的源代碼,對於提高自己的編程水平,比自己寫源代碼的幫助更大。當然不是說不用自己寫,而是說,自己寫代碼的同時,可以從別人寫的好的源代碼中間學習到更多的編程方法和技巧。第二個好處是,可以提高自己把握大規模源代碼的能力。一個比較大型的程序,往往都是經過了很多個版本很長的時間,有很多人參與開發,修正錯誤,添加功能而發展起來的。所以往往源代碼的規模都比較大,少則10-100多k, 多的有好幾十個MB. 在閱讀大量源代碼的時候,能夠提高自己對大的軟件的把握能力,快速了解脈絡,熟悉細節,不僅僅是編程技巧,還能在程序的架構,設計方面提高自己的能力。(這裡說一句題外話,<<設計模式>>這本書相信很多人都看過,而且很多人對它推崇備至,奉為經典。現在也出了不少書,都是冠以"設計模式"這一名稱。在書中就提到,設計模式並不是一本教材,不是教你如何去編程序,而是把平時編程中一些固定的模式記錄下來,加以不斷的測試和改進,分發給廣大程序員的一些經驗之談。我在看這本書的時候,有一些地方一些設計方法往往讓我有似曾相識的感覺,另外一些則是我以前就常常用到的。而這些經驗的獲得,一部分得益於自己的編碼過程,另外一個很重要的來源就是閱讀別人寫的源代碼。)閱讀源代碼第三個好處,就是獲得一些好的思想。比如,有很多人在開始一個軟件項目之前都喜歡到sourceforge.net上去找一下,是否有人以前做過相同或者相似的軟件,如果有,則拿下來讀一讀,可以使自己對這個軟件項目有更多更深的認識。我以前曾經想找一本關於如何閱讀源代碼的書來看看,卻沒有找到。相反,倒是找到了不少分析源代碼的書,比如Linux kernel, Apache source, 等等。所以我想,為什麼不自己來寫下一些經驗和大家交流呢?(當然不是寫書,沒有那個能力也沒有那個時間。)所以在這裡我准備用一個例子來寫一下如何閱讀源代碼,分享一些經驗,算是拋磚引玉吧!
我找的例子是一個統計日志的工具,webalizer. (這個工具我以前用過,似乎記得以前的版本是用perl寫的,不知道現在為什麼作者把它完全修改成了C,可能是為了效率,也可能根本就是我記錯了。)之所以選擇這個軟件來作為例子,一方面是因為它是用C寫的,流程比較簡單,沒有C++的程序那麼多的枝節,而且軟件功能不算復雜,代碼規模不大,能夠在一篇文章的篇幅裡面講完; 另外一個方面是因為恰巧前段時間我因為工作的關系把它拿來修改了一下,剛看過,還沒有忘記。 :-)我采用的例子是webalizer2.01-09, 也可以到它的網站http://www.mrunix.net/webalizer/ 下載最新的版本。這是一個用C寫的,處理文本文件(簡單的說是這樣,實際上它支持三種日志文本格式:CLF, FTP, SQUID), 並且用Html的方式輸出結果。讀者可以自己去下載它的源代碼包,並一邊讀文章,一邊看程序。解壓縮它的tar包(我download的是它的源代碼tar包),在文件目錄中看到這樣的結果:
$ ls
aclocal.m4 dns_resolv.c lang output.h webalizer.1
CHANGES dns_resolv.h lang.h parser.c webalizer.c
configure graphs.c linklist.c parser.h webalizer.h
configure.in graphs.h linklist.h preserve.c webalizer_lang.h
COPYING hashtab.c Makefile.in preserve.h webalizer.LSM
Copyright hashtab.h Makefile.std README webalizer.png
country-codes.txt INSTALL msfree.png README.FIRST
DNS.README install-sh output.c sample.conf
首先,我閱讀了它的README(這是很重要的一個環節), 大體了解了軟件的功能,歷史狀況,修改日志,安裝方法等等。然後是安裝並且按照說明中的缺省方式來運行它,看看它的輸出結果。(安裝比較簡單,因為它帶了一個configure, 在沒有特殊情況出現的時候,簡單的./configure, make, make install就可以安裝好。)然後就是閱讀源代碼了。我從makefile開始入手(我覺得這是了解一個軟件的最好的方法)在makefile開頭,有這些內容:
prefix = /usr/local
exec_prefix = ${prefix}
BINDIR = ${exec_prefix}/bin
MANDIR = ${prefix}/man/man1
ETCDIR = /etc
CC = gcc
CFLAGS = -Wall -O2
LIBS = -lgd -lpng -lz -lm
DEFS = -DETCDIR="/etc" -DHAVE_GETOPT_H=1 -DHAVE_MATH_H=1
LDFLAGS=
INSTALL= /usr/bin/install -c
INSTALL_PROGRAM=${INSTALL}
INSTALL_DATA=${INSTALL} -m 644
# where are the GD header files?
GDLIB=/usr/include
這些定義了安裝的路徑,執行程序的安裝路徑,編譯器,配置文件的安裝路徑,編譯的選項,安裝程序,安裝程序的選項等等。要注意的是,這些並不是軟件的作者寫的,而是./configure的輸出結果。呵呵. :-)下面才是主題內容,也是我們關心的。
# Shouldn't have to toUCh below here!
all: webalizer
webalizer: webalizer.o webalizer.h hashtab.o hashtab.h
linklist.o linklist.h preserve.o preserve.h
dns_resolv.o dns_resolv.h parser.o parser.h
output.o output.h graphs.o graphs.h lang.h
webalizer_lang.h
$(CC) ${LDFLAGS} -o webalizer webalizer.o hashtab.o linklist.o preserv
e.o parser.o output.o dns_resolv.o graphs.o ${LIBS}
rm -f webazolver
ln -s webalizer webazolver
webalizer.o: webalizer.c webalizer.h parser.h output.h preserve.h
graphs.h dns_resolv.h webalizer_lang.h
$(CC) ${CFLAGS} ${DEFS} -c webalizer.c
parser.o: parser.c parser.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c parser.c
hashtab.o: hashtab.c hashtab.h dns_resolv.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c hashtab.c
linklist.o: linklist.c linklist.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c linklist.c
output.o: output.c output.h webalizer.h preserve.h
hashtab.h graphs.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c output.c
preserve.o: preserve.c preserve.h webalizer.h parser.h
hashtab.h graphs.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c preserve.c
dns_resolv.o: dns_resolv.c dns_resolv.h lang.h webalizer.h
$(CC) ${CFLAGS} ${DEFS} -c dns_resolv.c
graphs.o: graphs.c graphs.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -I${GDLIB} -c graphs.c
好了,不用再往下看了,這些就已經足夠了。從這裡我們可以看到這個軟件的幾個源代碼文件和他們的結構。webalizer.c是主程序所在的文件,其他的是一些輔助程序模塊。對比一下目錄裡面的文件,
$ ls *.c *.h
dns_resolv.c graphs.h lang.h output.c parser.h webalizer.c
dns_resolv.h hashtab.c linklist.c output.h preserve.c webalizer.h
graphs.c hashtab.h linklist.h parser.c preserve.h webalizer_lang.h
於是,讓我們從webalizer.c開始吧。
作為一個C程序,在頭文件裡面,和C文件裡面定義的extern變量,結構等等肯定不會少,但是,單獨看這些東西我們不可能對這個程序有什麼認識。所以,從main函數入手,逐步分析,在需要的時候再回頭來看這些數據結構定義才是好的方法。(順便說一句,Visual C++, 等windows下的IDE工具提供了很方便的方法來獲取函數列表,C++的類列表以及資源文件,對於閱讀源代碼很有幫助。Unix/Linux也有這些工具,但是,我們在這裡暫時不說,而只是通過最簡單的文本編輯器vi來講)。跳過webalizer.c開頭的版權說明部分(GPL的),和數據結構定義,全局變量聲明部分,直接進入main()函數。在函數開頭,我們看到:
/* initalize epoch */
epoch=jdate(1,1,1970); /* used for timestamp adj. */
/* add default index. alias */
ad