天天看點

閱讀源代碼系列

 閱讀源代碼系列 轉 http://blog.csdn.net/goodfriends2007/article/details/6881883

摘要:本文從閱讀源代碼的目的和意義開始,主要介紹了怎樣閱讀别人的源代碼,列舉了閱讀開源代碼的例子,以及閱讀開源代碼工具和閱讀源代碼的技巧。

引言

大家在軟體開發過程中,在加入一個團隊後或多或少都會接觸到原來的源代碼,要不是在原有基礎上繼續開發或者維護,或者在原有代碼上改進優化進行新産品開發,或者在開發一個新子產品的時後需要借鑒下類似的開源軟體源代碼,接下來我講談談對源代碼的處理問題。

1閱讀源代碼的目的和意義

1.1閱讀源代碼的目的

閱讀别人的代碼作為軟體開發人員來說是一件經常要做的事情。

第一個是在學習新的程式設計語言的時候通過閱讀别人的代碼是一個最好的學習方法,也是積累程式設計經驗和技巧的過程。如果你有機會閱讀一些作業系統的代碼會幫助你了解一些基本的原理。

第二個就是在你作為一個品質保證人員或一個小上司的時候;如果你要做白盒測試的時候沒有閱讀代碼的能力是不能完成相應的任務。

第三個就是如果你中途接手一個項目的時候或給一個項目做代碼維護和優化的時候是要有閱讀代碼的能力的。

1.2.閱讀源代碼意義

因為源代碼的處理在程式員的軟體開發中不可避免,那麼在閱讀源代碼中有哪些意義呢?

第一個意義是可以學習到很多程式設計的方法和技巧。看好的源代碼,對于提高自己的程式設計水準,比自己寫源代碼的幫助更大。當然不是說不用自己寫,而是說,自己寫代碼的同時,可以從别人寫的好的源代碼中間學習到更多的程式設計方法和技巧。

第二個意義是,可以提高自己把握大規模源代碼的能力。一個比較大型的程式,往往都是經過了很多個版本很長的時間,有很多人參與開發,修正錯誤,添加功能而發展起來的。是以往往源代碼的規模都比較大,少則10-100多k, 多的有好幾十個MB. 在閱讀大量源代碼的時候,能夠提高自己對大的軟體的把握能力,快速了解脈絡,熟悉細節,不僅僅是程式設計技巧,還能在程式的架構,設計方面提高自己的能力。在設計模式的書中就提到,設計模式并不是一本教材,不是教你如何去程式設計式,而是把平時程式設計中一些固定的模式記錄下來,加以不斷的測試和改進,分發給廣大程式員的一些經驗之談。

第三個意義,就是獲得一些好的思想。比如有很多人在開始一個軟體項目之前都喜歡到sourceforge.net上去找一下,是否有人以前做過相同或者相似的軟體,如果有,則拿下來讀一讀,可以使自己對這個軟體項目有更多更深的認識。

2怎樣閱讀别人的源代碼

2.1收集所有可能收集的材料

閱讀代碼要做的第一件事情是收集所有和項目相關的資料。比如你要做一個項目的維護和優化,那麼你首先要搞明白項目做什麼用的,那麼調研、需求分析文檔、概要設計文檔、詳細設計文檔、測試文檔、使用手冊都是你要最先搞到手的。如果你是為了學習那麼盡量收集和你的學習有關的資料,比如你想學習linux的檔案系統的代碼,那最好要找到linux的使用手冊、以及檔案系統設計的方法、資料結構的說明。這些資料在網上或書店裡都可以找到。

材料的種類分為幾種類型:

a、基礎資料。

比如你閱讀turbo c2的源代碼你要有turbo c2的函數手冊,使用手冊等專業書籍;java 的話不但要有函數手冊,還要有類庫函數手冊。這些資料都是你的基礎資料。另外你要有一些關于uml的資料可以作為查詢手冊也是一個不錯的選擇。

b、和程式相關的專業資料。

每一個程式都是和相關行業相關的。比如我閱讀過一個關于氣象分析方面的代碼,因為裡邊用到了一個複雜的資料轉換公式,是以不得不把自己的大學時候課本找出來來複習一下高等數學的内容。如果你想閱讀linux的檔案管理的代碼,那麼找一本講解linux檔案系統的書對你的幫助會很大。

c、相關行業的文檔資料

這一部分的資料分為兩種,一個相關行業的資料,比如你要閱讀一個稅務系統的代碼那麼有一些财務/稅務系統的專業資料和國家的相關的法律、法規的資料是必不可少的。

