了解 Linux 系统调用与 strace

使用 strace 跟踪用户进程和 Linux 内核之间的薄层。
168 位读者喜欢这个。
Why the operating system matters even more in 2017

Internet Archive Book Images。由 Opensource.com 修改。CC BY-SA 4.0

系统调用是程序向内核请求服务的一种编程方式,而 strace 是一个强大的工具,允许您跟踪用户进程和 Linux 内核之间的薄层。

要了解操作系统如何工作,您首先需要了解系统调用如何工作。操作系统的主要功能之一是为用户程序提供抽象。

操作系统大致可以分为两种模式

  • 内核模式: 操作系统内核使用的特权和强大模式
  • 用户模式: 大多数用户应用程序运行的地方

用户主要使用命令行实用程序和图形用户界面 (GUI) 来完成日常任务。系统调用在后台静默工作,与内核交互以完成工作。

系统调用与函数调用非常相似,这意味着它们接受并处理参数和返回值。唯一的区别是系统调用进入内核,而函数调用不进入内核。从用户空间到内核空间的切换是使用特殊的 陷阱 机制完成的。

大多数这些都被系统库(在 Linux 系统上又名 glibc)对用户隐藏了。即使系统调用本质上是通用的,但发出系统调用的机制在很大程度上取决于机器。

本文通过使用一些通用命令并使用 strace 分析每个命令发出的系统调用,探索了一些实际示例。这些示例使用红帽企业 Linux,但这些命令在其他 Linux 发行版上应该工作方式相同

[root@sandbox ~]# cat /etc/redhat-release 
Red Hat Enterprise Linux Server release 7.7 (Maipo)
[root@sandbox ~]# 
[root@sandbox ~]# uname -r
3.10.0-1062.el7.x86_64
[root@sandbox ~]# 

首先,确保您的系统上安装了所需的工具。您可以使用以下 RPM 命令验证是否安装了 strace;如果已安装,您可以使用 -V 选项检查 strace 实用程序版本号

[root@sandbox ~]# rpm -qa | grep -i strace
strace-4.12-9.el7.x86_64
[root@sandbox ~]# 
[root@sandbox ~]# strace -V
strace -- version 4.12
[root@sandbox ~]# 

如果这不起作用,请通过运行以下命令安装 strace

yum install strace

为了本示例的目的,在 /tmp 中创建一个测试目录,并使用 touch 命令创建两个文件,如下所示

[root@sandbox ~]# cd /tmp/
[root@sandbox tmp]# 
[root@sandbox tmp]# mkdir testdir
[root@sandbox tmp]# 
[root@sandbox tmp]# touch testdir/file1
[root@sandbox tmp]# touch testdir/file2
[root@sandbox tmp]#

(我使用 /tmp 目录是因为每个人都可以访问它,但如果您愿意,可以选择另一个目录。)

使用 ls 命令在 testdir 目录中验证文件是否已创建

[root@sandbox tmp]# ls testdir/
file1  file2
[root@sandbox tmp]#

您可能每天都在使用 ls 命令,而没有意识到系统调用在其底层工作。这里存在抽象;以下是此命令的工作方式

Command-line utility -> Invokes functions from system libraries (glibc) -> Invokes system calls

ls 命令在内部调用来自 Linux 系统上的系统库(又名 glibc)的函数。这些库调用执行大部分工作的系统调用。

如果您想知道从 glibc 库中调用了哪些函数,请使用 ltrace 命令,后跟常规的 ls testdir/ 命令

ltrace ls testdir/

如果未安装 ltrace,请通过输入以下命令安装它

yum install ltrace

屏幕上会转储大量输出;不用担心——只需按照步骤操作即可。来自 ltrace 命令输出的一些重要的库函数与本示例相关,包括

