Linux 中的 Strace:历史、设计和使用

Linux 中的 Strace:历史、设计和使用

在类 Unix 操作系统中,程序与外界和操作系统的通信是通过一小组函数(系统调用)进行的。 这意味着出于调试目的,监视进程正在执行的系统调用可能很有用。

一个实用程序可帮助您监控 Linux 上程序的“亲密生活” strace,这就是本文的主题。 间谍设备的使用示例附有简要历史 strace 以及此类程序设计的描述。

内容

物种起源

Unix中程序与OS内核之间的主要接口是系统调用。 系统调用, 系统调用),程序与外界的交互完全通过它们发生。

但在 Unix 的第一个公共版本中(Unix版本6,1975)没有方便的方法来跟踪用户进程的行为。 为了解决这个问题,贝尔实验室将更新到下一个版本(Unix版本7,1979)提出了一个新的系统调用 - ptrace.

ptrace 主要是为了交互式调试器而开发的,但到了 80 年代末(商业化时代) 系统 V 版本 4)在此基础上,出现了狭隘的调试器——系统调用跟踪器——并得到广泛使用。

第一 Paul Cronenburg 于 1992 年在 comp.sources.sun 邮件列表上发布了 strace 的相同版本,作为封闭实用程序的替代品 trace 来自太阳。 克隆版和原版都是针对 SunOS 的,但到了 1994 年 strace 被移植到 System V、Solaris 和日益流行的 Linux。

目前strace仅支持Linux并且依赖相同的 ptrace,长满了许多扩展。

现代(并且非常活跃)的维护者 strace - 德米特里·莱文。 感谢他,该实用程序获得了高级功能,例如系统调用中的错误注入、对各种体系结构的支持,最重要的是, 吉祥物。 非官方消息称,这个选择落在了鸵鸟身上,因为俄语单词“ostrich”和英语单词“strace”之间的谐音。

同样重要的是,尽管 ptrace 系统调用和跟踪器在 Linux、FreeBSD、OpenBSD 和传统 Unix 中有着悠久的历史和实现,但从未包含在 POSIX 中。

Strace 设备简而言之:Piglet Trace

“你不应该理解这一点”(Dennis Ritchie,版本 6 Unix 源代码中的评论)

从孩提时代起,我就无法忍受黑匣子:我不玩玩具,而是试图理解它们的结构(大人用“打破”这个词,但不要相信邪恶的舌头)。 也许这就是为什么第一个 Unix 的非正式文化和现代开源运动如此接近我。

出于本文的目的,反汇编已经增长了数十年的 strace 源代码是不合理的。 但不应该给读者留下任何秘密。 因此,为了展示此类 strace 程序的运行原理,我将提供微型跟踪器的代码 - 仔猪踪迹 (指针)。 它不知道如何做任何特别的事情,但主要的是程序的系统调用 - 它输出:

$ gcc examples/piglet-trace.c -o ptr
$ ptr echo test > /dev/null
BRK(12) -> 94744690540544
ACCESS(21) -> 18446744073709551614
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694657216512
CLOSE(3) -> 0
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
READ(0) -> 832
FSTAT(5) -> 0
MMAP(9) -> 140694657208320
MMAP(9) -> 140694650953728
MPROTECT(10) -> 0
MMAP(9) -> 140694655045632
MMAP(9) -> 140694655070208
CLOSE(3) -> 0
unknown(158) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MUNMAP(11) -> 0
BRK(12) -> 94744690540544
BRK(12) -> 94744690675712
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694646390784
CLOSE(3) -> 0
FSTAT(5) -> 0
IOCTL(16) -> 18446744073709551591
WRITE(1) -> 5
CLOSE(3) -> 0
CLOSE(3) -> 0
unknown(231)
Tracee terminated

Piglet Trace 可识别大约数百个 Linux 系统调用(请参阅。 )并且仅适用于 x86-64 架构。 这对于教育目的来说已经足够了。

让我们看看克隆人的工作。 对于 Linux,调试器和跟踪器使用如上所述的 ptrace 系统调用。 它的工作原理是传入第一个参数命令标识符,我们只需要它 PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

跟踪器以通常的 Unix 风格启动: fork(2) 启动一个子进程,该子进程又使用 exec(3) 启动正在研究的计划。 这里唯一的微妙之处是挑战 ptrace(PTRACE_TRACEME)exec:子进程期望父进程监控它:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    /* Child here */
    /* A traced mode has to be enabled. A parent will have to wait(2) for it
     * to happen. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Replace itself with a program to be run. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

父进程现在应该调用 wait(2) 在子进程中,即确保已切换到跟踪模式:

/* Parent */

