Contents

Linux中strace命令简介

1. 概述

在本文中,我们将研究 Linux 中的工具strace。我们将从一个简单的介绍开始,然后介绍strace的一些用法。

2. strace

strace 是 Linux 中的一个诊断工具。它拦截并记录命令进行的任何系统调用。此外,它还记录发送到进程的任何 Linux 信号。然后我们可以使用这些信息来调试或诊断程序。如果命令的源代码不容易获得,它会特别有用。

3. 安装

在基于 Debian 的 Linux(例如 Ubuntu)上,我们可以使用apt-get 安装strace

$ apt-get install -y strace

另一方面,我们将使用yum为基于 RHEL 的 Linux(例如 CentOS )安装strace

$ yum install -y strace

4. 基本用法

以最简单的形式,我们可以调用strace后跟我们希望跟踪的命令:

$ strace pwd
execve("/usr/bin/pwd", ["pwd"], 0x7fffcb3aa770 /* 9 vars */) = 0
brk(NULL)                               = 0x5631e77f6000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffdb256c5e0) = -1 EINVAL (Invalid argument)
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
...(Subsequent output truncated)

在上面的示例中,strace运行命令pwd 。随后,strace拦截并记录pwd进行的所有系统调用。最后,当命令返回时,记录的系统调用和信号会显示在控制台上。

首先,输出中的每一行代表该命令进行的一个系统调用。在此示例中,第一行显示*execve *系统调用在命令开始时被调用。execve是一个系统调用,它执行第一个参数引用的程序。

然后,我们可以看到strace还显示了系统调用中涉及的确切参数。特别是,系统调用正在执行路径*/usr/bin/pwd上的二进制文件 ,并将“pwd”*作为其第一个参数传递。此外,系统调用的退出代码显示在等号旁边。在这种情况下,系统调用返回退出代码 0,表示成功。

同样,导致错误的系统调用将显示其错误退出代码和描述

access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)

5. 将strace附加到正在运行的进程

要将strace附加到正在运行的进程,我们可以使用标志-p*后跟 PID。*

让我们启动一个睡眠进程并返回 PID:

$ sh -c 'echo $$; exec sleep 60'
50

然后,在另一个终端上,我们可以使用标志*-p附加到带有strace*的进程:

$ strace -p 50
strace: Process 50 attached
restart_syscall(<... resuming interrupted clock_nanosleep ...>) = 0

6. 更改环境变量列表

使用strace,我们可以更改我们正在跟踪的进程继承的环境变量列表。要将额外的环境变量传递给进程,我们可以使用标志-E:**

$ strace -E var1=val1 pwd

在上面的示例中,环境变量var1将设置为val1。然后,这个环境变量将被传递给进程pwd。 同样,我们可以使用相同的标志来防止进程继承环境变量:

$ strace -E var1 pwd

当我们不指定值时,环境变量不会被进程继承

7. 以特定用户身份运行命令

要以另一个用户身份运行和跟踪程序,我们可以使用标志-u后跟用户名。**此选项的一个先决条件是我们需要以root 用户身份运行strace

要以用户“blogdemo”的身份运行,我们可以使用标志*-u*后跟用户名:

# strace -u blogdemo whoami

在上面的示例中,strace以用户blogdemo的身份运行命令whoami

8. 获取时序信息

我们还可以使用strace获取一些时间和持续时间信息。例如,我们可以使用标志*-t*显示每个系统调用的时间戳:

$ strace -t whoami
06:02:38 execve("/usr/bin/whoami", ["whoami"], 0x7ffdc4811038 /* 12 vars */) = 0
-TRUNCATED-

此外,我们可以使用标志-tt获得具有微秒分辨率的时间戳:

$ strace -tt whoami
06:07:10.899089 execve("/usr/bin/whoami", ["whoami"], 0x7fff396d2898 /* 12 vars */) = 0
-TRUNCATED-

最后,我们可以使用标志*-T*显示每个系统调用的持续时间:

$ strace -T whoami
execve("/usr/bin/whoami", ["whoami"], 0x7fff0493d078 /* 12 vars */) = 0 <0.000274>
-TRUNCATED-

使用标志*-T*,strace在退出代码旁边附加系统调用的持续时间(以秒为单位)。在上面的示例中,该命令在系统调用execve中花费了 0.000274 秒。

9. 报告统计

作为一个强大的诊断工具,strace能够计算和报告它所跟踪的程序的一些统计数据。

9.1. 命令运行摘要

要获得命令的摘要,我们可以使用标志-c:**

$ strace -c whoami
blogdemo
## % time     seconds  usecs/call     calls    errors syscall
 18.62    0.000264          26        10           close
 15.16    0.000215          16        13           mmap
 12.27    0.000174          29         6           openat
-TRUNCATED-
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
##   0.00    0.000000           0         2         1 arch_prctl
100.00    0.001418                    68         4 total

使用标志*-c*,strace显示命令运行的摘要,而不是每个系统调用和信号。

正如我们从输出中看到的,摘要按系统调用对结果进行分组。

从左边开始,第一列以百分比的形式显示此系统调用所花费的时间。然后,第二列以秒为单位描述相同的测量值。在 usecs/call列上,该值表示每次调用花费的平均微秒数。最后,第四列和第五列分别报告调用总数和错误总数。

附带说明一下,标志*-c仅显示命令运行的摘要。*要显示常规输出和摘要,我们可以使用标志-C代替**:

$ strace -C whoami

9.2. 按列对结果进行排序

我们可以使用标志-S*按不同的列对汇总结果进行排序*。例如,我们可以按错误数降序对结果进行排序:

$ strace -c -S errors whoami
blogdemo
## % time     seconds  usecs/call     calls    errors syscall
  2.37    0.000055          27         2         2 connect
  1.68    0.000039          39         1         1 access
  3.49    0.000081          40         2         1 arch_prctl
-TRUNCATED-
  8.30    0.000193         193         1           execve
  1.46    0.000034          34         1           geteuid
##   8.56    0.000199          33         6           openat
100.00    0.002324                    68         4 total

10. strace表达式

strace支持一组丰富的表达式,可以改变strace的多个方面。例如,使用表达式,我们可以按系统调用名称和退出代码过滤输出。此外,我们可以格式化输出以减少不需要的文本。最后,该表达式还支持通过故障和延迟注入来干预系统调用。

10.1. 一般语法

通常,标志*-e*在表达式之前。然后,表达式被指定为键值对:

-e 限定符=[!]value[,value]

在值之前,我们可以指定一个感叹号来否定该值。此外,我们可以用逗号分隔多个值。

10.2. 预选赛

限定符必须来自trace, status, signal, quiet, abbrev, verbose, raw, read, write, fault 和* inject*的列表。