opendir("testdir/")                                  = { 3 }
readdir({ 3 })                                       = { 101879119, "." }
readdir({ 3 })                                       = { 134, ".." }
readdir({ 3 })                                       = { 101879120, "file1" }
strlen("file1")                                      = 5
memcpy(0x1665be0, "file1\0", 6)                      = 0x1665be0
readdir({ 3 })                                       = { 101879122, "file2" }
strlen("file2")                                      = 5
memcpy(0x166dcb0, "file2\0", 6)                      = 0x166dcb0
readdir({ 3 })                                       = nil
closedir({ 3 })                       

通过查看上面的输出,您可能可以理解正在发生的事情。一个名为 testdir 的目录正在被 opendir 库函数打开,然后调用 readdir 函数,该函数正在读取目录的内容。最后,调用 closedir 函数,该函数关闭先前打开的目录。现在忽略其他 strlenmemcpy 函数。

您可以看到正在调用哪些库函数,但本文将重点介绍由系统库函数调用的系统调用。

与上面类似,要了解调用了哪些系统调用,只需将 strace 放在 ls testdir 命令之前,如下所示。再次,一堆乱码将被转储到您的屏幕上,您可以在此处继续操作

[root@sandbox tmp]# strace ls testdir/
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
brk(NULL)                               = 0x1f12000
<<< truncated strace output >>>
write(1, "file1  file2\n", 13file1  file2
)          = 13
close(1)                                = 0
munmap(0x7fd002c8d000, 4096)            = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++
[root@sandbox tmp]# 

运行 strace 命令后屏幕上的输出只是运行 ls 命令所做的系统调用。每个系统调用都为操作系统服务于特定目的,它们可以大致分为以下几类

  • 进程管理系统调用
  • 文件管理系统调用
  • 目录和文件系统管理系统调用
  • 其他系统调用

分析转储到屏幕上的信息的更简单方法是使用 strace 方便的 -o 标志将输出记录到文件中。在 -o 标志后添加合适的文件名,然后再次运行命令

[root@sandbox tmp]# strace -o trace.log ls testdir/
file1  file2
[root@sandbox tmp]#

这次,没有输出转储到屏幕上——ls 命令按预期工作,显示文件名并将所有输出记录到文件 trace.log。该文件仅针对简单的 ls 命令就包含近 100 行内容

[root@sandbox tmp]# ls -l trace.log 
-rw-r--r--. 1 root root 7809 Oct 12 13:52 trace.log
[root@sandbox tmp]# 
[root@sandbox tmp]# wc -l trace.log 
114 trace.log
[root@sandbox tmp]# 

查看示例 trace.log 中的第一行

execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
  • 该行的第一个单词 execve 是正在执行的系统调用的名称。
  • 括号内的文本是提供给系统调用的参数。
  • = 符号后的数字(在本例中为 0)是 execve 系统调用返回的值。

现在输出看起来不那么吓人了吧?您可以应用相同的逻辑来理解其他行。

现在,将您的注意力缩小到您调用的单个命令,即 ls testdir。您知道命令 ls 使用的目录名称,那么为什么不在您的 trace.log 文件中 grep 搜索 testdir 并看看您得到什么?详细查看结果的每一行

[root@sandbox tmp]# grep testdir trace.log
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0
openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
[root@sandbox tmp]# 

回顾上面对 execve 的分析,您能说出此系统调用是做什么的吗?

execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0

您不需要记住所有系统调用或它们的作用,因为您可以在需要时参考文档。手册页来救援!在运行 man 命令之前,确保安装了以下软件包

[root@sandbox tmp]# rpm -qa | grep -i man-pages
man-pages-3.53-5.el7.noarch
[root@sandbox tmp]# 

请记住,您需要在 man 命令和系统调用名称之间添加一个 2。如果您使用 man man 阅读 man 的手册页,您可以看到第 2 节是为系统调用保留的。同样,如果您需要有关库函数的信息,则需要在 man 和库函数名称之间添加一个 3

以下是手册的章节编号以及它们包含的页面类型

1. Executable programs or shell commands
2. System calls (functions provided by the kernel)
3. Library calls (functions within program libraries)
4. Special files (usually found in /dev)