2.2留備份,構造可運作的環境

了解基礎知識,不要上來就閱讀代碼,打好基礎可以做到事半功倍的效果,代碼拿到手之後的第一件事情是先做備份,最好是刻在一個CD光牒上,同時上傳在版本控制CVS、SVN或GIT庫,在代碼閱讀的時候一點不動代碼是很困難的一件事情,特别是你要做一些修改性或增強性維護的時候。而一旦做修改就可能發生問題,到時候要恢複是經常發生的事情,如果你不能很好的使用版本控制軟體那麼先留一個備份是一個最起碼的要求了。

在做完備份之後最好給自己構造一個可運作的環境,當然可能會很麻煩,但可運作代碼和不可運作的代碼閱讀起來難度會差很多的(運作的代碼可以debug,單步跟,資料流是怎麼走的,可以比較容易理清動态流程)。是以多用一點時間搭建一個環境是很值得的,而且我們閱讀代碼主要是為了修改其中的問題或做移植操作。不能運作的代碼除了可以學到一些技術以外,用處有限。

2.3 找閱讀開始的地方

做什麼事情都要知道從那裡開始,讀程式也不例外。在c語言裡,首先要找到main()函數,然後逐層去閱讀,其他的程式無論是VC、delphi、Java都要首先找到程式頭,否則你是很難分析清楚程式的層次關系。

2.4分層次閱讀

在閱讀代碼的時候不要一頭就紮下去,這樣往往容易隻見樹木不見森林,閱讀代碼比較好的方法有一點像二叉樹的廣度優先的周遊。在程式主體一般會比較簡單,調用的函數會比較少,根據函數的名字以及層次關系一般可以确定每一個函數的大緻用途,将你的了解作為注解寫在這些函數的邊上。當然很難一次就将全部注解都寫正确,有時候甚至可能是你猜測的結果,不過沒有關系這些注解在閱讀過程是不斷修正的,直到你全部了解了代碼為止。一般來說采用逐層閱讀的方法可以是你系統的了解保持在一個正确的方向上。避免一下子紮入到細節的問題上。在分層次閱讀的時候要注意一個問題,就是将系統的函數和開發人員編寫代碼區分開。在c, c++,java ,delphi中都有自己的系統函數,不要去閱讀這些系統函數,除非你要學習他們的程式設計方法,否則隻會浪費你的時間。将系統函數表示出來,注明它們的作用即可,區分系統函數和自編函數有幾個方法,一個是系統函數的程式設計風格一般會比較好,而自編的函數的程式設計風格一般比較會比較差。從變量名、行之間的縮進、注解等方面一般可以分辨出來,另外一個是像DELPHI會在你程式設計的時候給你生成一大堆檔案出來,其中有很多檔案是你用不到了,可以根據檔案名來區分一下時候是系統函數,最後如果你實在确定不了,那就用開發系統的幫助系統去查一下函數名,對一下參數等來确定即可。

2.5寫注解

寫注解是在閱讀代碼中最重要的一個步驟,在我們閱讀的源代碼一般來說是我們不熟悉的系統,閱讀别人的代碼一般會有幾個問題:

a、搞明白别人的程式設計思想不是一件很容易的事情,即使你知道這段程式的思路的時候也是一樣。

b、閱讀代碼的時候代碼量一般會比較大,如果不及時寫注解往往會造成讀明白了後邊忘了前邊的現象。

c、閱讀代碼的時候難免會出現了解錯誤,如果沒有及時的寫注解很難及時的發現這些錯誤。

d、不寫注解有時候你發生你很難确定一個函數你什麼時候閱讀過,它的功能是什麼,經常會發生重複閱讀、了解的現象。

一些寫注解的基本方法:

a、猜測的去寫,剛開始閱讀一個代碼的時候,你很難一下子就确定所有的函數的功能,不妨采用采用猜測的方法去寫注解,根據函數的名字、位置寫一個大緻的注解,當然一般會有錯誤,但你的注解實際是不但調整的,直到最後你了解了全部代碼。

b、按功能去寫,别把注解寫成文法說明書,千萬别看到fopen就寫打開檔案,看到fread就寫讀資料,這樣的注解一點用處都沒有,而應該寫在此處打開參數配置檔案(****.dat)讀出系統初始化參數……,這樣才是有用的注解。

