天天看点

使用strace了解Linux上的系统调用

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

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

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

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

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

陷阱机制。

通过使用系统库(在Linux系统上又称为glibc) ,大部分隐藏在用户面前 。 尽管系统调用本质上是通用的,但是发出系统调用的机制在很大程度上取决于机器。

本文通过使用一些通用命令并使用strace分析每个命令进行的系统调用来探索一些实际示例。 这些示例使用Red Hat Enterprise Linux,但是这些命令在其他Linux发行版上应该相同:

[[email protected] ~]# cat /etc/redhat-release 
     

Red Hat Enterprise Linux Server release 7.7 (Maipo)
     

[[email protected] ~]# 
     

[[email protected] ~]# uname -r
     

3.10.0-1062.el7.x86_64
     

[[email protected] ~]# 
    
              

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

[[email protected] ~]# rpm -qa | grep -i strace
     

strace-4.12-9.el7.x86_64
     

[[email protected] ~]# 
     

[[email protected] ~]# strace -V
     

strace -- version 4.12
     

[[email protected] ~]# 
    
              

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

就本示例而言,在/ tmp中创建一个测试目录,并使用touch命令使用以下命令创建两个文件:

[[email protected] ~]# cd /tmp/
     

[[email protected] tmp]# 
     

[[email protected] tmp]# mkdir testdir
     

[[email protected] tmp]# 
     

[[email protected] tmp]# touch testdir/file1
     

[[email protected] tmp]# touch testdir/file2
     

[[email protected] tmp]#
    
              

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

验证是否使用testdir目录上的ls命令创建了文件:

[[email protected] tmp]# ls testdir/
     

file1  file2
     

[[email protected] tmp]#
    
              

您可能每天都使用ls命令,而没有意识到系统调用在其下进行。 这里有抽象作用。 此命令的工作方式如下:

ls命令在Linux上从系统库(aka glibc )内部调用函数。 这些库调用完成大部分工作的系统调用。

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

如果未安装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 })                       
    
              

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

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

与上述类似,要了解调用了哪些系统调用,只需将strace放在ls testdir命令之前,如下所示。 再次,将一堆乱码丢到您的屏幕上,您可以按照以下步骤进行操作:

[[email protected] 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 +++
     

[[email protected] tmp]# 
    
              

运行strace命令后,屏幕上的输出只是运行ls命令的系统调用。 每个系统调用都为操作系统提供特定的用途,它们可以大致分为以下几节:

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

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

[[email protected] tmp]# strace -o trace.log ls testdir/
     

file1  file2
     

[[email protected] tmp]#
    
              

这次,没有任何输出转储到屏幕上-ls命令通过显示文件名并将所有输出记录到文件trace.log来按预期工作。 仅一个简单的ls命令,该文件就有近100行内容:

[[email protected] tmp]# ls -l trace.log 
     

-rw-r--r--. 1 root root 7809 Oct 12 13:52 trace.log
     

[[email protected] tmp]# 
     

[[email protected] tmp]# wc -l trace.log 
     

114 trace.log
     

[[email protected] tmp]# 
    
              

看一下示例的trace.log的第一行:

  • 该行的第一个单词execve是正在执行的系统调用的名称。
  • 括号内的文本是提供给系统调用的参数。
  • =符号后面的数字(在这种情况下为0 )是execve系统调用返回的值。

现在的输出似乎还不太吓人,不是吗? 您可以应用相同的逻辑来理解其他内容。

现在,将焦点集中在您所调用的单个命令上,即ls testdir 。 您知道命令ls所使用的目录名称,那么为什么不在trace.log文件中为testdir使用 grep并查看得到的结果呢? 详细查看结果的每一行:

[[email protected] 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
     

[[email protected] tmp]# 
    
              

回想上面对execve的分析,您能说一下这个系统调用做什么吗?

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

[[email protected] tmp]# rpm -qa | grep -i man-pages
     

man-pages-3.53-5.el7.noarch
     

[[email protected] 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命令以查看该系统调用的文档:

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

'execve - execute program'
     


     

'DESCRIPTION
     

       execve()  executes  the  program  pointed to by filename'
    
              

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

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

接下来, openat系统调用将打开testdir。 留意返回的3 。 这是一个文件描述,将在以后的系统调用中使用:

到目前为止,一切都很好。 现在,打开trace.log文件,并转到openat系统调用之后的行。 您将看到getdents系统调用被调用,该调用完成执行ls testdir命令所需的大部分操作。 现在,从trace.log文件中获取grep getdents :

[[email protected] tmp]# grep getdents trace.log 
     

getdents(3, /* 4 entries */, 32768)     = 112
     

getdents(3, /* 0 entries */, 32768)     = 0
     

[[email protected] tmp]# 
    
              

getdents手册页将其描述为get目录条目 ,这是您要执行的操作。 请注意, getdents的参数是3 ,这是来自上述openat系统调用的文件描述符 。

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

[[email protected] tmp]# grep write trace.log
     

write(1, "file1  file2\n", 13)          = 13
     

[[email protected] tmp]# 
    
              

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

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

因此, 写入系统调用在标准显示器(即终端,由1标识)上显示file1和file2 。

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

既然您知道如何分析给定命令的系统调用,那么您就可以将该知识用于其他命令来了解正在执行哪些系统调用。 strace提供了许多有用的命令行标志,使您更容易使用,下面将对其中的一些进行描述。

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

优良作法是在运行strace命令时始终使用-f选项。 它允许strace跟踪由当前正在跟踪的进程创建的任何子进程:

假设您只需要系统调用的名称,运行的次数以及每个系统调用花费的时间百分比。 您可以使用-c标志获取这些统计信息:

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

[[email protected] 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 +++
     

[[email protected] tmp]# 
    
              

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

[[email protected] 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 +++
     

[[email protected] tmp]# 
    
              

到目前为止,这些示例已明确跟踪了运行命令。 但是,已经运行并正在执行的命令又如何呢? 例如,如果要跟踪只是长时间运行的进程的守护程序,该怎么办? 为此, strace提供了一个特殊的-p标志,您可以向其提供进程ID。

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

从一个终端运行cat命令; 它会向您显示一个提示,只需在那里等待即可(记住cat仍在运行并且尚未退出):

在另一个终端上,使用ps命令找到进程标识符(PID):

[[email protected] ~]# 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
     

[[email protected] ~]# 
    
              

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

[[email protected] ~]# strace -p 22443
     

strace: Process 22443 attached
     

read(0,
    
              

现在,返回到让cat命令运行的终端,然后输入一些文本。 我出于演示目的输入了x0x0 。 注意cat是如何重复输入的内容。 因此, x0x0出现两次。 我输入了第一个,第二个是cat命令重复的输出:

[[email protected] tmp]# cat
     

x0x0
     

x0x0
    
              

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

[[email protected] ~]# 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会话。

如果要查看所有系统调用的时间戳,只需在strace中使用-t选项:

[[email protected] ~]#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命令,该命令显示执行每个系统调用所花费的时间。 非常有用,不是吗?

[[email protected] ~]#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上的系统调用。 要了解其其他命令行标志,请参考手册页和在线文档。

翻译自: https://opensource.com/article/19/10/strace