在软件部署中使用 strace 进行调试
我的大部分工作都涉及到部署软件系统,这意味着我需要花费很多时间来解决以下问题:
- 这个软件可以在原开发者的机器上工作,但是为什么不能在我这里运行?
- 这个软件昨天可以在我的机器上工作,但是为什么今天就不行?
这是一种调试的类型,但是与一般的软件调试有所不同。一般的调试通常只关心代码的逻辑,但是在软件部署中的调试关注的是程序的代码和它所在的运行环境之间的相互影响。即便问题的根源是代码的逻辑错误,但软件显然可以在别的机器上运行的事实意味着这类问题与运行环境密切相关。
所以,在软件部署过程中,我没有使用传统的调试工具(例如 gdb
),而是选择了其它工具进行调试。我最喜欢的用来解决“为什么这个软件无法在这台机器上运行?”这类问题的工具就是 strace
。
什么是 strace?
strace 是一个用来“追踪系统调用”的工具。它主要是一个 Linux 工具,但是你也可以在其它系统上使用类似的工具(例如 DTrace 和 ktrace)。
它的基本用法非常简单。只需要在 strace
后面跟上你需要运行的命令,它就会显示出该命令触发的所有系统调用(你可能需要先安装好 strace
):
$ strace echo Hello
...Snip lots of stuff...
write(1, "Hellon", 6) = 6
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
这些系统调用都是什么?它们就像是操作系统内核提供的 API。很久以前,软件拥有直接访问硬件的权限。如果软件需要在屏幕上显示一些东西,它将会与视频硬件的端口和内存映射寄存器纠缠不清。当多任务操作系统变得流行以后,这就导致了混乱的局面,因为不同的应用程序将“争夺”硬件,并且一个应用程序的错误可能致使其它应用程序崩溃,甚至导致整个系统崩溃。所以 CPU 开始支持多种不同的特权模式(或者称为“保护环”)。它们让操作系统内核在具有完全硬件访问权限的最高特权模式下运行,于此同时,其它在低特权模式下运行的应用程序必须通过向内核发起系统调用才能够与硬件进行交互。
在二进制级别上,发起系统调用相比简单的函数调用有一些区别,但是大部分程序都使用标准库提供的封装函数。例如,POSIX C 标准库包含一个 write()
函数,该函数包含用于进行 write
系统调用的所有与硬件体系结构相关的代码。
简单来说,一个应用程序与其环境(计算机系统)的交互都是通过系统调用来完成的。所以当软件在一台机器上可以工作但是在另一台机器无法工作的时候,追踪系统调用是一个很好的查错方法。具体地说,你可以通过追踪系统调用分析以下典型操作:
- 控制台输入与输出 (IO)
- 网络 IO
- 文件系统访问以及文件 IO
- 进程/线程生命周期管理
- 原始内存管理
- 访问特定的设备驱动
什么时候可以使用 strace?
理论上,strace
适用于任何用户空间程序,因为所有的用户空间程序都需要进行系统调用。strace
对于已编译的低级程序最有效果,但如果你可以避免运行时环境和解释器带来的大量额外输出,则仍然可以与 Python 等高级语言程序一起使用。
当软件在一台机器上正常工作,但在另一台机器上却不能正常工作,同时抛出了有关文件、权限或者不能运行某某命令等模糊的错误信息时,strace
往往能大显身手。不幸的是,它不能诊断高等级的问题,例如数字证书验证错误等。这些问题通常需要组合使用 strace
(有时候是 ltrace
)和其它高级工具(例如使用 openssl
命令行工具调试数字证书错误)。
本文中的示例基于独立的服务器,但是对系统调用的追踪通常也可以在更复杂的部署平台上完成,仅需要找到合适的工具。
一个简单的例子
假设你正在尝试运行一个叫做 foo
的服务器应用程序,但是发生了以下情况:
$ foo
Error opening configuration file: No such file or directory
显然,它没有找到你已经写好的配置文件。之所以会发生这种情况,是因为包管理工具有时候在编译应用程序时指定了自定义的路径,所以你应当遵循特定发行版提供的安装指南。如果错误信息告诉你正确的配置文件应该在什么地方,你就可以在几秒钟内解决这个问题,但如果没有告诉你呢?你该如何找到正确的路径?
如果你有权访问源代码,则可以通过阅读源代码来解决问题。这是一个好的备用计划,但不是最快的解决方案。你还可以使用类似 gdb
的单步调试器来观察程序的行为,但使用专门用于展示程序与系统环境交互作用的工具 strace
更加有效。
一开始, strace
产生的大量输出可能会让你不知所措,幸好你可以忽略其中大部分的无用信息。我经常使用 -o
参数把输出的追踪结果保存到单独的文件里:
$ strace -o /tmp/trace foo
Error opening configuration file: No such file or directory
$ cat /tmp/trace
execve("foo", ["foo"], 0x7ffce98dc010 /* 16 vars */) = 0
brk(NULL) = 0x56363b3fb000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=25186, ...}) = 0
mmap(NULL, 25186, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2f12cf1000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >