编写 UNIX® 系统程序充满乐趣,并且具有教育意义。使用 UNIX strace 工具和 GDB(GNU 项目调试工具),您可以真正地深入研究系统的功能,并了解组成这些功能的各种各样的程序。同时使用这两种工具,能够在查看 UNIX 计算机底层信息 的时候,给您带来更好的体验。
UNIX 家族总是为用户提供了丰富的工具。UNIX 是一个工具财宝箱,有了这些工具,您不仅可以完成具有创造性的工作,还可以在深入研究该操作系统的同时得到教育和娱乐。strace(用来跟踪任何程序的系统调用)和 GDB 调试工具(用来在受控的环境中运行程序的功能齐全的调试工具)是实现这个目标的两个有价值的工具。
UNIX 的设计由大量的函数调用(称为系统调用)组成,其中包括一些简单的任务,如在屏幕上显示字符串来设置任务优先级。所有的 UNIX 程序都是通过调用操作系统提供的这些底层服务来完成它们的任务,使用 strace 工具,您可以清楚地看到这些调用过程及其使用的参数。通过这种方式,您可以操作这些程序,以了解它们与操作系统之间的底层交互。
开始游戏
让我们以一个简单的 UNIX 命令 pwd
作为开始,然后更深入地研究该命令在完成其任务的过程中进行了哪些工作。启动 xterm 以创建一个进行实验的受控环境,然后输入下面的命令:
$ pwd
这个 pwd
命令显示了当前的工作目录。在我的计算机上,当时的输出是:
/home/bill/
一个如此简单的函数掩饰了该命令底层的复杂性(顺便说一下,所有的计算机程序都是这样的)。要真正地了解其复杂性,请使用 strace 工具再次运行 pwd
命令:
$ strace pwd
通过该命令,您可以看到,在显示和列举当前工作目录的过程中,UNIX 计算机执行了相当多的操作(请参见清单 1)。
清单 1:strace pwd 命令的输出
execve("/bin/pwd", ["pwd"], [/* 39 vars */]) = 0 uname({sys="Linux", node="sammy", ...}) = 0 brk(0) = 0x804c000 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4001... . . . fstat64(3, {st_mode=S_IFREG|0644, st_size=115031, ...}) = 0 old_mmap(NULL, 115031, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000 close(3) = 0 open("/lib/tls/libc.so.6", O_RDONLY) = 3 read(3, "177ELF111 3 3 1 360U1"..., 1024) = 1024 fstat64(3, {st_mode=S_IFREG|0755, st_size=1547996, ...}) = 0 old_mmap(0x42000000, 1257224, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x42000000 mprotect(0x4212e000, 20232, PROT_NONE) = 0 old_mmap(0x4212e000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x12e000)... old_mmap(0x42131000, 7944, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,... close(3) = 0 set_thread_area({entry_number:-1 -> 6, base_addr:0x40016ac0, limit:1048575, seg_32bit... munmap(0x40017000, 115031) = 0 brk(0) = 0x804c000 brk(0x804d000) = 0x804d000 brk(0) = 0x804d000 open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=30301680, ...}) = 0 mmap2(NULL, 2097152, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000 close(3) = 0 brk(0) = 0x804d000 brk(0x804e000) = 0x804e000 getcwd("/home/bill", 4096) = 11 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 6), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4021700... write(1, "/home/bill ", 11/home/bill ) = 11 munmap(0x40217000, 4096) = 0 exit_group(0) = ?
UNIX 系统调用的具体细节
关于检索和显示当前工作目录所需的所有系统调用的详细细节,已经超出了本文的讨论范围,但我会介绍如何获取这些信息。清单 1 中的每一行都以类似 C
的格式,清楚地说明了一项系统调用及其参数,这也是 C
程序员希望看到的。Strace 同样以这种方式显示这些调用,不管在实际创建该程序时使用的是何种编程语言。
如果要了解清单 1 中的所有细节信息,对于所有这些系统调用,UNIX 为您提供了大量的文档。清单 1 中“最重要”的函数是 getcwd()
函数,它表示获取当前工作目录。当前的 xterm 显示了 strace pwd
的输出,同时再启动另一个 xterm 并输入下面的命令以查看 UNIX 对该函数的显示:
$ man getcwd
您所看到的应该是 getcwd()
函数完整的清单以及这个重要的 C
函数需要的和返回的参数清单。同样地,您可以输入 man brk
或 man fstat64
等等。通常,UNIX 系统通过文档对这些系统函数进行了详细的说明,如果花些时间仔细地研究它们,您将逐渐地了解到 UNIX 的功能是多么的强大,以及学习这些底层系统细节是多么的容易。在所有的操作系统中,UNIX 最善于帮助您理解其底层的处理过程。
观察 nweb
对于下面几个步骤,您需要使用更庞大且更复杂的程序,而不是像 pwd
这样简单的 UNIX 命令。简单的超文本传输协议 (HTTP) 服务器,如 nweb,是非常适合的。当您在 Internet 上冲浪 的时候,HTTP 服务器侦听浏览器请求,然后通过发送所请求的对象,如 Web 页面和图形文件,以此响应浏览器的请求。
下载并安装 nweb,该软件由 IBM developerWorks 投稿作家 Nigel Griffiths 编写。 (请参阅参考资料部分提供的 Nigel 的文章“nweb: ?"a tiny, safe Web server (static pages only)”(developerWorks,2004 年 6 月)的链接。)
下载 es-nweb.zip 到 $HOME/downloads 目录,然后输入清单 2 中所示的简单命令,以提取、编译并启动该程序:
注意:我假设您需要为 Linux® 工作站编译这个程序。如果实际情况并非如此,那么有关在其他的 UNIX 操作系统上对该程序进行编译的详细信息,请阅读这篇 nweb 文章。
清单 2. 用于提取、编译和启动 nweb 的命令
$ cd src $ mkdir nweb $ cd nweb $ unzip $HOME/downloads/es-nweb.zip $ gcc -ggdb -O -DLINUX nweb.c -o nweb $ ./nweb 9090 $HOME/src/nweb &
注意:清单 2 中的 -ggdb
选项和 Nigel 的文章中的内容有些不同,该选项用于告诉 GCC 编译器对该程序进行优化,以便使用 GDB 调试工具对其进行调试,您将在以后用到该调试工具。
接下来,要确认 nweb 服务器已经运行,可以使用清单 3 中所示的 ps
命令来对它进行检查。
清单 3. ps 命令
$ ps PID TTY TIME CMD 2913 pts/5 00:00:00 bash 4009 pts/5 00:00:00 nweb 4011 pts/5 00:00:00 ps
最后,要确认 nweb 确实正在运行并且状态正常,可以在您的计算机上启动一个 Web 浏览器并在地址栏中输入 http://localhost:9090
。
针对 nweb 使用 strace
现在,让我们来进行一些有趣的工作。启动另一个 xterm,然后使用 strace 来跟踪正在运行的 nweb 服务器。要完成该任务,您必须清楚该程序的进程 ID,并且必须具有适当的权限。您仅仅可以看到一组特定的系统调用,即那些与网络相关的系统调用。输入清单 4 第一行所示的命令作为开始,其中使用了前面显示的 nweb 的进程 ID。您应该看到如下的输出(清单 4 中的第二行)。
清单 4. 开始对 nweb 进行跟踪
$ strace -e trace=network -p 4009 accept(0,
请注意,在调用网络 accept()
函数的过程中停止了跟踪操作。在浏览器中刷新几次 http://localhost:9090
页面,请注意每次刷新该页面时 strace 的显示。这是不是很棒呢?您所看到的是,当 Web 浏览器调用 HTTP 服务器 (nweb
) 时,服务器所进行的底层网络调用。简单地说,nweb
正在接受 来自您的浏览器的调用。
您可以在运行 strace 的具有窗口焦点的 xterm 中按下 Ctrl+C 以停止对网络调用的跟踪。
再来研究 GDB 调试工具
正如您所看到的,strace 可以作为了解用户程序如何通过某些系统调用与操作系统进行交互的一个很好的程序。GDB 调试工具本身也可以附加于一个正在运行的进程,并帮助您进行更深入的研究。
GDB 调试工具非常有用,Internet 上提供了大量的有关该工具的可用信息。通常,调试工具是很有价值的工具,并且任何负责开发和维护计算机系统的人员应该了解如何使用它们。因此,在 nweb 运行于另一个 xterm 会话的同时,按下 Ctrl+C 停止 strace,然后输入清单 5 中所示的命令启动 GDB 调试工具。
清单 5. 启动 GDB 调试工具
$ gdb --quiet (gdb) attach 4009 Attaching to process 4009 Reading symbols from /home/bill/src/nweb/nweb...done. Reading symbols from /lib/tls/libc.so.6...done. Loaded symbols for /lib/tls/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 0xffffe410 in ?? () (gdb)
-quiet
选项告诉 GDB 调试工具仅显示其提示符,而不要显示所有其他的启动信息。如果需要显示额外的文本信息,可以去掉 -quiet
选项。
attach 4009
命令启动对当前正在运行的 nweb 服务器的调试工作,并且 GDB 调试工具通过读取有关该进程的所有的符号信息来做出同样方式的响应。下一步,使用 info
命令来列举您所研究的程序的相关信息(请参见清单 6)。
清单 6. info 命令列出程序信息
(gdb) info proc process 4009 cmdline = './nweb' cwd = '/home/bill/src/nweb' exe = '/home/bill/src/nweb/nweb' (gdb)
info
命令(请参见清单 7)的另一个有用的变种是 info functions
,然而,函数的列表可能很长。
清单 7. info functions 命令得到的函数列表
(gdb) info functions All defined functions: File nweb.c: void log(int, char *, char *, int); int main(int, char **); void web(int, int); File __finite: int __finite(); . . . (gdb)
因为使用 -ggdb
选项对 nweb 程序进行了编译,所以可执行文件中包含了大量的调试信息,允许该调试工具查看文件中列举的已定义的函数,如清单 7 所示。
list 和 disassemble 命令
有两个重要的 GDB 调试工具命令,它们分别是 list
和 disassemble
。通过使用清单 8 中所示的代码尝试使用这些命令。
清单 8. list 命令
(gdb) list main 121 exit(1); 122 } 123 124 125 main(int argc, char **argv) 126 { 127 int i, port, pid, listenfd, socketfd, hit; 128 size_t length; 129 char *str; 130 static struct sockaddr_in cli_addr; /* static = initialised to zeros */ (gdb) 131 static struct sockaddr_in serv_addr; /* static = initialised to zeros */ 132 133 if( argc < 3 || argc > 3 || !strcmp(argv[1], "-?") ) { 134 (void)printf("hint: nweb Port-Number Top-Directory " 135 " nweb is a small and very safe mini web server " 136 " nweb only servers out file/web pages with extensions named below " 137 " and only from the named directory or its sub-directories. " 138 " There is no fancy features = safe and secure. " 139 " Example: nweb 8181 /home/nwebdir & " 140 " Only Supports:");
正如您所看到的,list
命令以源文件的形式列出了正在运行的程序,并标注了相应的行号。按下 Return 键(如第 130 行和第 131 行之间所示)以接着上次的列表继续进行列举。现在,尝试使用 disassemble
命令,可以缩写为 disass
(请参见清单 9)。
清单 9. disassemble 命令
(gdb) disass main Dump of assembler code for function main: 0x08048ba2 <main+0>: push ebp 0x08048ba3 <main+1>: mov ebp,esp 0x08048ba5 <main+3>: push edi 0x08048ba6 <main+4>: push esi 0x08048ba7 <main+5>: push ebx 0x08048ba8 <main+6>: sub esp,0xc 0x08048bab <main+9>: mov ebx,DWORD PTR [ebp+12] 0x08048bae <main+12>: and esp,0xfffffff0 . . . 0x08048c01 <main+95>: call 0x8048664 <printf> 0x08048c06 <main+100>: add esp,0x10 0x08048c09 <main+103>: inc esi 0x08048c0a <main+104>: cmp DWORD PTR [ebx+esi],0x0 ---Type <return> to continue, or q <return> to quit---
反汇编清单显示了该 main
函数的汇编语言清单。在本示例中,汇编代码指示出运行该代码的计算机使用的是 Intel® Pentium® 处理器。如果该程序运行于不同类型的处理器上,如基于 IBM Power PC® 的计算机,那么您的代码看上去将有很大的区别。
在其运行的过程中进行监视
因为您所监视的是一个正在运行的程序,所以可以设置相应的断点,然后在它响应浏览器请求并向提出请求的浏览器传输 .html 和 .jpg 文件的同时,对该程序进行监视。清单 10 介绍了如何完成该任务。
清单 10. 设置断点
(gdb) break 188 Breakpoint 1 at 0x8048e70: file nweb.c, line 188. (gdb) commands 1 Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >continue >end (gdb) c Continuing.
此时,GDB 调试工具已设置为在 nweb 服务器接受 浏览器请求处进行中断,该调试工具将仅仅显示相应的请求并继续处理其他的请求,而不会中断正在运行的程序。刷新几次浏览器中的 http://localhost:9090/
页面,可以观察到,GDB 调试工具显示了断点并继续运行。
在刷新浏览器页面的同时,您应该看到如清单 11 所示的断点信息,在 GDB 调试工具 xterm 中滚动输出。与 strace 相同,您可以按下 Ctrl+C 来停止对 nweb 服务器的调试。在停止了跟踪操作之后,您可以输入 quit
命令以退出 GDB 调试工具。
清单 11. GDB 调试工具 xterm 中的断点信息
Breakpoint 1, main (argc=3, argv=0x1) at nweb.c:188 188 if((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0) Breakpoint 1, main (argc=3, argv=0x1) at nweb.c:188 188 if((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0) Breakpoint 1, main (argc=3, argv=0x1) at nweb.c:188 188 if((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0) Breakpoint 1, main (argc=3, argv=0x1) at nweb.c:188 188 if((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0) Program received signal SIGINT, Interrupt. 0xffffe410 in ?? () (gdb) quit The program is running. Quit anyway (and detach it)? (y or n) y Detaching from program: /home/bill/src/nweb/nweb, process 4009 $
请注意,您正告诉 GDB 调试工具停止对一个仍在内存中活动的程序的调试。即使是在退出了调试工具之后,您还可以刷新浏览器页面,并将看到 nweb 仍在运行。可以输入 kill 4009
命令来停止该程序,或者在您退出会话时,该页面将会消失。
和平常一样,您可以通过其 man 和 info 页面来了解各种各样的工具,如 strace 和 GDB 调试工具。请确保使用 UNIX 为您提供的这些工具!
了解尽可能多的信息
了解关于您所使用的计算机的尽可能多的信息,绝不是件坏事,并且还可以从这个过程中获得乐趣。实际上,UNIX 通过提供各种工具,如 strace 和 GDB 调试工具以及包含在相应的 man 和 info 页面中的大量的信息,鼓励您对系统进行研究和学习。计算机是人类智慧的延伸,并且我们对其了解得越多,它们将变得越有用。