c、在寫注解的時候另外要注意的一個問題是厘清楚系統自動生成的代碼和使用者自己開發的代碼,一般來說沒有必要給系統自動生成的代碼寫注解。像delphi的代碼,我們往往要自己編寫一些自己的代碼段,還要對一些系統自動生成的代碼段進行修改,這些代碼在閱讀過程是要寫注解的,但有一些沒有修改過的自動生成的代碼就沒有必要寫注解了。

d、在主要代碼段要寫較為詳細的注解。有一些函數或類在程式中起關鍵的作用,那麼要寫比較詳細的注解。這樣對你了解代碼有很大的幫助。

e、對你了解起來比較困難的地方要寫詳細的注解,在這些地方往往會有一些程式設計的技巧。不了解這些程式設計技巧對你以後的了解或移植會有問題。

f、寫中文注解。如果你的英文足夠的好,不用看這條了,但很多的人英文實在不怎麼樣,那就寫中文注解吧,我們寫注解是為了加快自己的了解速度。中文在大多數的時候比英文更适應中國人。與其寫一些誰也看不懂的英文注解還不如不寫。

2.6重複閱讀

一次就可以将所有的代碼都閱讀明白的人是沒有的。至少我還沒有遇到過。反複的去閱讀同一段代碼有助于代碼的了解。一般來說,在第一次閱讀代碼的時候你可以跳過很多一時不明白的代碼段,隻寫一些簡單的注解,在以後的重複閱讀過程用,你對代碼的了解會比上一次了解的更深刻,這樣你可以修改那些注解錯誤的地方和上一次沒有了解的對方。一般來說,對代碼閱讀3,4次基本可以了解代碼的含義和作用。(書讀百遍,其意自現)

2.7運作并修改代碼

如果你的代碼是可運作的,那麼先讓它運作起來,用單步跟蹤的方法來閱讀代碼,會提高你的代碼了解速度。通過看中間變量了解代碼的含義,而且對以後的修改會提供很大的幫助。

用自己的代碼代替原有代碼,看效果,但在之前要保留源代碼。600行的一個函數,閱讀起來很困難,程式設計的人不是一個好的習慣。在閱讀這個代碼的時候将代碼進行修改,變成了14個函數。每一個大約是40-50 行左右。

3怎樣閱讀開源代碼的例子

開源代碼在linux下多一些,下面借鑒别的一個例子對閱讀源代碼的過程了解下。

我找的例子是一個統計日志的工具,webalizer。之是以選擇這個軟體來作為例子,一方面是因為它是用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()函數。在函數開頭,我們看到:

  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  for (i=0;i<31;i++)

  {

  tm_xfer[i]=0.0;

  tm_hit[i]=tm_file[i]=tm_site[i]=tm_page[i]=tm_visit[i]=0;

  }

  for (i=0;i<24;i++)

  {

   th_hit[i]=th_file[i]=th_page[i]=0;

   th_xfer[i]=0.0;

  }

......

}略過去一大串代碼,不用看了,肯定是計數器清0。在主程式中,接下來是:

  if (page_type==NULL)

  {

   if ((log_type == LOG_CLF) || (log_type == LOG_SQUID))

   {

     add_nlist("htm*" ,&page_type);

     add_nlist("cgi" ,&page_type);

     if (!isinlist(page_type,html_ext)) add_nlist(html_ext,&page_type);

   }

   else add_nlist("txt" ,&page_type);

  }

page_type這個變量在前面見過,

case 'P': add_nlist(optarg,&page_type); break;

   ntop_entry=ntop_exit=0;

   ntop_search=0;

  }

  else

  .....

