保持我们最初的理想,当面对无数歧路
保持我们最初的理想,当面对无数歧路
本文以实例非常粗浅地介绍Linux下的工具 find, grep, tar 的使用,还有正则表达式。侯捷先生用这些工具帮助写了<STL源码剖析>和<深入浅出MFC>,我们将用这些工具帮助编译一个叫做calltree的小工具。
本文还讨论了如何保持最初的理想。
最近的照片,大月亮有环形山的,长春暴雨的,在这里[http://www.douban.com/photos/album/105812475/]。
0. calltee
calltree是个静态分析C代码中的函数调用关系的工具,用来画 call graph。为了找个工具帮助画几个模块间的依赖关系,找到了它。刚刚这句你看不懂也没关系,你知道calltree是个程序就行了。事实上,大部分文章中的大部分话你看不懂,都没关系。直到因为你不知道这段话所提供的事实影响了你的阅读,那才有关系了。
1. 多歧路
在你读某段话时,如果没有完全读懂,那么宇宙会因此而分裂成为无数条分支,每条分支对应这段话的一种含义的可能。但是,这没有什么影响,你大可以把这些分支都忘了。不过,当你继续读,后面某一段可能会用到刚刚那句话提供的信息了,此时,因为你的注视,所有那些分支宇宙就坍缩为唯一的一个。如果你不知道那句话的确切含义,随着一步步深入,分支就会呈爆炸状迅速增长。
初学者在涉足某个领域的时候,所面临的正是这个问题--在每一步中,可能性都太多了,稍有不慎就拐进了一个死胡同。而且越走越远,接下来全是无用功。正是因为这个,我们要在每一步中检查自己当前的状态,而不能等到最后。
经常有同学问到,为什么我这段程序编译通不过呢?你要来他的程序一看,好几百行,编译一下,错误在第十几行。如果前面的你都没有通过,为什么要继续向下走呢,后面那好几百行都是怎么写出来的啊。
这是理工类的一个重要特点,如果当前的一步没有处理好,未来很难弥补。事实上,能在未来弥补现在的错误,是个巨牛的活儿。一般地,如果你幼稚到现在会犯这样的错误,基本可以断定,你不具备在未来弥补的能力。
所以写代码的时候,如果你承认自己不够牛 (通常如此),那么你应该写一会代码就编译一下,让编译器帮助检查错误。有同学说,我还得一阵才能写到下一个"括回}"呢。你应该在写"{"的下一个字符就写"}",然后再写中间的部分。步子要保证足够小,能在足够短的时间内编译,从而得到检验。如果航海技术不够发达,就应该珍惜生命,不远离陆地,在能看到海岸的地方航行。铁子同学,你是不是想起了《文明II》?
为了回避失败感,初学者们经常想要计划得非常周全,按他们的话说,"我想等想好了的再动手"。他们太骄傲了,计划,这是世界上最难的事实,远远比动手再失败要难很多个数量级。尤其是,你计划这件事情的时候,你尚不知道未来会遇到哪些问题--如果你知道,那么你就是这个并非陌生的领域的专家了。所谓初学者,就是两眼一抹黑地往前走的人。你不走,就永远也不会熟悉这个领域。
2. 解压
你说什么,等专家引领?专家哪有那个功夫。
比如安装calltree。你随便搜索一下,就有人告诉你从哪个网址下载。你下载完了,文件名是 "calltree-2.3.tar.bz2"。
你可能认识不少扩展名,认识 tar.gz,认识 zip,认识 rar,甚至还认识7z。可是bz2是什么?
大家一般的选择:1.找个支持bz2解压的工具,2.搜索一下,3.QQ上找个好友,"bz2怎么解压?" 最后这种人后来就被QQ好友拉黑了。有位大侠在网上提到: tar jxvf calltree-2.3.tar.bz2。这也可能是你的QQ好友告诉你的。在命令行下跑一遍,你得到了 calltree-2.3 目录。
看起来解压完成了,很多人以为这一节就结束了。不过,在这个时刻,你可能没有注意到,宇宙分裂了。 tar jxvf 是什么意思,你没有注意,它存在各种可能,等到你再遇到需要它的时候,世界将坍缩为"你还是不会"。
这段话的意思,你当然可以搜索一下,或者QQ (你永远可以QQ别人,因此以后这条选择略掉),不过Linux/Unix的习惯是手册里有主要的信息。
我们执行:
$ tar --help | grep \-j
-j, --bzip2 filter the archive through bzip2
然后我们就明白了,j的作用是"filter the archive through bzip2",原来是用bzip2解压缩的。我们的文档刚好是bz2扩展名,合理合理。
"tar --help"会输出tar的所有帮助。有不少人习惯于把 tar --help 全输出来,然后用眼睛找想要的信息。
如果你知道自己想要什么,那么可以直接告诉计算机,而不用自己再亲力亲为。对大量数据的重复工作,计算机比我们擅长,只要我们指令明确。grep就是干这个用的。grep是一个过滤器,它把符合条件的留下来。"-j"前面的那个"\",是因为"-"是个特殊字符,它的后面是grep的参数,"\"的作用是避免grep把"-j"作为参数,而是作为过滤条件。
如果你不知道自己想要什么,就只能在歧路中摸索;如果你知道自己的目标,那么,就可以直接命中,无需犹豫。大毅同学问我,为什么要看康德呢。我说,因为我想要过毫不犹豫的生活。道理是一样的。
初学者和专家最大的区别在于,专家不必尝试,他知道应该如何做,知道这样做的结果如何,甚至还知道误差和出错的概率有多大,甚至还知道如果出错如何改善。这些"如何"的难度,从前向后,越来越大。
2. make失败
解压以后,按Linux下从源代码编译安装,惯例就是进目录,读INSTALL文件;或者不读,直接跑configure或者make。
我make,然后出了一大堆错误。有的同学可能就要一行行找错误了,用眼睛。这很难,因为make的输出信息长达511行。这个数据也不是我一行行查的,而是:
$ make | wc -l
511
你要到哪里找错误呢?先想想错误信息的特征是什么。没错,"error"。所以,我们再用 grep。
$ make | grep -i "error"
../include/schily.h:186: error: conflicting types for ‘getline’
make[2]: *** [OBJ/i686-linux-cc/avoffset.o] Error 1
make[1]: *** [all] Error 2
../include/schily.h:110: error: conflicting types for ‘fexecve’
../include/schily.h:186: error: conflicting types for ‘getline’
make[2]: *** [OBJ/i686-linux-cc/cvmod.o] Error 1
make[1]: *** [all] Error 2
../include/schily.h:110: error: conflicting types for ‘fexecve’
../include/schily.h:186: error: conflicting types for ‘getline’
make[1]: *** [OBJ/i686-linux-cc/calltree.o] Error 1
~/Downloads/calltree-2.3 $
这错误有点让人感到莫名其妙,因为getline和fexecve是两个著名的函数,你甚至能man出来它们,是posix的一部分。又或者你根本不知道它们是什么也行,你就会觉得,"这源代码不是应该一编译就通过吗,搞什么..."
3. 修改源代码
搜索一下,我找到了EnuLL大侠的博客[http://www.3null.org/?p=439],他说,"在比较新的内核(glibc库)上编译,会出现编译不过的问题。具体现象大概像下面这个样子..." 这个样子,就是你上面看到的样子。
然后EnuLL大侠说,"具体解决办法有两种,一种就是给glibc打patch,另一种就麻烦点,把冲突的命名手动都改了。"
关于解决方案,这句话到这里就完了,初学者就也完了。打patch,怎么打啊,手动改命名,怎么改啊。此处说来话长,我略过吧。总之,我们就知道,应该这样手动命名:把calltree源代码里所有声明、定义、调用这两个函数的地方,都把这两个函数改个名字。只要新的名字不再与posix的重名,那就没冲突了,编译就能通过了。
好了,目标明确了,你打算怎么动手?
有同学可能又想到了,一个个打开文件,然后有可能一行行看,用眼睛找到它们。或者,如果你有个够好的IDE,能全工程全文搜索--但是有时候你正用的项目恰恰不能在这个IDE上打开。再好的IDE和恒久远的钻石都是浮云,只有纯文本才是永恒的。
我们这样:
$ find . -name "*.[c|h]" -exec grep getline -nH {} \;
./include/schily.h:117:extern
int
fgetline __PR((FILE *, char *, int));
./include/schily.h:186:extern
int
getline __PR((char *, int));
./calltree/calltree.c:852:
while (fgetline(fp, fname, sizeof(fname)) >= 0)
./libschily/stdio/fgetline.c:1:/* @(#)fgetline.c
1.6 03/06/15 Copyright 1986, 1996-2003 J. Schilling */
./libschily/stdio/fgetline.c:29:fgetline(f, buf, len)
./libschily/stdio/fgetline.c:67:getline(buf, len)
./libschily/stdio/fgetline.c:71:
return (fgetline(stdin, buf, len));
从输出结果中能够看到:
(1) 在 include/schily.h的186行,是getline的声明;
(2) libschily/stdio/fgetline.c第67行,也提到了getline,查看代码以后发现,这是getline的定义;
(3) 奇怪,我没有找到调用getline的地方,不调用为什么要声明和定义。
(4) 那些fgetline是误伤的。
再来看产生这个结果的命令,"find . -name "*.[c|h]" -exec grep getline -nH {} \;"。有点乱,我们一段一段看。
find是个命令,找符合条件的文件。
(1) "find . -name"的意思是在当前目录"."下,找文件名符合条件的。
(2) "*.[c|h]"就是文件名要符合的条件。这是个正则表达式,意思是文件名任意("*"),扩展名(从"."开始)是"c"或者是"h"。我对正则表达式的运用还不是那么本能,所以实践操作中,没有先想到用正则表达式,而是先找了文件名为"*.c"的,然后找了文件名为"*.h"的。这也是典型的幼推的初学者的表现--不能一次性全部地了解自己的想法。
(3) "-exec ... {}... \;"。"-exec"的作用是,对find到的符合条件的文件,都施加一个动作 (命令) ,在本例中,这个动作就是grep。"{}"就代表找到的文件在这个命令中的位置。";"的作用是标识"-exec"的命令部分结束了,"\"是避免shell把";"当作find这个命令和下一个命令分隔符。wikipedia说得更明白一些,"The semicolon (backslashed to avoid the shell interpreting it as a command separator) indicates the end of the command."
如果找到的文件是 gold.c,那么"-exec"以后的部分相当于:grep getline -nH gold.c。注意,gold.c刚好取代了"{}"。
(4)"grep getline -nH {}"。grep是按正则表达式 (或退化为字符串)在文本中找到符合条件的行。"getline"是要匹配的正则表达式,在本例中,是我们要找到的那个函数。"{}"的意思上面说过了,代表find找到的文件,"-nH"的意思是打印出文件名和行号,你可以这样知道:
$ grep --help | grep \-H
-H, --with-filename print the filename for each match
$ grep --help | grep \-n,
-n, --line-number print line number with output lines
(5) 以上连起来,就是 "find . -name "*.[c|h]" -exec grep getline -nH {} \;"。
含义是:在当前目录下,找到所有的*.c和*.h;然后在这些文本文件的正文中,找到存在"getline"字样的地方。
然后呢?然后我们用文本编辑器打开那几个文件,找到对应的地方。我把getline都改为 getline_calltree。
然后呢?然后把 fexecve 也都改了,我改成 fexecve_calltree。
4. 可以更自动化
本例中,要改的地方不过五六处而已。如果要改的地方特别多呢?有个工具,叫做sed。
5. 再make
再make,通过了。为什么知道通过了呢?
我们执行
$ make | grep -i "error"
没有输出。没有消息就是好消息。
在".../OBJ/"下,我们得到了 calltree 文件,是可执行的。
6. 回顾歧路
每一个步骤,如果我们不选用自动化的 (批量的) 工具,我们都可能更倾向于陷入细节当中,迷失方向。尤其当我们是初学者的时候,我们尚不知道眼睛该往哪里看。这些细节具有多种可能性,很多步骤的多种可能性叠加起来,我们的面前摆着的就是一张非常复杂的网,每个节点都通向好几个地方。
我们总觉得眼前的就是最重要的,"活在当下"。就这样,我们被眼前的细节引导向完全不同的没有预期的方向;就这样,我们忘掉了最初的理想。
--------------------
博客会手工同步到以下地址:
[http://giftdotyoung.blogspot.com]
[http://blog.csdn.net/younggift]
。