/* First we wait for the child to set the traced mode (see
 * ptrace(PTRACE_TRACEME) above) */
if (waitpid(child_pid, NULL, 0) == -1)
    err(EXIT_FAILURE, "traceme -> waitpid");

至此,准备工作就完成了,可以直接无限循环地跟踪系统调用了。

通话 ptrace(PTRACE_SYSCALL) 保证后续 wait Parent 将在执行系统调用之前或在系统调用完成后立即完成。 在两次调用之间,您可以执行任何操作:用替代调用替换调用、更改参数或返回值。

我们只需要调用该命令两次 ptrace(PTRACE_GETREGS)获取寄存器状态 rax 在调用之前(系统调用号)和之后(返回值)。

实际上,循环:

/* A system call tracing loop, one interation per call. */
for (;;) {
    /* A non-portable structure defined for ptrace/GDB/strace usage mostly.
     * It allows to conveniently dump and access register state using
     * ptrace. */
    struct user_regs_struct registers;

    /* Enter syscall: continue execution until the next system call
     * beginning. Stop right before syscall.
     *
     * It's possible to change the system call number, system call
     * arguments, return value or even avoid executing the system call
     * completely. */
  if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
      err(EXIT_FAILURE, "enter_syscall");
  if (waitpid(child_pid, NULL, 0) == -1)
      err(EXIT_FAILURE, "enter_syscall -> waitpid");

  /* According to the x86-64 system call convention on Linux (see man 2
   * syscall) the number identifying a syscall should be put into the rax
   * general purpose register, with the rest of the arguments residing in
   * other general purpose registers (rdi,rsi, rdx, r10, r8, r9). */
  if (ptrace(PTRACE_GETREGS, child_pid, NULL, &registers) == -1)
      err(EXIT_FAILURE, "enter_syscall -> getregs");

  /* Note how orig_rax is used here. That's because on x86-64 rax is used
   * both for executing a syscall, and returning a value from it. To
   * differentiate between the cases both rax and orig_rax are updated on
   * syscall entry/exit, and only rax is updated on exit. */
  print_syscall_enter(registers.orig_rax);

  /* Exit syscall: execute of the syscall, and stop on system
   * call exit.
   *
   * More system call tinkering possible: change the return value, record
   * time it took to finish the system call, etc. */
  if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
      err(EXIT_FAILURE, "exit_syscall");
  if (waitpid(child_pid, NULL, 0) == -1)
      err(EXIT_FAILURE, "exit_syscall -> waitpid");

  /* Retrieve register state again as we want to inspect system call
   * return value. */
  if (ptrace(PTRACE_GETREGS, child_pid, NULL, &registers) == -1) {
      /* ESRCH is returned when a child terminates using a syscall and no
       * return value is possible, e.g. as a result of exit(2). */
      if (errno == ESRCH) {
          fprintf(stderr, "nTracee terminatedn");
          break;
      }
      err(EXIT_FAILURE, "exit_syscall -> getregs");
  }

  /* Done with this system call, let the next iteration handle the next
   * one */
  print_syscall_exit(registers.rax);
}

这就是整个追踪器。 现在您知道从哪里开始下一次移植 跟踪 在 Linux 上。

基础知识:运行运行 strace 的程序

作为第一个用例 strace,也许值得引用最简单的方法 - 启动正在运行的应用程序 strace.

