我的大部分工作都涉及到部署软件系统,这意味着我需要花费很多时间来解决以下问题:
这个软件可以在原开发者的机器上工作,但是为什么不能在我这里运行?
这个软件昨天可以在我的机器上工作,但是为什么今天就不行?
所以,在软件部署过程中,我没有使用传统的调试工具(例如gdb),而是选择了其它工具进行调试。我最喜欢的用来解决“为什么这个软件无法在这台机器上运行?”这类问题的工具就是strace。
什么是strace?strace是一个用来“追踪系统调用”的工具。它主要是一个Linux工具,但是你也可以在其它系统上使用类似的工具(例如DTrace和ktrace)。
它的基本用法非常简单。只需要在strace后面跟上你需要运行的命令,它就会显示出该命令触发的所有系统调用(你可能需要先安装好strace):
$straceechoHelloSniplotsofstuffwrite(1,"Hello\n",6)=6close(1)=0close(2)=0exit_group(0)=?+++exitedwith0+++
$straceechoHelloSniplotsofstuffwrite(1,"Hello\n",6)=6close(1)=0close(2)=0exit_group(0)=?+++exitedwith0+++
在二进制级别上,发起系统调用相比简单的函数调用有一些区别,但是大部分程序都使用标准库提供的封装函数。例如,POSIXC标准库包含一个write()函数,该函数包含用于进行write系统调用的所有与硬件体系结构相关的代码。
简单来说,一个应用程序与其环境(计算机系统)的交互都是通过系统调用来完成的。所以当软件在一台机器上可以工作但是在另一台机器无法工作的时候,追踪系统调用是一个很好的查错方法。具体地说,你可以通过追踪系统调用分析以下典型操作:
控制台输入与输出(IO)
网络IO
文件系统访问以及文件IO
进程/线程生命周期管理
原始内存管理
访问特定的设备驱动
什么时候可以使用strace?理论上,strace适用于任何用户空间程序,因为所有的用户空间程序都需要进行系统调用。strace对于已编译的低级程序最有效果,但如果你可以避免运行时环境和解释器带来的大量额外输出,则仍然可以与Python等高级语言程序一起使用。
当软件在一台机器上正常工作,但在另一台机器上却不能正常工作,同时抛出了有关文件、权限或者不能运行某某命令等模糊的错误信息时,strace往往能大显身手。不幸的是,它不能诊断高等级的问题,例如数字证书验证错误等。这些问题通常需要组合使用strace(有时候是ltrace)和其它高级工具(例如使用openssl命令行工具调试数字证书错误)。
本文中的示例基于独立的服务器,但是对系统调用的追踪通常也可以在更复杂的部署平台上完成,仅需要找到合适的工具。
一个简单的例子假设你正在尝试运行一个叫做foo的服务器应用程序,但是发生了以下情况:
$fooErroropeningconfigurationfile:Nosuchfileordirectory
显然,它没有找到你已经写好的配置文件。之所以会发生这种情况,是因为包管理工具有时候在编译应用程序时指定了自定义的路径,所以你应当遵循特定发行版提供的安装指南。如果错误信息告诉你正确的配置文件应该在什么地方,你就可以在几秒钟内解决这个问题,但如果没有告诉你呢?你该如何找到正确的路径?
如果你有权访问源代码,则可以通过阅读源代码来解决问题。这是一个好的备用计划,但不是最快的解决方案。你还可以使用类似gdb的单步调试器来观察程序的行为,但使用专门用于展示程序与系统环境交互作用的工具strace更加有效。
一开始,strace产生的大量输出可能会让你不知所措,幸好你可以忽略其中大部分的无用信息。我经常使用-o参数把输出的追踪结果保存到单独的文件里:
$strace-o/tmp/tracefooErroropeningconfigurationfile:Nosuchfileordirectory$cat/tmp/traceexecve("foo",["foo"],0x7ffce98dc010/*16vars*/)=0brk(NULL)=0x56363b3fb000access("/etc/",R_OK)=-1ENOENT(Nosuchfileordirectory)openat(AT_FDCWD,"/etc/",O_RDONLY|O_CLOEXEC)=3fstat(3,{st_mode=S_IFREG|0644,st_size=25186,})=0mmap(NULL,25186,PROT_READ,MAP_PRIVATE,3,0)=0x7f2f12cf1000close(3)=0openat(AT_FDCWD,"/lib/x86_64-linux-gnu/",O_RDONLY|O_CLOEXEC)=3read(3,"\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\0\1\0\0\0\260A\2\0\0\0\0\0",832)=832fstat(3,{st_mode=S_IFREG|0755,st_size=1824496,})=0mmap(NULL,8192,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0x7f2f12cef000mmap(NULL,1837056,PROT_READ,MAP_PRIVATE|MAP_DENYWRITE,3,0)=0x7f2f12b2e000mprotect(0x7f2f12b50000,1658880,PROT_NONE)=0mmap(0x7f2f12b50000,1343488,PROT_READ|PROT_EXEC,MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE,3,0x22000)=0x7f2f12b50000mmap(0x7f2f12c98000,311296,PROT_READ,MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE,3,0x16a000)=0x7f2f12c98000mmap(0x7f2f12ce5000,24576,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE,3,0x1b6000)=0x7f2f12ce5000mmap(0x7f2f12ceb000,14336,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,-1,0)=0x7f2f12ceb000close(3)=0arch_prctl(ARCH_SET_FS,0x7f2f12cf0500)=0mprotect(0x7f2f12ce5000,16384,PROT_READ)=0mprotect(0x56363b08b000,4096,PROT_READ)=0mprotect(0x7f2f12d1f000,4096,PROT_READ)=0munmap(0x7f2f12cf1000,25186)=0openat(AT_FDCWD,"/etc/foo/",O_RDONLY)=-1ENOENT(Nosuchfileordirectory)dup(2)=3fcntl(3,F_GETFL)=0x2(flagsO_RDWR)brk(NULL)=0x56363b3fb000brk(0x56363b41c000)=0x56363b41c000fstat(3,{st_mode=S_IFCHR|0620,st_rdev=makedev(0x88,0x8),})=0write(3,"Erroropeningconfigurationfile",60)=60close(3)=0exit_group(1)=?+++exitedwith1+++strace输出的第一页通常是低级的进程启动过程。(你可以看到很多mmap、mprotect、brk调用,这是用来分配原始内存和映射动态链接库的。)实际上,在查找错误时,最好从下往上阅读strace的输出。你可以看到write调用在最后返回了错误信息。如果你向上找,你将会看到第一个失败的系统调用是openat,它在尝试打开/etc/foo/时抛出了ENOENT(“Nosuchfileordirectory”)的错误。现在我们已经知道了配置文件应该放在哪里。
这是一个简单的例子,但我敢说在90%的情况下,使用strace进行调试不需要更多复杂的工作。以下是完整的调试步骤:
从程序中获得含糊不清的错误信息
使用strace运行程序
在输出中找到错误信息
往前追溯并找到第一个失败的系统调用
第四步中的系统调用很可能向你显示出问题所在。
小技巧在开始更加复杂的调试之前,这里有一些有用的调试技巧帮助你高效使用strace:
man是你的朋友在很多*nix操作系统中,你可以通过mansyscalls查看系统调用的列表。你将会看到类似于brk(2)之类的东西,这意味着你可以通过运行man2brk得到与此相关的更多信息。
一个小问题:man2fork会显示出在GNUlibc里封装的fork()手册页,而fork()现在实际上是由clone系统调用实现的。fork的语义与clone相同,但是如果我写了一个含有fork()的程序并使用strace去调试它,我将找不到任何关于fork调用的信息,只能看到clone调用。如果将源代码与strace的输出进行比较的时候,像这种问题会让人感到困惑。
使用-o将输出保存到文件strace可以生成很多输出,所以将输出保存到单独的文件是很有帮助的(就像上面的例子一样)。它还能够在控制台中避免程序自身的输出与strace的输出发生混淆。
你可能已经注意到,错误信息的第二部分没有出现在上面的例子中。这是因为strace默认仅显示字符串参数的前32个字节。如果你需要捕获更多参数,请向strace追加类似于-s128之类的参数。
-y使得追踪文件或套接字更加容易“一切皆文件”意味着*nix系统通过文件描述符进行所有IO操作,不管是真实的文件还是通过网络或者进程间管道。这对于编程而言是很方便的,但是在追踪系统调用时,你将很难分辨出read和write的真实行为。
-y参数使strace在注释中注明每个文件描述符的具体指向。
使用-p附加到正在运行的进程中正如我们将在后面的例子中看到的,有时候你想追踪一个正在运行的程序。如果你知道这个程序的进程号为1337(可以通过ps查询),则可以这样操作:
$strace-p1337systemcalltraceoutput
你可能需要root权限才能运行。
使用-f追踪子进程strace默认只追踪一个进程。如果这个进程产生了一个子进程,你将会看到创建子进程的系统调用(一般是clone),但是你看不到子进程内触发的任何调用。
如果你认为在子进程中存在错误,则需要使用-f参数启用子进程追踪功能。这样做的缺点是输出的内容会让人更加困惑。当追踪一个进程时,strace显示的是单个调用事件流。当追踪多个进程的时候,你将会看到以unfinished开始的初始调用,接着是一系列针对其它线程的调用,最后才出现以foocallresumed结束的初始调用。此外,你可以使用-ff参数将所有的调用分离到不同的文件中(查看strace手册获取更多信息)。
使用-e进行过滤正如你所看到的,默认的追踪输出是所有的系统调用。你可以使用-e参数过滤你需要追踪的调用(查看strace手册)。这样做的好处是运行过滤后的strace比起使用grep进行二次过滤要更快。老实说,我大部分时间都不会被打扰。
并非所有的错误都是不好的一个简单而常用的例子是一个程序在多个位置搜索文件,例如shell搜索哪个bin/目录包含可执行文件:
$stracesh-cunamestat("/home/user/bin/uname",0x7ffceb817820)=-1ENOENT(Nosuchfileordirectory)stat("/usr/local/bin/uname",0x7ffceb817820)=-1ENOENT(Nosuchfileordirectory)stat("/usr/bin/uname",{st_mode=S_IFREG|0755,st_size=39584,})=0“错误信息之前的最后一次失败调用”这种启发式方法非常适合于查找错误。无论如何,自下而上地查找是有道理的。
C编程指南非常有助于理解系统调用标准C库函数调用不属于系统调用,但它们仅是系统调用之上的唯一一个薄层。所以如果你了解(甚至只是略知一二)如何使用C语言,那么阅读系统调用追踪信息就非常容易。例如,如果你在调试网络系统调用,你可以尝试略读Beej经典的《网络编程指南》。
一个更复杂的调试例子就像我说的那样,简单的调试例子表现了我在大部分情况下如何使用strace。然而,有时候需要一些更加细致的工作,所以这里有一个稍微复杂(且真实)的例子。
bcron是一个任务调度器,它是经典*nixcron守护程序的另一种实现。它已经被安装到一台服务器上,但是当有人尝试编辑作业时间表时,发生了以下情况:
strace-o/tmp/tracecrontab-e-ulogsbcrontab:Fatal:CouldnotcreatetemporaryfileAnsible:logsagg\n2014***lo",8192)=150read(3,"",8192)=0munmap(0x7f82049b4000,8192)=0close(3)=0socket(AF_UNIX,SOCK_STREAM,0)=3connect(3,{sa_family=AF_UNIX,sun_path="/var/run/bcron-spool"},110)=0mmap(NULL,8192,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0x7f82049b4000write(3,"156:Slogs\0ss-pl|grep/var/run/bcron-spoolu_strLISTEN0128/var/run/bcron-spool1466637*0users:(("unixserver",pid=20629,fd=3))这告诉我们/var/run/bcron-spool套接字的监听程序是unixserver这个命令,它的进程ID为20629。(巧合的是,这个程序也使用文件描述符3去连接这个套接字。)
第二个常用的工具就是使用lsof查找相同的信息。它可以列出当前系统中打开的所有文件(或文件描述符)。或者,我们可以得到一个具体文件的信息:
ls-ld/var/spool/cron/tmp/drwxr-xr-x2rootroot4096Nov605:33/var/spool/cron/tmp/#psu-p20629USERPID%CPU%?SsNov140:00unixserver-U/var/run/bcron-spool--bcron-spool
这就是问题所在!这个服务进程以cron用户运行,但是只有root用户才有向/var/spool/cron/tmp/目录写入的权限。一个简单chowncron/var/spool/cron/tmp/命令就能让bcron正常工作。(如果不是这个问题,那么下一个最有可能的怀疑对象是诸如SELinux或者AppArmor之类的内核安全模块,因此我将会使用dmesg检查内核日志。)
总结最初,系统调用追踪可能会让人不知所措,但是我希望我已经证明它们是调试一整套常见部署问题的快速方法。你可以设想一下尝试用单步调试器去调试多进程的bcron问题。
通过一连串的系统调用解决问题是需要练习的,但正如我说的那样,在大多数情况下,我只需要使用strace从下往上追踪并查找错误。不管怎样,strace节省了我很多的调试时间。我希望这也对你有所帮助。
via:
作者:SimonArneaud选题:lujun9972译者:hanwckf校对:wxy
本文由LCTT原创编译,Linux中国荣誉推出