这些限定符可以根据它们各自的功能大致分为以下组:

  • 过滤(trace, status, signal, quiet
  • 输出格式(abbrev, verbose, raw
  • 系统调用篡改(fault, inject
  • 文件描述符数据转储(read, write

10.3. 价值

表达式中的值表示依赖于限定符的值。此外,我们可以用逗号分隔单个键的多个值。

11. 用表达式过滤

11.1. 按系统调用名称过滤输出

使用过滤限定符,我们可以减少strace的输出。例如,我们只能输出fstat系统调用:

$ strace -e trace=fstat whoami 
fstat(3, {st_mode=S_IFREG|0644, st_size=9394, ...}) = 0
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
-TRUNCATED-
fstat(3, {st_mode=S_IFREG|0644, st_size=494, ...}) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0
blogdemo
+++ exited with 0 +++

同样,我们可以使用否定显示除fstat之外的每个系统调用:

$ strace -e trace=!fstat whoami

11.2. 按返回状态过滤输出

使用status限定符,我们可以通过返回状态过滤系统调用。例如,我们可以只显示不成功的系统调用:

$ strace -e status=!successful whoami

status限定符的有效值为: successful、failed、unfinished、availabledetached

此外,我们可以用逗号组合多个状态。例如,我们可以显示以unfinishedunavailable状态退出的系统调用:

$ strace -e status=unfinished,unavailable whoami

11.3. 按信号过滤输出

除了系统调用,strace还记录进程接收到的任何信号。默认情况下,这些记录的信号与系统调用一起显示在输出中。我们可以使用限定符signal过滤这些信号。例如,我们只能跟踪信号SIGBUS

$ strace -e signal=SIGBUS whoami

signal限定符的有效值是所有标准 Linux 信号

11.4. 抑制附加信息消息

除了系统调用和信号之外,strace还显示一些信息性消息。例如,每当进程退出时, strace都会打印一条消息:

+++ exited with 0 +++

为了抑制这些消息,我们可以使用限定符quiet

$ strace -e quiet=exit whoami

我们可以用这个限定符抑制的消息包括attachexitpath-resolutionpersonalitythread-execve 、 superseded

12. 格式化输出

12.1. 取消引用系统调用参数

使用限定词verbose,我们可以使strace以取消引用的形式显示系统调用参数。以取消引用的形式显示参数比指针值更有帮助。这就是为什么默认情况下所有系统调用参数都被取消引用的原因。换句话说,详细限定符的默认值为all

要查看限定符的实际作用,我们可以首先禁用所有系统调用的verbose表达式:

$ strace -e verbose=none whoami
execve(0x7fff4e3efdc0, 0x7fff4e3f1100, 0x7fff4e3f1110) = 0
brk(0)                                  = 0x55970f3be000
arch_prctl(0x3001, 0x7fff2630af10)      = -1 EINVAL (Invalid argument)
-TRUNCATED-

通过设置表达式verbose=nonestrace显示参数指针而不是其取消引用的结构。

12.2. 缩写系统调用

一些系统调用的取消引用参数可能很长,使输出混乱。因此,strace应用表达式abbrev=all作为其默认行为。例如,如果我们不覆盖abbrev的默认表达式,则fstat系统调用的记录会被缩写:

$ strace -e whoami
-TRUNCATED-
fstat(3, {st_mode=S_IFREG|0644, st_size=971, ...}) = 0
-TRUNCATED-

正如我们所看到的,其余的参数值都用省略号缩写。

现在让我们禁用所有系统调用的abbrev表达式:

$ strace -e abbrev=none whoami
-TRUNCATED-
fstat(1, {st_dev=makedev(0, 0x76), st_ino=4, st_mode=S_IFCHR|0620, st_nlink=1, st_uid=0, st_gid=5, st_blksize=1024, st_blocks=0, st_rdev=makedev(0x88, 0x1), st_atime=1614501905 /* 2021-02-28T08:45:05.353968000+0000 */, st_atime_nsec=353968000, st_mtime=1614501905 /* 2021-02-28T08:45:05.353968000+0000 */, st_mtime_nsec=353968000, st_ctime=1614343760 /* 2021-02-26T12:49:20.354968000+0000 */, st_ctime_nsec=354968000}) = 0
-TRUNCATED-

通过禁用所有系统调用的abbrev表达式,strace将完整地显示参数结构。

12.3. 显示未解码的参数

使用raw限定符,strace显示系统调用参数地址而不是其底层 char 值。

默认情况下,所有参数都被解码为各自的字符表示:

$ strace whoami
execve("/usr/bin/whoami", ["whoami"], 0x7ffd9bc11880 /* 12 vars */) = 0
brk(NULL)                               = 0x5574c7e68000
-TRUNCATED-

让我们设置表达式raw=execve

$ strace -e raw=execve whoami
execve(0x7ffec1f3d6c0, 0x7ffec1f3ea00, 0x7ffec1f3ea10) = 0
brk(NULL)                               = 0x5574c7e68000
-TRUNCATED-

正如我们所看到的,execve系统调用的参数指针被显示而不是它们的实际值。

13. 系统调用篡改

strace表达式最强大的特性之一是它能够使用注入故障限定符来改变系统调用行为。此功能与单元测试中的方法模拟非常相似。例如,我们可以模拟一个系统调用,让它在被命令调用时总是返回一个错误。篡改系统调用的能力对于在不同条件下试验程序很有用。

通常,inject的表达式可以表示为:

--inject=syscall_set[:error=errno|:retval=value][:signal=sig][:syscall=syscall][:delay_enter=delay][:delay_exit=delay][:when=expr]

请注意,errorretval是互斥的。换句话说,如果我们为一组系统调用注入error,我们不能为同一组系统调用注入retval

正如我们可以从一般表达式中推断出的,inject限定符非常灵活。特别是,我们可以在进入或退出系统调用时使用delay_enterdelay_exit插入一些延迟。此外,我们可以通过when子表达式控制注入。

另一方面,**限定符error是限定符inject的特定情况。**具体来说,fault限定符只能用于注入故障。因此,我们将只在本文中查看 inject限定符。

13.1. 将故障注入系统调用

让我们将故障注入fstat系统调用。特别是,我们将fstat调用替换为 EPERM 退出代码:

$ strace -e inject=fstat:error=EPERM whoami
execve("/usr/bin/whoami", ["whoami"], 0x7ffc481220a0 /* 12 vars */) = 0
-TRUNCATED-
fstat(3, 0x7ffd9337e640)                = -1 EPERM (Operation not permitted) (INJECTED)
-TRUNCATED-

请注意fstat系统调用如何始终以退出状态 -1 EPERM 返回。此外,** strace用文本*(INJECTED)注释退出状态,以明确错误是注入的*。

13.2. 控制故障何时注入

我们可以使用when子表达式进一步控制注入的发生,而不是在每次调用时都注入错误。例如,我们可以仅在第二次调用fstat时将错误错误 EPERM 注入:

$ strace -e inject=fstat:error=EPERM:when=2 whoami

在上面的示例中,strace仅在第二次调用fstat时注入错误。如果有第 3 次和第 4 次调用,则不会注入任何错误。要在第二次及以后的调用中注入错误,我们可以使用加号:

$ strace -e inject=fstat:error=EPERM:when=2+ whoami

最后,我们还可以使注入遵循特定的步长。例如,我们可以在第 2 次调用和后续调用中注入错误,步长为 2。具体而言,我们希望在第 2、第 4、第 6 次调用中注入错误,依此类推。

为此,我们将在加号之后指定步长 2:

$ strace -e inject=fstat:error=EPERM:when=2+2 whoami

13.3. 在系统调用中引入延迟

**使用限定符delay_enter,我们可以在系统调用调用之前注入一些延迟。同样,我们可以在系统调用返回后使用限定符delay_exit注入延迟。**两个限定符都接受以微秒为单位的时间值。

例如,我们可以在命令调用fstat系统调用之前引入 2 秒(2000000 微秒)的延迟:

$ strace -e inject=fstat:delay_enter=2000000 whoami

另一方面,要在系统调用之后注入延迟,我们将使用表达式delay_exit

$ strace -e inject=fstat:delay_exit=2000000 whoami

两个示例之间的重要区别是延迟注入的顺序。在第一个示例中,在调用fstat之前引入了 2 秒延迟。相反,第二个示例在fstat系统调用返回后注入了 2 秒延迟。

14.文件描述符数据转储

14.1. 在每个输入活动中转储文件描述符的数据

使用限定符read,我们可以在每个输入活动上转储任何文件描述符的十六进制数据。每当文件描述符 3 上有输入活动时,让我们转储数据:

$ strace -e read=3 whoami

每当对文件描述符 3进行read系统调用时,输出现在将包含十六进制数据块。

14.2. 在每个输出活动中转储文件描述符的数据

只要在文件描述符上调用了write系统调用,我们就可以在文件描述符上显示数据。例如,要在每个输出活动中转储文件描述符 5 的数据,我们将使用表达式write=5

$ strace -e write=5 whoami