這一段是對于FTP的日志格式,設定搜尋清單。

  for (i=0;i  {

   sm_htab[i]=sd_htab[i]=NULL;

   um_htab[i]=NULL;

   rm_htab[i]=NULL;

   am_htab[i]=NULL;

   sr_htab[i]=NULL;

  }

清空哈西表,為下面即将進行的排序工作做好準備。關于哈西表,這是資料結構中常用的一種用來快速排序的結構,如果不清楚,可以參考相關書籍,比如清華的<<資料結構>>教材或者<<資料結構的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[i]=hist_year[i]=hist_fday[i]=hist_lday[i]=0;

   hist_hit[i]=hist_files[i]=hist_site[i]=hist_page[i]=hist_visit[i]=0;

   hist_xfer[i]=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[i],

            &hist_year[i],

            &hist_hit[i],

            &hist_files[i],

            &hist_site[i],

            &hist_xfer[i],

            &hist_fday[i],

            &hist_lday[i],

            &hist_page[i],

            &hist_visit[i]);

     if (numfields==8)

     {

      hist_page[i] = 0;

      hist_visit[i] = 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[i] != 0) && (hist_hit[i] != 0))

     {

      fprintf(hist_fp,"%d %d %lu %lu %lu %.0f %d %d %lu %lu ",

              hist_month[i],

              hist_year[i],

              hist_hit[i],

              hist_files[i],

              hist_site[i],

              hist_xfer[i],

              hist_fday[i],

              hist_lday[i],

              hist_page[i],

              hist_visit[i]);

     }

   }

   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[i]=tolower(log_rec.datetime[i]);

     for (i=0;i<12;i++)

     {

      if (strncmp(log_month[i],&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        { total_ignore++; continue; }

      else

      {

        rec_sec = cur_sec;

        rec_min = cur_min;

        rec_hour = cur_hour;

        rec_day = cur_day;

        rec_month = cur_month;

        rec_year = cur_year;

        rec_tstamp= cur_tstamp;

      }

     }

     cur_tstamp=rec_tstamp;

如果該日期、時間沒有錯誤,則該資料是一個好的資料,将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(" ");

   }

這一段,做了一些後期的處理。接下來的部分,我想在本文中略過,留給感興趣的讀者自己去做分析。原因有兩點:

a、這個程式在前面結構化比較強,而到了後面結構上有些亂,雖然代碼效率還是比較高,但是可重用性不夠強, 限于篇幅,我就不再一一解釋了。

b、前面分析程式過程中,也對後面的代碼做了一些預測和估計,也略微涉及到了後面的代碼,而且讀者可以根據上面提到的原則來自己分析代碼,也作為一個實踐吧。

4閱讀開源代碼工具

工欲善其事,必先利其器。閱讀源代碼最好的工具是understand,用Source Insight也可以,至于Understand工具的使用技巧和方法參見代碼閱讀分析工具Understand使用總結這篇文章。

5閱讀源代碼的技巧

最後,對于在這篇文章中提到的分析源代碼程式的一些方法做一下小結,以作為本文的結束。

5.1分析一個源代碼,一個有效的方法是:

a、閱讀源代碼的說明文檔,比如源代碼中的README, 作者寫的非常的詳細,仔細讀過之後,在閱讀程式的時候往往能夠從README檔案中找到相應的說明,進而簡化了源程式的閱讀工作。

b、如果源代碼有文檔目錄,一般為doc或者docs, 最好也在閱讀源程式之前仔細閱讀,因為這些文檔同樣起了很好的說明注釋作用。

c、從makefile檔案入手,分析源代碼的層次結構,找出哪個是主程式,哪些是函數包。這對于快速把握程式結構有很大幫助。

d、從main函數入手,一步一步往下閱讀,遇到可以猜測出意思來的簡單的函數,可以跳過。但是一定要注意程式中使用的全局變量(如果是C程式),可以把關鍵的資料結構說明拷貝到一個文本編輯器中以便随時查找。

e、分析函數包(針對C程式),要注意哪些是全局函數,哪些是内部使用的函數,注意extern關鍵字。對于變量,也需要同樣注意。先分析清楚内部函數,再來分析外部函數,因為内部函數肯定是在外部函數中被調用的。

f、需要說明的是資料結構的重要性:對于一個C程式來說,所有的函數都是在操作同一些資料,而由于沒有較好的封裝性,這些資料可能出現在程式的任何地方,被任何函數修改,是以一定要注意這些資料的定義和意義,也要注意是哪些函數在對它們進行操作,做了哪些改變。

g、在閱讀程式的同時,最好能夠把程式存入到SVN之類的版本控制器中去,在需要的時候可以對源代碼做一些修改試驗,因為動手修改是比僅僅是閱讀要好得多的讀程式的方法。在你修改運作程式的時候,可以從SVN中把原來的代碼調出來與你改動的部分進行比較(diff指令), 可以看出一些源代碼的優缺點并且能夠實際的練習自己的程式設計技術。

h、閱讀程式的同時,要注意一些小工具的使用,能夠提高速度,比如vi中的查找功能,模式比對查找,做标記,還有grep,find這兩個最強大最常用的文本搜尋工具的使用。

i、對于一個大的項目,首先要弄清項目的架構結構和各個項目子產品的功能(輸入什麼,處理以後輸出什麼). 在這一點上Ant工具做的相當到位,通過編寫build.xml和xml的良好的文法結構可以清楚的看到架構。Make工具也做比較出色。具體細節可參考GNU Make /Apache Ant Manual和程式的build.xml或makefile檔案。

j、參照源代碼和對應文檔及業務知識 掌握各個項目子產品的主流程也就是先從每個子產品的main函數開始,按照順序列出所用的函數,試着畫流程圖。注意:對于列出的函數我們現在隻關心輸入什麼,處理後輸出什麼即函數的功能,不關心函數的實作,用UltraEdit32最新版閱讀時十分友善。(用sourceinsight或者understand閱讀源代碼工具更好)

k、以上兩步熟悉以後,在進一步熟悉各個項目子產品的主流程,要弄清各個自定義函數的具體實作(标準庫函數除外 原因:由廠商提供,廠商隻提供函數的功能)。

l、在每一步都要做好源代碼閱讀筆記,總結方法和技巧。每個項目的源代碼閱讀要多讀幾遍,書讀百遍,其義之見,定期與同仁切磋交流。

m、提出更好的解決方案,(按照軟體工程的設計步驟)評估方案的性能(界面,易用性,記憶體等方面).

n、每日建構 具體參考建構工具和相關文擋,接着,看一看大師是如何做的。一般,要初步了解人家的架構模型,(這可以通過追蹤一些核心函數/類得到一些印象,或者開發文檔等);進一步,找到核心資料結構,核心資料結構會直接影響代碼的品質。(曾經有人說:我不要看你的程式,讓我看一看你的資料結構!)事實上,當你完全讀懂它的資料結構時,在來閱讀源代碼,就是水到渠成的事了。

5.2 Unix/Linux下面以指令行方式運作的程式

對于一個Unix/Linux下面以指令行方式運作的程式,有這麼一些套路,大家可以在閱讀程式的時候作為參考。

a、在程式開頭,往往都是分析指令行,根據指令行參數對一些變量或者數組,或者結構指派,後面的程式就是根據這些變量來進行不同的操作。

b、分析指令行之後,進行資料準備,往往是計數器清空,結構清零等等。

c、在程式中間有一些預編譯選項,可以在makefile中找到相應部分。

d、注意程式中對于日志的處理,和調試選項打開的時候做的動作,這些對于調試程式有很大的幫助。

e、注意多線程對資料的操作。

對于開源軟體,下列方法也不錯:

先用看看實作的功能,如果自己會咋樣設計實作,檢視相關文檔,多次閱讀了解其中設計模式、架構、算法實作細節

5.3 開源軟體

開源項目已閱讀了不少,總結下來按照下面的steps來操作比較恰當:

a閱讀features。以此來搞清楚該項目有哪些特性

b思考。想想如果自己來做有這些features的項目該如何構架

c下載下傳并安裝demo或sample。通過demo或sample直覺地感受這個項目

d搜集能得到的doc,盡快地掌握如何使用這個項目

e如果有介紹項目架構的文檔,通過它了解項目的總體架構,如果沒有,通過api-doc了解源碼包的結構

f分兩遍來閱讀源碼。第一遍以應用為線索,以總體結構為基礎,閱讀在應用中使用到的類和方法,但不用過深挖掘細節,對于嵌套調用,隻用通過函數名了解最上層函數的意義,這一遍的目的在于把大緻結構了然于心。第二遍就是閱讀類和方法的實作細節,以第一遍的閱讀為基礎,帶着疑問去閱讀那些自己難以實作的子產品。

g總結。回味這個項目設計上的精妙,用到了哪些設計模式,能在哪些領域可以借鑒等等。

結束語:

當然,在這篇文章中,并沒有闡述所有的閱讀源代碼的方法和技巧,也沒有涉及面向對象程式的閱讀方法。我想把這些留到以後再做讨論。也請大家可以就這些話題展開讨論。

6、參考文獻

1)、怎樣閱讀源代碼

2)、如何閱讀源代碼

3)、如何閱讀别人的代碼

注:因一些問題有疑惑或需要經驗的指導,在網上搜尋了一些文章,經過自己的體會整理成這篇部落格,有些文章拷貝時沒記錄原作者而隻能在參考文獻中列出文章名稱,在此對原作者的開源共享精神十分感謝,也希望和大家多交流,不當之處請指教,謝謝!

轉載于:https://www.cnblogs.com/ajian005/archive/2012/08/30/2753683.html