运行以下 man 命令以及系统调用名称以查看该系统调用的文档

man 2 execve

根据 execve 手册页,这将执行在参数中传递的程序(在本例中,它是 ls)。可以为 ls 提供其他参数,例如本示例中的 testdir。因此,此系统调用只是使用 testdir 作为参数运行 ls

'execve - execute program'

'DESCRIPTION
       execve()  executes  the  program  pointed to by filename'

下一个名为 stat 的系统调用使用 testdir 参数

stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0

使用 man 2 stat 访问文档。 stat 是获取文件状态的系统调用——请记住,Linux 中的所有内容都是文件,包括目录。

接下来,openat 系统调用打开 testdir。 注意返回的 3。这是文件描述符,稍后的系统调用将使用它

openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3

到目前为止,一切顺利。现在,打开 trace.log 文件并转到 openat 系统调用之后的行。您将看到 getdents 系统调用被调用,它完成了执行 ls testdir 命令所需的大部分工作。现在,从 trace.log 文件中 grep getdents

[root@sandbox tmp]# grep getdents trace.log 
getdents(3, /* 4 entries */, 32768)     = 112
getdents(3, /* 0 entries */, 32768)     = 0
[root@sandbox tmp]# 

getdents 手册页将其描述为 获取目录条目,这正是您想要做的。请注意,getdents 的参数是 3,它是来自上面 openat 系统调用的文件描述符。

现在您有了目录列表,您需要一种在终端中显示它的方法。因此,grep 搜索另一个系统调用 write,它用于在日志中写入终端

[root@sandbox tmp]# grep write trace.log
write(1, "file1  file2\n", 13)          = 13
[root@sandbox tmp]# 

在这些参数中,您可以看到将显示的文件名:file1file2。关于第一个参数 (1),请记住在 Linux 中,当任何进程运行时,默认情况下会为其打开三个文件描述符。以下是默认文件描述符

  • 0 - 标准输入
  • 1 - 标准输出
  • 2 - 标准错误

因此,write 系统调用正在标准显示器(即终端,由 1 标识)上显示 file1file2

现在您知道哪些系统调用为 ls testdir/ 命令完成了大部分工作。但是 trace.log 文件中的其他 100 多个系统调用呢?操作系统必须做很多内务处理才能运行进程,因此您在日志文件中看到的很多内容都是进程初始化和清理。阅读整个 trace.log 文件,尝试了解发生了什么使 ls 命令工作。

现在您知道如何分析给定命令的系统调用,您可以将此知识用于其他命令,以了解正在执行哪些系统调用。 strace 提供了许多有用的命令行标志,使您可以更轻松地使用它,下面介绍其中一些标志。

默认情况下,strace 不包括所有系统调用信息。但是,它有一个方便的 -v verbose 选项,可以提供有关每个系统调用的其他信息

strace -v ls testdir

在运行 strace 命令时,始终使用 -f 选项是一个好习惯。它允许 strace 跟踪当前正在跟踪的进程创建的任何子进程

strace -f ls testdir

假设您只想知道系统调用的名称、它们运行的次数以及每个系统调用花费的时间百分比。您可以使用 -c 标志来获取这些统计信息

strace -c ls testdir/

假设您想专注于特定的系统调用,例如专注于 open 系统调用而忽略其余的。您可以使用 -e 标志,后跟系统调用名称

[root@sandbox tmp]# strace -e open ls testdir
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
file1  file2
+++ exited with 0 +++
[root@sandbox tmp]# 

如果您想专注于多个系统调用怎么办?不用担心,您可以使用相同的 -e 命令行标志,并在两个系统调用之间用逗号分隔。例如,要查看 writegetdents 系统调用

[root@sandbox tmp]# strace -e write,getdents ls testdir
getdents(3, /* 4 entries */, 32768)     = 112
getdents(3, /* 0 entries */, 32768)     = 0
write(1, "file1  file2\n", 13file1  file2
)          = 13
+++ exited with 0 +++
[root@sandbox tmp]# 