为了不深入研究典型程序的无休止的调用列表,我们编写 最小程序 周围 write:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
    /* write(2) is a simple wrapper around a syscall so it should be easy to
     * find in the syscall trace. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

让我们构建该程序并确保它有效:

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout

最后,让我们在 strace 控制下运行它:

$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL)                               = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
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=197410, ...}) = 0
mmap(NULL, 197410, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7a2a633000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a631000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7a2a04c000
mprotect(0x7f7a2a233000, 2097152, PROT_NONE) = 0
mmap(0x7f7a2a433000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f7a2a433000
mmap(0x7f7a2a439000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a439000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f7a2a6324c0) = 0
mprotect(0x7f7a2a433000, 16384, PROT_READ) = 0
mprotect(0x55ff52b52000, 4096, PROT_READ) = 0
mprotect(0x7f7a2a664000, 4096, PROT_READ) = 0
munmap(0x7f7a2a633000, 197410)          = 0
write(1, "write me to stdoutn", 20write me to stdout
)  = 20
exit_group(0)                           = ?

非常“罗嗦”,而且没有什么教育意义。 这里有两个问题:程序输出与输出混合 strace 以及大量我们不感兴趣的系统调用。

您可以使用 -o 开关分隔程序的标准输出流和 strace 错误输出,该开关将系统调用列表重定向到参数文件。

仍然需要解决“额外”调用的问题。 假设我们只对调用感兴趣 write. 钥匙 -e 允许您指定过滤系统调用的表达式。 最流行的条件选项自然是, trace=*,您可以只留下我们感兴趣的电话。

同时使用时 -o и -e 我们将得到:

$ strace -e trace=write -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20
)  = 20
+++ exited with 0 +++

所以,你看,它更容易阅读。

您还可以删除系统调用,例如与内存分配和释放相关的系统调用:

$ strace -e trace=!brk,mmap,mprotect,munmap -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
execve("./write-simple", ["./write-simple"], 0x7ffe9972a498 /* 69 vars */) = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
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=124066, ...}) = 0
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f00f0be74c0) = 0
write(1, "write me to stdoutn", 20)  = 20
exit_group(0)                           = ?
+++ exited with 0 +++

请注意排除的调用列表中的转义感叹号:这是命令 shell 所必需的。 ).

在我的 glibc 版本中,系统调用终止进程 exit_group,不是传统的 _exit。 这就是使用系统调用的困难:程序员使用的接口与系统调用没有直接关系。 此外,它会根据实施和平台定期变化。

基础知识:即时加入流程

最初,它所基于的 ptrace 系统调用 strace,只能在特殊模式下运行程序时使用。 在 Unix 版本 6 的时代,这个限制听起来可能很合理。 如今,这已经不够了:有时您需要调查工作程序的问题。 一个典型的例子是进程在句柄上被阻塞或休眠。 因此现代 strace 可以即时加入流程。

冻结示例 节目:

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str[] = "write men";

    write(STDOUT_FILENO, str, sizeof(str));

    /* Sleep indefinitely or until a signal arrives */
    pause();

    write(STDOUT_FILENO, str, sizeof(str));

    return EXIT_SUCCESS;
}

让我们构建该程序并确保它已冻结:

$ gcc examples/write-sleep.c -o write-sleep
$ ./write-sleep
./write-sleep
write me
^C
$

现在让我们尝试加入它:

$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
 <detached ...>

程序被调用阻止 pause。 让我们看看她对信号有何反应:

$ strace -o write-sleep.log -p 15329 &
strace: Process 15329 attached
$
$ kill -CONT 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause(
$
$ kill -TERM 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
+++ killed by SIGTERM +++

我们启动了冻结程序并使用以下方式加入它 strace。 有两件事变得很清楚:暂停系统调用会忽略没有处理程序的信号,更有趣的是,strace 不仅监视系统调用,还监视传入信号。

示例:跟踪子进程

通过调用处理流程 fork - 所有 Unix 的基础。 让我们通过简单的“繁殖”示例来了解 strace 如何与进程树一起工作 节目:

int main(int argc, char *argv[])
{
    pid_t parent_pid = getpid();
    pid_t child_pid = fork();
    if (child_pid == 0) {
        /* A child is born! */
        child_pid = getpid();

        /* In the end of the day printf is just a call to write(2). */
        printf("child (self=%d)n", child_pid);
        exit(EXIT_SUCCESS);
    }

    printf("parent (self=%d, child=%d)n", parent_pid, child_pid);

    wait(NULL);

    exit(EXIT_SUCCESS);
}

这里原始进程创建一个子进程,两者都写入标准输出:

$ gcc examples/fork-write.c -o fork-write
$ ./fork-write
parent (self=11274, child=11275)
child (self=11275)

默认情况下,我们只会看到来自父进程的系统调用:

$ strace -e trace=write -ofork-write.log ./fork-write
child (self=22049)
parent (self=22048, child=22049)
$ cat fork-write.log
write(1, "parent (self=22048, child=22049)"..., 33) = 33
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22049, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

该标志可帮助您跟踪整个进程树 -f用哪个 strace 监视子进程中的系统调用。 这会添加到输出的每一行 pid 产生系统输出的过程:

$ strace -f -e trace=write -ofork-write.log ./fork-write
parent (self=22710, child=22711)
child (self=22711)
$ cat fork-write.log
22710 write(1, "parent (self=22710, child=22711)"..., 33) = 33
22711 write(1, "child (self=22711)n", 19) = 19
22711 +++ exited with 0 +++
22710 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22711, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
22710 +++ exited with 0 +++

在这种情况下,按系统调用组进行过滤可能会很有用:

$ strace -f -e trace=%process -ofork-write.log ./fork-write
parent (self=23610, child=23611)
child (self=23611)
$ cat fork-write.log
23610 execve("./fork-write", ["./fork-write"], 0x7fff696ff720 /* 63 vars */) = 0
23610 arch_prctl(ARCH_SET_FS, 0x7f3d03ba44c0) = 0
23610 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3d03ba4790) = 23611
23610 wait4(-1,  <unfinished ...>
23611 exit_group(0)                     = ?
23611 +++ exited with 0 +++
23610 <... wait4 resumed> NULL, 0, NULL) = 23611
23610 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23611, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
23610 exit_group(0)                     = ?
23610 +++ exited with 0 +++

