写在前面的话: 自从我在linuxaid.com.cn上发表一些文章开始,就不断的有网友发来电子邮件,或者是就其中某些问题进行探讨,或者是查询其他文章的地址(往往这些网友看的是其他网站转载的我的文章),我很高兴自己写出的文章有这么多人回应,因为这是对我最好的赞赏,也很高兴有这么多人对我的文章感兴趣。但是常常因为工作关系。有很多邮件是询问我的其他文章在哪里能够找到,我不一定能够及时回复,也觉得回复同样的问题比较麻烦,所以在这里重复一下,我为linuxaid.com.cn写的文章都能在www.linuxaid.com.cn的应用开发栏目中找到,我的一部分文章收集在bambi.may10.ca/~ariesram/articles下面(这是一个很简陋的网页,只有文本格式的文章,也欢迎有兴趣的网友帮我设计一下网页),我的邮件地址:ariesram@linuxaid.com.cn, 或者ariesram@may10.ca。请转载文章的网站保留这一说明,欢迎网友写email给我探讨问题,虽然我不能保证能及时回复。 正文:
由于工作的关系,我常常需要读一些源代码,并在上面做一些修改并且拿来使用,或者是借鉴其中的某些部分。可以说,open source对于程序员来说,是很有意义的事情。根据我的经验,读源代码,至少有3个好处。第一个好处是可以学习到很多编程的方法,看好的源代码,对于提高自己的编程水平,比自己写源代码的帮助更大。当然不是说不用自己写,而是说,自己写代码的同时,可以从别人写的好的源代码中间学习到更多的编程方法和技巧。第二个好处是,可以提高自己把握大规模源代码的能力。一个比较大型的程序,往往都是经过了很多个版本很长的时间,有很多人参与开发,修正错误,添加功能而发展起来的。所以往往源代码的规模都比较大,少则10-100多k, 多的有好几十个MB. 在阅读大量源代码的时候,能够提高自己对大的软件的把握能力,快速了解脉络,熟悉细节,不仅仅是编程技巧,还能在程序的架构,设计方面提高自己的能力。(这里说一句题外话,>这本书相信很多人都看过,而且很多人对它推崇备至,奉为经典。现在也出了不少书,都是冠以设计模式这一名称。在书中就提到,设计模式并不是一本教材,不是教你如何去编程序,而是把平时编程中一些固定的模式记录下来,加以不断的测试和改进,分发给广大程序员的一些经验之谈。我在看这本书的时候,有一些地方一些设计方法往往让我有似曾相识的感觉,另外一些则是我以前就常常用到的。而这些经验的获得,一部分得益于自己的编码过程,另外一个很重要的来源就是阅读别人写的源代码。)阅读源代码第三个好处,就是获得一些好的思想。比如,有很多人在开始一个软件项目之前都喜欢到sourceforge.net上去找一下,是否有人以前做过相同或者相似的软件,如果有,则拿下来读一读,可以使自己对这个软件项目有更多更深的认识。我以前曾经想找一本关于如何阅读源代码的书来看看,却没有找到。相反,倒是找到了不少分析源代码的书,比如Linux kernel, Apache source, 等等。所以我想,为什么不自己来写下一些经验和大家交流呢?(当然不是写书,没有那个能力也没有那个时间。)所以在这里我准备用一个例子来写一下如何阅读源代码,分享一些经验,算是抛砖引玉吧! 我找的例子是一个统计日志的工具,webalizer. (这个工具我以前用过,似乎记得以前的版本是用perl写的,不知道现在为什么作者把它完全修改成了C,可能是为了效率,也可能根本就是我记错了。)之所以选择这个软件来作为例子,一方面是因为它是用C写的,流程比较简单,没有C++的程序那么多的枝节,而且软件功能不算复杂,代码规模不大,能够在一篇文章的篇幅里面讲完; 另外一个方面是因为恰巧前段时间我因为工作的关系把它拿来修改了一下,刚看过,还没有忘记。 :-)我采用的例子是webalizer2.01-09, 也可以到它的网[url=#]_[/url]blank href=http://www.mrunix.net/webalizer/>http://www.mrunix.net/webalizer/ 下载最新的版本。这是一个用C写的,处理文本文件(简单的说是这样,实际上它支持三种日志文本格式:CLF, FTP, SQUID), 并且用html的方式输出结果。读者可以自己去下载它的源代码包,并一边读文章,一边看程序。解压缩它的tar包(我download的是它的源代码tar包),在文件目录中看到这样的结果: $ ls aclocal.m4 dns[url=#]_[/url]resolv.c lang output.h webalizer.1 CHANGES dns[url=#]_[/url]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[url=#]_[/url]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[url=#]_[/url]prefix = ${prefix} BINDIR = ${exec[url=#]_[/url]prefix}/bin MANDIR = ${prefix}/man/man1 ETCDIR = /etc CC = gcc CFLAGS = -Wall -O2 LIBS = -lgd -lpng -lz -lm DEFS = -DETCDIR=/etc -DHAVE[url=#]_[/url]GETOPT[url=#]_[/url]H=1 -DHAVE[url=#]_[/url]MATH[url=#]_[/url]H=1 LDFLAGS= INSTALL= /usr/bin/install -c INSTALL[url=#]_[/url]PROGRAM=${INSTALL} INSTALL[url=#]_[/url]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[url=#]_[/url]resolv.o dns[url=#]_[/url]resolv.h parser.o parser.h output.o output.h graphs.o graphs.h lang.h webalizer[url=#]_[/url]lang.h $(CC) ${LDFLAGS} -o webalizer webalizer.o hashtab.o linklist.o preserv e.o parser.o output.o dns[url=#]_[/url]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[url=#]_[/url]resolv.h webalizer[url=#]_[/url]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[url=#]_[/url]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[url=#]_[/url]resolv.o: dns[url=#]_[/url]resolv.c dns[url=#]_[/url]resolv.h lang.h webalizer.h $(CC) ${CFLAGS} ${DEFS} -c dns[url=#]_[/url]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[url=#]_[/url]resolv.c graphs.h lang.h output.c parser.h webalizer.c dns[url=#]_[/url]resolv.h hashtab.c linklist.c output.h preserve.c webalizer.h graphs.c hashtab.h linklist.h parser.c preserve.h webalizer[url=#]_[/url]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 */ add[url=#]_[/url]nlist(index.,&index[url=#]_[/url]alias); 这两个函数暂时不用仔细看,后面会提到,略过。 sprintf(tmp[url=#]_[/url]buf,%s/webalizer.conf,ETCDIR); /* check for default config file */ if (!access(webalizer.conf,F[url=#]_[/url]OK)) get[url=#]_[/url]config(webalizer.conf); else if (!access(tmp[url=#]_[/url]buf,F[url=#]_[/url]OK)) get[url=#]_[/url]config(tmp[url=#]_[/url]buf); 从注释和程序本身可以看出,这是查找是否存在一个叫做webalizer.conf的配置文件,如果当前目录下有,则用get[url=#]_[/url]config来读入其中内容,如果没有,则查找ETCDIR/webalizer.conf是否存在。如果都没有,则进入下一部分。(注意:ETCDIR = @ETCDIR@在makefile中有定义) /* get command line options */ opterr = 0; /* disable parser errors */ 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[url=#]_[/url]nlist(optarg,&hidden[url=#]_[/url]agents); break; /* Hide agents */ case ’A’: ntop[url=#]_[/url]agents=atoi(optarg); break; /* Top agents */ case ’c’: get[url=#]_[/url]config(optarg); break; /* Config file */ case ’C’: ntop[url=#]_[/url]ctrys=atoi(optarg); break; /* Top countries */ case ’d’: debug[url=#]_[/url]mode=1; break; /* Debug */ case ’D’: dns[url=#]_[/url]cache=optarg; break; /* DNS Cache filename */ case ’e’: ntop[url=#]_[/url]entry=atoi(optarg); break; /* Top entry pages */ case ’E’: ntop[url=#]_[/url]exit=atoi(optarg); break; /* Top exit pages */ case ’f’: fold[url=#]_[/url]seq[url=#]_[/url]err=1; break; /* Fold sequence errs */ case ’F’: log[url=#]_[/url]type=(optarg[0]==’f’)? LOG[url=#]_[/url]FTP:(optarg[0]==’s’)? LOG[url=#]_[/url]SQUID:LOG[url=#]_[/url]CLF; break; /* define log type */ case ’g’: group[url=#]_[/url]domains=atoi(optarg); break; /* GroupDomains (0=no) */ case ’G’: hourly[url=#]_[/url]graph=0; break; /* no hourly graph */ case ’h’: print[url=#]_[/url]opts(argv[0]); break; /* help */ case ’H’: hourly[url=#]_[/url]stats=0; break; /* no hourly stats */ case ’i’: ignore[url=#]_[/url]hist=1; break; /* Ignore history */ case ’I’: add[url=#]_[/url]nlist(optarg,&index[url=#]_[/url]alias); break; /* Index alias */ case ’l’: graph[url=#]_[/url]lines=atoi(optarg); break; /* Graph Lines */ case ’L’: graph[url=#]_[/url]legend=0; break; /* Graph Legends */ case ’m’: visit[url=#]_[/url]timeout=atoi(optarg); break; /* Visit Timeout */ case ’M’: mangle[url=#]_[/url]agent=atoi(optarg); break; /* mangle user agents */ case ’n’: hname=optarg; break; /* Hostname */ case ’N’: dns[url=#]_[/url]children=atoi(optarg); break; /* # of DNS children */ case ’o’: out[url=#]_[/url]dir=optarg; break; /* Output directory */ case ’p’: incremental=1; break; /* Incremental run */ case ’P’: add[url=#]_[/url]nlist(optarg,&page[url=#]_[/url]type); break; /* page view types */ case ’q’: verbose=1; break; /* Quiet (verbose=1) */ case ’Q’: verbose=0; break; /* Really Quiet */ case ’r’: add[url=#]_[/url]nlist(optarg,&hidden[url=#]_[/url]refs); break; /* Hide referrer */ case ’R’: ntop[url=#]_[/url]refs=atoi(optarg); break; /* Top referrers */ case ’s’: add[url=#]_[/url]nlist(optarg,&hidden[url=#]_[/url]sites); break; /* Hide site */ case ’S’: ntop[url=#]_[/url]sites=atoi(optarg); break; /* Top sites */ case ’t’: msg[url=#]_[/url]title=optarg; break; /* Report title */ case ’T’: time[url=#]_[/url]me=1; break; /* TimeMe */ case ’u’: add[url=#]_[/url]nlist(optarg,&hidden[url=#]_[/url]urls); break; /* hide URL */ case ’U’: ntop[url=#]_[/url]urls=atoi(optarg); break; /* Top urls */ case ’v’: case ’V’: print[url=#]_[/url]version(); break; /* Version */ case ’x’: html[url=#]_[/url]ext=optarg; break; /* HTML file extension */ case ’X’: hide[url=#]_[/url]sites=1; break; /* Hide ind. sites */ case ’Y’: ctry[url=#]_[/url]graph=0; break; /* Supress ctry graph */ } }
if (argc - optind != 0) log[url=#]_[/url]fname = argv[optind]; if ( log[url=#]_[/url]fname && (log[url=#]_[/url]fname[0]==’-’)) log[url=#]_[/url]fname=NULL; /* force STDIN? */
/* check for gzipped file - .gz */ if (log[url=#]_[/url]fname) if (!strcmp((log[url=#]_[/url]fname+strlen(log[url=#]_[/url]fname)-3),.gz)) gz[url=#]_[/url]log=1; 这一段是分析命令行参数及开关。(getopt()的用法我在另外一篇文章中讲过,这里就不再重复了。)可以看到,这个软件虽然功能不太复杂,但是开关选项还是不少。大多数的unix/linux程序的开头部分都是这个套路,初始化配置文件,并且读入分析命令行。在这段程序中,我们需要注意一个函数:add[url=#]_[/url]nlist(). print[url=#]_[/url]opts(), get[url=#]_[/url]config()等等一看就明白,就不用多讲了。这里我们已经是第二次遇到add[url=#]_[/url]nlist这个函数了,就仔细看看吧。 $ grep add[url=#]_[/url]nlist *.h linklist.h:extern int add[url=#]_[/url]nlist(char *, NLISTPTR *); /* add list item */ 可以发现它定义在linklist.h中。 在这个h文件中,当然会有一些数据结构的定义,比如: struct nlist { char string[80]; /* list struct for HIDE items */ struct nlist *next; }; typedef struct nlist *NLISTPTR;
struct glist { char string[80]; /* list struct for GROUP items */ char name[80]; struct glist *next; }; typedef struct glist *GLISTPTR; 这是两个链表结构。还有 extern GLISTPTR group[url=#]_[/url]sites ; /* group lists */ extern GLISTPTR group[url=#]_[/url]urls ; extern GLISTPTR group[url=#]_[/url]refs ; 这些都是链表, 太多了,不用一一看得很仔细,因为目前也看不出来什么东西。当然要注意它们是extern的, 也就是说,可以在其他地方(文件)看到它们的数值(类似于C++中的public变量)。这里还定义了4个函数: extern char *isinlist(NLISTPTR, char *); /* scan list for str */ extern char *isinglist(GLISTPTR, char *); /* scan glist for str */ extern int add[url=#]_[/url]nlist(char *, NLISTPTR *); /* add list item */ extern int add[url=#]_[/url]glist(char *, GLISTPTR *); /* add group list item */ 注意,这些都是extern的,也就是说,可以在其他地方见到它们的调用(有点相当于C++中的public函数)。再来看看linklist.c, NLISTPTR new[url=#]_[/url]nlist(char *); /* new list node */ void del[url=#]_[/url]nlist(NLISTPTR *); /* del list */
GLISTPTR new[url=#]_[/url]glist(char *, char *); /* new group list node */ void del[url=#]_[/url]glist(GLISTPTR *); /* del group list */
int isinstr(char *, char *); 这5个函数是内部使用的(相当于C++中的private), 也就是说,这些函数只被isinlist(NLISTPTR, char *), isinglist(GLISTPTR, char *), add[url=#]_[/url]nlist(char *, NLISTPTR *), add[url=#]_[/url]glist(char *, GLISTPTR *)调用,而不会出现在其他地方。所以,我们先来看这几个内部函数。举例来说, add[url=#]_[/url]nlist(char *) NLISTPTR new[url=#]_[/url]nlist(char *str) { NLISTPTR newptr;
if (sizeof(newptr->string) string, str, sizeof(newptr->string));newptr->next=NULL;} return newptr; } 这个函数分配了一个struct nlist, 并且把其中的string赋值为str, next赋值为NULL.这实际上是创建了链表中的一个节点。verbose是一个全局变量,定义了输出信息的类型,如果verbose为1,则输出很详细的信息,否则输出简略信息。这是为了调试或者使用者详细了解程序情况来用的。不是重要内容,虽然我们常常可以在这个源程序的其他地方看到它。另外一个函数: void del[url=#]_[/url]nlist(NLISTPTR *list) { NLISTPTR cptr,nptr;
cptr=*list; while (cptr!=NULL) { nptr=cptr->next; free(cptr); cptr=nptr; } } 这个函数删除了一个nlist(也可能是list所指向的那一个部分开始知道链表结尾),比较简单。看完了这两个内部函数,可以来看 /*********************************************/ /* ADD[url=#]_[/url]NLIST - add item to FIFO linked list */ /*********************************************/
int add[url=#]_[/url]nlist(char *str, NLISTPTR *list) { NLISTPTR newptr,cptr,pptr;
if ( (newptr = new[url=#]_[/url]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[url=#]_[/url]glist(), del[url=#]_[/url]glist(), add[url=#]_[/url]glist()完成的功能和上述三个差不多,所不同的只是它们所处理的数据结构不同。看完了这几个函数,我们回到main程序。接下来是, /* setup our internal variables */ init[url=#]_[/url]counters(); /* initalize main counters */ 我们所阅读的这个软件是用来分析日志并且做出统计的,那么这个函数的名字已经告诉了我们,这是一个初始化计数器的函数。简略的看看吧! $ grep init[url=#]_[/url]counters *.h webalizer.h:extern void init[url=#]_[/url]counters(); 在webalizer.c中找到: void init[url=#]_[/url]counters() { int i; for (i=0;i>教材或者>等书。 if (verbose>1) { uname(&system[url=#]_[/url]info); printf(Webalizer V%s-%s (%s %s) %s , version,editlvl,system[url=#]_[/url]info.sysname, system[url=#]_[/url]info.release,language); } 这一段,是打印有关系统的信息和webalizer程序的信息(可以参考uname的函数说明)。 #ifndef USE[url=#]_[/url]DNS if (strstr(argv[0],webazolver)!=0) { printf(DNS support not present, aborting... ); exit(1); } #endif /* USE[url=#]_[/url]DNS */ 这一段,回忆我们在看README文件的时候,曾经提到过可以在编译的时候设置选项开关来设定DNS支持,在源代码中可以看到多次这样的代码段出现,如果不指定DNS支持,这些代码段则会出现(ifdef)或者不出现(ifndef).下面略过这些代码段,不再重复。 /* open log file */ if (gz[url=#]_[/url]log) { gzlog[url=#]_[/url]fp = gzopen(log[url=#]_[/url]fname,rb); if (gzlog[url=#]_[/url]fp==Z[url=#]_[/url]NULL) { /* Error: Can’t open log file ... */ fprintf(stderr, %s %s ,msg[url=#]_[/url]log[url=#]_[/url]err,log[url=#]_[/url]fname); exit(1); } } else { if (log[url=#]_[/url]fname) { log[url=#]_[/url]fp = fopen(log[url=#]_[/url]fname,r); if (log[url=#]_[/url]fp==NULL) { /* Error: Can’t open log file ... */ fprintf(stderr, %s %s ,msg[url=#]_[/url]log[url=#]_[/url]err,log[url=#]_[/url]fname); exit(1); } } } 这一段,回忆在README文件中曾经读到过,如果log文件是gzip压缩格式,则用gzopen函数打开(可以猜想gz***是一套针对gzip压缩格式的实时解压缩函数),如果不是,则用fopen打开。 /* switch directories if needed */ if (out[url=#]_[/url]dir) { if (chdir(out[url=#]_[/url]dir) != 0) { /* Error: Can’t change directory to ... */ fprintf(stderr, %s %s ,msg[url=#]_[/url]dir[url=#]_[/url]err,out[url=#]_[/url]dir); exit(1); } } 同样,回忆在README文件中读到过,如果参数行有-o out[url=#]_[/url]dir, 则将输出结果到该目录,否则,则输出到当前目录。在这一段中,如果输出目录不存在(chdir(out[url=#]_[/url]dir) != 0)则出错。 #ifdef USE[url=#]_[/url]DNS if (strstr(argv[0],webazolver)!=0) { if (!dns[url=#]_[/url]children) dns[url=#]_[/url]children=5; /* default dns children if needed */ if (!dns[url=#]_[/url]cache) { /* No cache file specified, aborting... */ fprintf(stderr,%s ,msg[url=#]_[/url]dns[url=#]_[/url]nocf); /* Must have a cache file */ exit(1); } } ...... 在上面曾经提到过,这是DNS解析的代码部分,可以略过不看,不会影响对整个程序的理解。 /* prep hostname */ if (!hname) { if (uname(&system[url=#]_[/url]info)) hname=localhost; else hname=system[url=#]_[/url]info.nodename; } 这一段继续处理参数做准备工作。如果在命令行中指定了hostname(机器名)则采用指定的名称,否则调用uname查找机器名,如果没有,则用localhost来作为机器名。(同样在README中说得很详细) /* get past history */ if (ignore[url=#]_[/url]hist) {if (verbose>1) printf(%s ,msg[url=#]_[/url]ign[url=#]_[/url]hist); } else get[url=#]_[/url]history(); 如果在命令行中指定了忽略历史文件,则不读取历史文件,否则调用get[url=#]_[/url]history()来读取历史数据。在这里,我们可以回想在README文件中同样说过这一细节,在命令行或者配置文件中都能指定这一开关。需要说明的是,我们在这里并不一定需要去看get[url=#]_[/url]history这一函数,因为从函数的名称,README文件和程序注释都能很清楚的得知这一函数的功能,不一定要去看代码。而如果要猜想的话,也可以想到,history是webalizer在上次运行的时候记录下来的一个文件,而这个文件则是去读取它,并将它的数据包括到这次的分析中去。不信,我们可以来看看。 void get[url=#]_[/url]history() { int i,numfields; FILE *hist[url=#]_[/url]fp; char buffer[BUFSIZE];
/* first initalize internal array */ for (i=0;i1) printf(%s %s ,msg[url=#]_[/url]get[url=#]_[/url]hist,hist[url=#]_[/url]fname); while ((fgets(buffer,BUFSIZE,hist[url=#]_[/url]fp)) != NULL) { i = atoi(buffer) -1; if (i>11) { if (verbose) fprintf(stderr,%s (mth=%d) ,msg[url=#]_[/url]bad[url=#]_[/url]hist,i+1); continue; }
/* month# year# requests files sites xfer firstday lastday */ numfields = sscanf(buffer,%d %d %lu %lu %lu %lf %d %d %lu %lu, &hist[url=#]_[/url]month, &hist[url=#]_[/url]year, &hist[url=#]_[/url]hit, &hist[url=#]_[/url]files, &hist[url=#]_[/url]site, &hist[url=#]_[/url]xfer, &hist[url=#]_[/url]fday, &hist[url=#]_[/url]lday, &hist[url=#]_[/url]page, &hist[url=#]_[/url]visit);
if (numfields==8) /* kludge for reading 1.20.xx history files */ { hist[url=#]_[/url]page = 0; hist[url=#]_[/url]visit = 0; } } fclose(hist[url=#]_[/url]fp); } else if (verbose>1) printf(%s ,msg[url=#]_[/url]no[url=#]_[/url]hist); } /*********************************************/ /* PUT[url=#]_[/url]HISTORY - write out history file */ /*********************************************/
void put[url=#]_[/url]history() { int i; FILE *hist[url=#]_[/url]fp;
hist[url=#]_[/url]fp = fopen(hist[url=#]_[/url]fname,w);
if (hist[url=#]_[/url]fp) { if (verbose>1) printf(%s ,msg[url=#]_[/url]put[url=#]_[/url]hist); for (i=0;i0) b--; break; case ’(’: if (q) break; p++; break; case ’)’: if (q) break; if (p>0) p--; break; } cp++; } } 从parser.h头文件中就可以看到,这个函数是一个内部函数,这个函数把一行字符串中间的空格字符用’’字符(结束字符)来代替,同时考虑了不替换在双引号,方括号,圆括号中间的空格字符以免得将一行数据错误的分隔开了。(请参考WEB日志的文件格式,可以更清楚的理解这一函数) int parse[url=#]_[/url]record[url=#]_[/url]web(char *buffer) { int size; char *cp1, *cp2, *cpx, *eob, *eos;
size = strlen(buffer); /* get length of buffer */ eob = buffer+size; /* calculate end of buffer */ fmt[url=#]_[/url]logrec(buffer); /* seperate fields with ’s */
/* HOSTNAME */ cp1 = cpx = buffer; cp2=log[url=#]_[/url]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[url=#]_[/url]big[url=#]_[/url]host); if (debug[url=#]_[/url]mode) fprintf(stderr,: %s ,cpx); else fprintf(stderr, ); } while (*cp1 != ’’) cp1++; } if (cp1 = eob) eos=eob-1;
while ( (*cp1 != ’[’) && (cp1 = eob) return 0;
/* check if oversized username */ if (*cp1 != ’[’) { if (verbose) { fprintf(stderr,%s,msg[url=#]_[/url]big[url=#]_[/url]user); if (debug[url=#]_[/url]mode) fprintf(stderr,: %s ,cpx); else fprintf(stderr, ); } while ( (*cp1 != ’[’) && (cp1 = eob) eos=eob-1;
while ( (*cp1 != ’’) && (cp1 != eos) ) *cp2++ = *cp1++; *cp2 = ’’; if (*cp1 != ’’) { if (verbose) { fprintf(stderr,%s,msg[url=#]_[/url]big[url=#]_[/url]date); if (debug[url=#]_[/url]mode) fprintf(stderr,: %s ,cpx); else fprintf(stderr, ); } while (*cp1 != ’’) cp1++; } if (cp1 = eob)) return 0;
/* HTTP request */ cpx = cp1; cp2 = log[url=#]_[/url]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[url=#]_[/url]big[url=#]_[/url]req); if (debug[url=#]_[/url]mode) fprintf(stderr,: %s ,cpx); else fprintf(stderr, ); } while (*cp1 != ’’) cp1++; } if (cp1 = eob) ) return 0;
/* response code */ log[url=#]_[/url]rec.resp[url=#]_[/url]code = atoi(cp1);
/* xfer size */ while ( (*cp1 != ’’) && (cp1 ’9’) log[url=#]_[/url]rec.xfer[url=#]_[/url]size=0; else log[url=#]_[/url]rec.xfer[url=#]_[/url]size = strtoul(cp1,NULL,10);
/* done with CLF record */ if (cp1>=eob) return 1;
while ( (*cp1 != ’’) && (*cp1 != ’ ’) && (cp1 = eob) eos = eob-1;
while ( (*cp1 != ’’) && (*cp1 != ’ ’) && (cp1 != eos) ) *cp2++ = *cp1++; *cp2 = ’’; if (*cp1 != ’’) { if (verbose) { fprintf(stderr,%s,msg[url=#]_[/url]big[url=#]_[/url]ref); if (debug[url=#]_[/url]mode) fprintf(stderr,: %s ,cpx); else fprintf(stderr, ); } while (*cp1 != ’’) cp1++; } if (cp1 = eob) eos = eob-1;
while ( (*cp1 != ’’) && (cp1 != eos) ) *cp2++ = *cp1++; *cp2 = ’’;
return 1; /* maybe a valid record, return with TRUE */ } 该函数,一次读入一行(其实是一段日志数据中间的一个域,因为该行数据已经被fmt[url=#]_[/url]logrec分开成多行数据了。根据CLF中的定义,检查该数据并将其拷贝到log[url=#]_[/url]rec结构中去,如果检查该数据有效,则返回1。回到主程序, /* convert month name to lowercase */ for (i=4;i=12)||(rec[url=#]_[/url]min>59)||(rec[url=#]_[/url]sec>59)||(rec[url=#]_[/url]year=’A’) && (*cp1string))!=NULL) { if ((cp1==log[url=#]_[/url]rec.url)||(*(cp1-1)==’/’)) { *cp1=’’; if (log[url=#]_[/url]rec.url[0]==’’) { log[url=#]_[/url]rec.url[0]=’/’; log[url=#]_[/url]rec.url[1]=’’; } break; } } lptr=lptr->next; }
/* unescape referrer */ unescape(log[url=#]_[/url]rec.refer); ...... 这一段,做了一些URL字符串中的字符转换工作,很长,我个人认为为了程序的模块化,结构化和可复用性,应该将这一段代码改为函数,避免主程序体太长,造成可读性不强和没有移植性,和不够结构化。跳过这一段乏味的代码,进入到下面一个部分---后处理。 if (gz[url=#]_[/url]log) gzclose(gzlog[url=#]_[/url]fp); else if (log[url=#]_[/url]fname) fclose(log[url=#]_[/url]fp);
if (good[url=#]_[/url]rec) /* were any good records? */ { tm[url=#]_[/url]site[cur[url=#]_[/url]day-1]=dt[url=#]_[/url]site; /* If yes, clean up a bit */ tm[url=#]_[/url]visit[cur[url=#]_[/url]day-1]=tot[url=#]_[/url]visit(sd[url=#]_[/url]htab); t[url=#]_[/url]visit=tot[url=#]_[/url]visit(sm[url=#]_[/url]htab); if (ht[url=#]_[/url]hit > mh[url=#]_[/url]hit) mh[url=#]_[/url]hit = ht[url=#]_[/url]hit;
if (total[url=#]_[/url]rec > (total[url=#]_[/url]ignore+total[url=#]_[/url]bad)) /* did we process any? */ { if (incremental) { if (save[url=#]_[/url]state()) /* incremental stuff */ { /* Error: Unable to save current run data */ if (verbose) fprintf(stderr,%s ,msg[url=#]_[/url]data[url=#]_[/url]err); unlink(state[url=#]_[/url]fname); } } month[url=#]_[/url]update[url=#]_[/url]exit(rec[url=#]_[/url]tstamp); /* calculate exit pages */ write[url=#]_[/url]month[url=#]_[/url]html(); /* write monthly HTML file */ write[url=#]_[/url]main[url=#]_[/url]index(); /* write main HTML file */ put[url=#]_[/url]history(); /* write history */ }
end[url=#]_[/url]time = times(&mytms); /* display timing totals? */ if (time[url=#]_[/url]me’ ’(verbose>1)) { printf(%lu %s ,total[url=#]_[/url]rec, msg[url=#]_[/url]records); if (total[url=#]_[/url]ignore) { printf((%lu %s,total[url=#]_[/url]ignore,msg[url=#]_[/url]ignored); if (total[url=#]_[/url]bad) printf(, %lu %s) ,total[url=#]_[/url]bad,msg[url=#]_[/url]bad); else printf() ); } else if (total[url=#]_[/url]bad) printf((%lu %s) ,total[url=#]_[/url]bad,msg[url=#]_[/url]bad);
/* get processing time (end-start) */ temp[url=#]_[/url]time = (float)(end[url=#]_[/url]time-start[url=#]_[/url]time)/CLK[url=#]_[/url]TCK; printf(%s %.2f %s, msg[url=#]_[/url]in, temp[url=#]_[/url]time, msg[url=#]_[/url]seconds);
/* calculate records per second */ if (temp[url=#]_[/url]time) i=( (int)( (float)total[url=#]_[/url]rec/temp[url=#]_[/url]time ) ); else i=0;
if ( (i>0) && (i<=total[url=#]_[/url]rec) ) printf(, %d/sec , i); else printf( ); } 这一段,做了一些后期的处理。接下来的部分,我想在本文中略过,留给感兴趣的读者自己去做分析。原因有两点: 1、这个程序在前面结构化比较强,而到了结构上后面有些乱,虽然代码效率还是比较高,但是可重用性不够强, 限于篇幅,我就不再一一解释了。 2、前面分析程序过程中,也对后面的代码做了一些预测和估计,也略微涉及到了后面的代码,而且读者可以根据上面提到的原则来自己分析代码,也作为一个实践吧。
最后,对于在这篇文章中提到的分析源代码程序的一些方法做一下小结,以作为本文的结束。 分析一个源代码,一个有效的方法是: 1、阅读源代码的说明文档,比如本例中的README, 作者写的非常的详细,仔细读过之后,在阅读程序的时候往往能够从README文件中找到相应的说明,从而简化了源程序的阅读工作。 2、如果源代码有文档目录,一般为doc或者docs, 最好也在阅读源程序之前仔细阅读,因为这些文档同样起了很好的说明注释作用。 3、从makefile文件入手,分析源代码的层次结构,找出哪个是主程序,哪些是函数包。这对于快速把握程序结构有很大帮助。 4、从main函数入手,一步一步往下阅读,遇到可以猜测出意思来的简单的函数,可以跳过。但是一定要注意程序中使用的全局变量(如果是C程序),可以把关键的数据结构说明拷贝到一个文本编辑器中以便随时查找。 5、分析函数包(针对C程序),要注意哪些是全局函数,哪些是内部使用的函数,注意extern关键字。对于变量,也需要同样注意。先分析清楚内部函数,再来分析外部函数,因为内部函数肯定是在外部函数中被调用的。 6、需要说明的是数据结构的重要性:对于一个C程序来说,所有的函数都是在操作同一些数据,而由于没有较好的封装性,这些数据可能出现在程序的任何地方,被任何函数修改,所以一定要注意这些数据的定义和意义,也要注意是哪些函数在对它们进行操作,做了哪些改变。 7、在阅读程序的同时,最好能够把程序存入到cvs之类的版本控制器中去,在需要的时候可以对源代码做一些修改试验,因为动手修改是比仅仅是阅读要好得多的读程序的方法。在你修改运行程序的时候,可以从cvs中把原来的代码调出来与你改动的部分进行比较(diff命令), 可以看出一些源代码的优缺点并且能够实际的练习自己的编程技术。 8、阅读程序的同时,要注意一些小工具的使用,能够提高速度,比如vi中的查找功能,模式匹配查找,做标记,还有grep,find这两个最强大最常用的文本搜索工具的使用。
对于一个Unix/Linux下面以命令行方式运行的程序,有这么一些套路,大家可以在阅读程序的时候作为参考。 1、在程序开头,往往都是分析命令行,根据命令行参数对一些变量或者数组,或者结构赋值,后面的程序就是根据这些变量来进行不同的操作。 2、分析命令行之后,进行数据准备,往往是计数器清空,结构清零等等。 3、在程序中间有一些预编译选项,可以在makefile中找到相应部分。 4、注意程序中对于日志的处理,和调试选项打开的时候做的动作,这些对于调试程序有很大的帮助。 5、注意多线程对数据的操作。(这在本例中没有涉及) 结束语: 当然,在这篇文章中,并没有阐述所有的阅读源代码的方法和技巧,也没有涉及任何辅助工具(除了简单的文本编辑器),也没有涉及面向对象程序的阅读方法。我想把这些留到以后再做讨论。也请大家可以就这些话题展开讨论。 【转自世纪安全网 http://www.21safe.com】
|