到目前为止的示例已跟踪显式运行的命令。但是,已经运行并且正在执行的命令呢?例如,如果您想跟踪只是长期运行的进程的守护程序怎么办?为此,strace 提供了一个特殊的 -p 标志,您可以向其提供进程 ID。

不要在守护程序上运行 strace,而是以 cat 命令为例,如果您提供文件名作为参数,它通常会显示文件的内容。如果未给出任何参数,则 cat 命令只是在终端等待用户输入文本。输入文本后,它会重复给定的文本,直到用户按下 Ctrl+C 退出。

从一个终端运行 cat 命令;它将显示一个提示符并在那里等待(记住 cat 仍在运行且尚未退出)

[root@sandbox tmp]# cat

从另一个终端,使用 ps 命令查找进程标识符 (PID)

[root@sandbox ~]# ps -ef | grep cat
root      22443  20164  0 14:19 pts/0    00:00:00 cat
root      22482  20300  0 14:20 pts/1    00:00:00 grep --color=auto cat
[root@sandbox ~]# 

现在,使用 -p 标志和 PID(您上面使用 ps 找到的 PID)在正在运行的进程上运行 strace。运行 strace 后,输出声明了进程附加到的内容以及 PID 号。现在,strace 正在跟踪 cat 命令发出的系统调用。您看到的第一个系统调用是 read,它正在等待来自 0 或标准输入(即运行 cat 命令的终端)的输入

[root@sandbox ~]# strace -p 22443
strace: Process 22443 attached
read(0,

现在,移回您将 cat 命令留在运行状态的终端,然后输入一些文本。我输入了 x0x0 用于演示目的。请注意 cat 如何简单地重复了我输入的内容;因此,x0x0 出现了两次。我输入了第一个,第二个是 cat 命令重复的输出

[root@sandbox tmp]# cat
x0x0
x0x0

移回 strace 附加到 cat 进程的终端。您现在看到两个额外的系统调用:较早的 read 系统调用,现在在终端中读取 x0x0 ,以及另一个用于 write 的系统调用,它将 x0x0 写回终端,以及再次新的 read 系统调用,它正在等待从终端读取。请注意,标准输入 (0) 和标准输出 (1) 都在同一个终端中

[root@sandbox ~]# strace -p 22443
strace: Process 22443 attached
read(0, "x0x0\n", 65536)                = 5
write(1, "x0x0\n", 5)                   = 5
read(0, 

想象一下,当针对守护程序运行 strace 以查看其在后台执行的所有操作时,这有多么有用。按 Ctrl+C 杀死 cat 命令;这也将杀死您的 strace 会话,因为该进程不再运行。

如果您想查看所有系统调用的时间戳,只需将 -t 选项与 strace 一起使用即可

[root@sandbox ~]#strace -t ls testdir/

14:24:47 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
14:24:47 brk(NULL)                      = 0x1f07000
14:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2530bc8000
14:24:47 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
14:24:47 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

如果您想知道系统调用之间花费的时间怎么办? strace 有一个方便的 -r 命令,可以显示执行每个系统调用所花费的时间。非常有用,不是吗?

[root@sandbox ~]#strace -r ls testdir/

0.000000 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
0.000368 brk(NULL)                 = 0x1966000
0.000073 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b1155000
0.000047 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
0.000119 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

结论

strace 实用程序对于理解 Linux 上的系统调用非常方便。要了解其其他命令行标志,请参阅手册页和在线文档。

下一步阅读
标签
User profile image.
经验丰富的软件工程专业人士。主要兴趣是安全、Linux、恶意软件。喜欢在命令行工作。对底层软件和理解事物的工作原理感兴趣。此处表达的观点仅代表我个人,不代表我的雇主

2 条评论

我从未读过这种有效且独特的内容!保持更新……

嗨 Gaurav,这是一篇很棒的文章。非常简洁明了的解释,结合了常见的用例和 strace 的选项。我肯定会保存它以供将来参考。继续努力。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.