顺便问一下,创建新进程使用什么系统调用?

示例:文件路径而不是句柄

了解文件描述符当然很有用,但程序访问的特定文件的名称也可以派上用场。

下一个 程序 将该行写入临时文件:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    char tmp_filename_template[] = "/tmp/output_fileXXXXXX";

    int out_fd = mkstemp(tmp_filename_template);
    if (out_fd == -1) {
        perror("mkstemp");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

正常通话时 strace 将显示传递给系统调用的描述符编号的值:

$ strace -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3, "write me to a filen", 20)  = 20
+++ exited with 0 +++

带着旗帜 -y 该实用程序显示描述符对应的文件的路径:

$ strace -y -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3</tmp/output_fileCf5MyW>, "write me to a filen", 20) = 20
+++ exited with 0 +++

示例:文件访问跟踪

另一个有用的功能:仅显示与特定文件关联的系统调用。 下一个 程序 将一行追加到作为参数传递的任意文件中:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    /*
     * Path will be provided by the first program argument.
     *  */
    const char *path = argv[1];

    /*
     * Open an existing file for writing in append mode.
     *  */
    int out_fd = open(path, O_APPEND | O_WRONLY);
    if (out_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

默认情况下, strace 显示很多不必要的信息。 旗帜 -P 带参数会导致 strace 仅打印对指定文件的调用:

$ strace -y -P/tmp/test_file.log -o write-file.log ./write-file /tmp/test_file.log
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = 3</tmp/test_file.log>
write(3</tmp/test_file.log>, "write me to a filen", 20) = 20
+++ exited with 0 +++

示例:多线程程序

效用 strace 在使用多线程时也可以提供帮助 该计划。 以下程序从两个流写入标准输出:

void *thread(void *arg)
{
    (void) arg;

    printf("Secondary thread: workingn");
    sleep(1);
    printf("Secondary thread: donen");

    return NULL;
}

int main(int argc, char *argv[])
{
    printf("Initial thread: launching a threadn");

    pthread_t thr;
    if (0 != pthread_create(&thr, NULL, thread, NULL)) {
        fprintf(stderr, "Initial thread: failed to create a thread");
        exit(EXIT_FAILURE);
    }

    printf("Initial thread: joining a threadn");
    if (0 != pthread_join(thr, NULL)) {
        fprintf(stderr, "Initial thread: failed to join a thread");
        exit(EXIT_FAILURE);
    };

    printf("Initial thread: done");

    exit(EXIT_SUCCESS);
}

当然,它必须使用对链接器的特殊问候语进行编译 - -pthread 标志:

$ gcc examples/thread-write.c -pthread -o thread-write
$ ./thread-write
/thread-write
Initial thread: launching a thread
Initial thread: joining a thread
Secondary thread: working
Secondary thread: done
Initial thread: done
$

-f与常规进程的情况一样,会将进程的 pid 添加到每行的开头。

当然,我们谈论的不是 POSIX 线程标准实现意义上的线程标识符,而是 Linux 中任务调度程序使用的编号。 从后者的角度来看,没有进程或线程——有需要在机器的可用核心之间分配的任务。

当在多线程中工作时,系统调用变得太多:

$ strace -f -othread-write.log ./thread-write
$ wc -l thread-write.log
60 thread-write.log

将自己限制在进程管理和系统调用上是有意义的 write:

$ strace -f -e trace="%process,write" -othread-write.log ./thread-write
$ cat thread-write.log
18211 execve("./thread-write", ["./thread-write"], 0x7ffc6b8d58f0 /* 64 vars */) = 0
18211 arch_prctl(ARCH_SET_FS, 0x7f38ea3b7740) = 0
18211 write(1, "Initial thread: launching a thre"..., 35) = 35
18211 clone(child_stack=0x7f38e9ba2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f38e9ba39d0, tls=0x7f38e9ba3700, child_tidptr=0x7f38e9ba39d0) = 18212
18211 write(1, "Initial thread: joining a thread"..., 33) = 33
18212 write(1, "Secondary thread: workingn", 26) = 26
18212 write(1, "Secondary thread: donen", 23) = 23
18212 exit(0)                           = ?
18212 +++ exited with 0 +++
18211 write(1, "Initial thread: done", 20) = 20
18211 exit_group(0)                     = ?
18211 +++ exited with 0 +++

顺便问一下问题。 使用什么系统调用来创建新线程? 对线程的调用与对进程的调用有何不同?

主类:系统调用时的进程堆栈

最近出现的其中之一 strace 功能 - 显示系统调用时的函数调用堆栈。 简单的 例子:

void do_write(void)
{
    char str[] = "write me to stdoutn";
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    do_write();
    return EXIT_SUCCESS;
}

自然地,程序输出变得非常庞大,并且,除了标志之外 -k (调用堆栈显示),按名称过滤系统调用是有意义的:

$ gcc examples/write-simple.c -o write-simple
$ strace -k -e trace=write -o write-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20)  = 20
 > /lib/x86_64-linux-gnu/libc-2.27.so(__write+0x14) [0x110154]
 > /home/vkazanov/projects-my/strace-post/write-simple(do_write+0x50) [0x78a]
 > /home/vkazanov/projects-my/strace-post/write-simple(main+0x14) [0x7d1]
 > /lib/x86_64-linux-gnu/libc-2.27.so(__libc_start_main+0xe7) [0x21b97]
 > /home/vkazanov/projects-my/strace-post/write-simple(_start+0x2a) [0x65a]
+++ exited with 0 +++

大师班:错误注入

还有一项非常有用的新功能:错误注入。 这里 程序,将两行写入输出流:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void do_write(const char *str, ssize_t len)
{
    if (len != write(STDOUT_FILENO, str, (size_t)len)){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str1[] = "write me 1n";
    do_write(str1, sizeof(str1));

    char str2[] = "write me 2n";
    do_write(str2, sizeof(str2));

    return EXIT_SUCCESS;
}

让我们跟踪两个写入调用:

$ gcc examples/write-twice.c -o write-twice
$ ./write-twice
write me 1
write me 2
$ strace -e trace=write -owrite-twice.log ./write-twice
write me 1
write me 2
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = 12
+++ exited with 0 +++

现在我们使用表达式 inject插入一个错误 EBADF 在所有写入调用中:

$ strace -e trace=write -e inject=write:error=EBADF -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = -1 EBADF (Bad file descriptor) (INJECTED)
+++ exited with 1 +++

返回的错误很有趣 所有 挑战 write,包括隐藏在 perror 后面的调用。 只有在第一次调用时返回错误才有意义:

$ strace -e trace=write -e inject=write:error=EBADF:when=1 -owrite-twice.log ./write-twice
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

或者第二个:

$ strace -e trace=write -e inject=write:error=EBADF:when=2 -owrite-twice.log ./write-twice
write me 1
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

没有必要指定错误类型:

$ strace -e trace=write -e fault=write:when=1 -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 ENOSYS (Function not implemented) (INJECTED)
write(3, "write: Function not implementedn", 32) = 32
+++ exited with 1 +++

与其他标志结合使用,您可以“中断”对特定文件的访问。 例子:

$ strace -y -P/tmp/test_file.log -e inject=file:error=ENOENT -o write-file.log ./write-file /tmp/test_file.log
open: No such file or directory
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory) (INJECTED)
+++ exited with 1 +++

除了错误注入之外, 人们可以 拨打电话或接收信号时会造成延迟。

后记

效用 strace - 一个简单而可靠的工具。 但除了系统调用之外,程序和操作系统运行的其他方面都是可以调试的。 例如,它可以跟踪对动态链接库的调用。 跟踪,他们可以查看操作系统的运行情况 系统抽头 и 跟踪,并允许您深入研究程序性能 PERF。 尽管如此,它是 strace - 当我自己和其他人的程序出现问题时,这是第一道防线,我每周至少使用几次。

简而言之,如果您喜欢 Unix,请阅读 man 1 strace 并随意查看您的程序!

来源: habr.com

添加评论