[转帖][译] strace 是如何工作的(2016)

strace,如何,工作 · 浏览次数 : 0

小编点评

**race 代码流程** 1. **ptrace_request** 函数调用 **ptrace_check_attach** 检查可以对 tracee 进程进行操作。 2. **ptrace_check_attach** 调用 **arch_ptrace** 函数,里面包含 CPU 相关的代码。 3. **arch_ptrace** 函数没有特殊处理 for **PTRACE_ATTACH**,直接调用到 **ptrace_request** 函数。 4. **ptrace_request** 调用 **ptrace_resume** 函数,该函数首先给 tracee 的内核结构体变量**task 设置** TIF_SYSCALL_TRACE** flag。 5. **ptrace_resume** 检查几种可能的状态,最后 tracee 被唤醒,直到它遇到下一个系统调用。 6. **syscall_trace_enter** 函数定义在 **arch/x86/kernel/ptrace.c** 中,它处理 **PTRACE_SYSCALL** 系统调用。 7. **tracehook_report_syscall_entry** 是 **tracesys** 函数的静态内联函数,它处理 **PTRACE_SYSCALL** 系统调用。 8. **ptrace_report_syscall** 函数在 **kernel/signal.c** 中定义,当 tracee 进入系统调用时生成一个 SIGTRAP 信号,该信号会导致 **tracehook_report_syscall_entry** 函数被调用。 9. **SIGTRAPtracee** 一旦收到 SIGTRAP 信号就停止执行,tracer 会收到通知说有信号待处理。 10. **ptrace_do_notify** 函数初始化一个 **siginfo_t info 变量,交给 **ptrace_stop** 函数处理。 11. **ptrace_stop** 函数处理 SIGTRAP 信号,并返回给跟踪器的信息。 12. **syscall_trace_leave** 函数退出系统调用的过程,调用 **tracehook_report_syscall_exit** 和 **ptrace_report_syscall** 来获取返回值和信息。

正文

http://arthurchiao.art/blog/how-does-strace-work-zh/

 

译者序

本文翻译自 2016 年的一篇英文博客 How Does strace Work 。如果能看懂英文,我建议你阅读原文,或者和本文对照看。

阅读本文之前,强烈建议先阅读这篇之前的文章:

  1. (译) Linux 系统调用权威指南

其中包含了本文所需的部分预备知识。

以下是译文。


太长不读(TL;DR)

本文介绍 strace 内部是如何工作的。我们会研究 strace 工具内部所依赖的 ptrace 系统调用,对其 API 层及内部实现进行分析,以弄清楚 strace 是如何获取被跟踪进程的(系统调用相关的)详细信息的。

ptrace 是什么

ptrace 是一个系统调用,可以用来:

  1. 跟踪(其他)系统调用
  2. 读写内存和寄存器
  3. 控制(manipulate)被跟踪进程的信号传送(signal delivery)

以上可以看出,ptrace 在跟踪和控制程序方面非常有用。strace、 GDB等工具内部都用到了它。

可以通过它的 man page 查看更多信息。

2 跟踪过程

本文将使用如下两个术语:

  1. tracer:跟踪(其他程序的)程序
  2. tracee:被跟踪程序

tracer 跟踪 tracee 的过程:

首先,attach 到 tracee 进程:调用 ptrace,带 PTRACE_ATTACH 及 tracee 进程 ID 作为参数。

之后当 tracee 运行到系统调用函数时就会被内核暂停;对 tracer 来说,就像 tracee 收到了 SIGTRAP 信号而停下来一样。接下来 tracer 就可以查看这次系统调 用的参数,打印相关的信息。

然后,恢复 tracee 执行:再次调用 ptrace,带 PTRACE_SYSCALL 和 tracee 进程 ID。 tracee 会继续运行,进入到系统调用;在退出系统调用之前,再次被内核暂停。

以上“暂停-采集-恢复执行”过程不断重复,tracer 就可以获取每次系统调用的信息,打印 出参数、返回值、时间等等。

以上就是 ptrace 跟踪其他系统调用的大致过程,接下来看它在内核中具体是如何工作的。

3 内核实现

内核的 ptrace 系统调用是一个很好的起点。接下来的代码会基于内核 3.13,并提供 github 的代码连接。

整个 ptrace 系统的代码见 kernel/ptrace.c

3.1 PTRACE_ATTACH 代码流程

首先看 PTRACE_ATTACH 干了什么事情。

检查 request 参数,然后调用 ptrace_attach

if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) {
  ret = ptrace_attach(child, request, addr, data);
  /*
   * Some architectures need to do book-keeping after
   * a ptrace attach.
   */
  if (!ret)
    arch_ptrace_attach(child);
  goto out_put_task_struct;
}

3.1.1 ptrace_attach

这个函数做的事情:

  1. 初始化一个 ptrace flags 变量
  2. 确保 tracee 不是内核线程
  3. 确保 tracee 不是当前进程的一个线程
  4. 通过 __ptrace_may_access 做一些安全检查

然后,将 flags 赋值给 tracee 进程的内核结构体变量上(struct task_struct *task),并停止 tracee

在我们的例子中,这个 flags 的值为 PT_PTRACED

函数结束后,执行回到 ptrace

3.1.2 从 ptrace_attach 返回到 ptrace

接下来,ptrace 调用 ptrace_check_attach 来检查是否可以操作 tracee 了。

最后, ptrace 调用 CPU 相关的 arch_ptrace 函数。对于 x86 平台,这个函数在 arch/x86/kernel/ptrace.c 中,见这里。如果你看完了代码里巨长的 switch 语句,会发现并没有对应 PTRACE_ATTACH 的 case,这说明这种 case 走的是 default 分支。default 分支做的事情就是调用 ptrace_request 函数,然后回到 ptrace 代码。

ptrace_request 也没有对 PTRACE_ATTACH 做特殊处理,接下来的代码就是一路返回到 ptrace 系统调用,然后再从 ptrace 函数返回。

以上就是 PTRACE_ATTACH 的工作流程。接下来看 PTRACE_SYSCALL

3.2 PTRACE_SYSCALL 代码流程

首先会调用 ptrace_check_attach 以确保可以对 tracee 进程进行操作。

接下来和 attach 部分类似,调用 arch_ptrace 函数,里面包含 CPU 相关的代码。同样的, arch_ptrace 也没有什么需要为 PTRACE_SYSCALL 做的,直接调用到 ptrace_request

到目前为止,流程和 attach 过程都是类似的,但接下来就不一样了。在 ptrace_request 中,针对 PTRACE_SYSCALL,调用了 ptrace_resume 函数。

该函数首先给 tracee 的内核结构体变量 task 设置 TIF_SYSCALL_TRACE flag。

接下来检查几种可能的状态(因为其他函数可能也在调用 ptrace_resume),最后 tracee 被唤醒,直到它遇到下一个系统调用。

4 进入系统调用

到目前为止,我们已经通过设置内核结构体变量 struct task_struct *task 的 TIF_SYSCALL_TRACE 来使内核跟踪指定进程的系统调用。

那么:设置的参数是何时被检查和使用的呢?

程序发起一个系统调用时,在系统调用执行之前,会执行一段 CPU 相关的内核代码。在 x86 平台上,这段代码位于 arch/x86/kernel/entry_64.S

4.1 _TIF_WORK_SYSCALL_ENTRY

如果查看汇编函数 system_call,会看到它会检查一个 _TIF_WORK_SYSCALL_ENTRY flag:

  testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)
  jnz tracesys

如果设置了这个 flag,执行会转到 tracesys 函数。

这个 flag 的定义:

/* work to do in syscall_trace_enter() */
#define _TIF_WORK_SYSCALL_ENTRY \
        (_TIF_SYSCALL_TRACE | _TIF_SYSCALL_EMU | _TIF_SYSCALL_AUDIT |   \
         _TIF_SECCOMP | _TIF_SINGLESTEP | _TIF_SYSCALL_TRACEPOINT |     \
         _TIF_NOHZ)

可以看到这个 flag 其实就是多个 flag 的组合,其中包括我们之前设置的那个:_TIF_SYSCALL_TRACE

到这里就明白了,如果进程的内核结构体变量设置了 _TIF_SYSCALL_TRACE,到这里就会检测到,然后执行转到 tracesys

4.2 tracesys

代码会调用 syscall_trace_enter。这个函数定义在 arch/x86/kernel/ptrace.c,是 CPU 相关的代码,可以查看这里

代码如果检测到设置了 _TIF_SYSCALL_TRACE flag,就会调用 tracehook_report_syscall_entry

  if ((ret || test_thread_flag(TIF_SYSCALL_TRACE)) &&
      tracehook_report_syscall_entry(regs))
    ret = -1L;

tracehook_report_syscall_entry 是一个静态内联函数,定义在 include/linux/tracehook.h,有很好的文档

它接下来又调用了 ptrace_report_syscall

4.3 ptrace_report_syscall

这个函数符合之前我们描述过的:当 tracee 进入系统调用时生成一个 SIGTRAP 信号:

ptrace_notify(SIGTRAP | ((ptrace & PT_TRACESYSGOOD) ? 0x80 : 0));

其中 ptrace_notify 定义在 kernel/signal.c。 它会进一步调用 ptrace_do_notify,后者会初始化一个 siginfo_t info 变量,交给 ptrace_stop

4.4 SIGTRAP

tracee 一旦收到 SIGTRAP 信号就停止执行,tracer 会收到通知说有信号待处理。接下来 tracer 就可以查看 tracee 的状态,打印寄存器的值、时间戳等等信息。

当你用 strace 工具跟踪进程时,屏幕上的输出就是这么来的。

4.5 syscall_trace_leave

退出系统调用的过程与此类似:

  1. 汇编代码 调用 syscall_trace_leave
  2. 这个函数 调用 tracehook_report_syscall_exit
  3. 继续调用 ptrace_report_syscall

这就是 tracee 的系统调用完成时,tracer 如何获取返回值、时间戳等等信息以打印输出的。

6 结束语

ptrace 系统调用对调试器、跟踪器和其他的从进程中提取信息的程序非常有用,strace 主要就是基于 ptrace 实现的。

ptrace 内部略微有些复杂,因为执行过程在一些文件之间跳来跳去,但总体来说,实现还是挺简单直接的。

我建议你也看一看你最喜欢的调试器的源码,看它是如何基于 ptrace 来完成检查程序状态、修改寄存器和内存等工作的。

7 我们的相关文章

如果对本文感兴趣,那么你可能对我们的以下文章也感兴趣:

  1. (译) Linux 系统调用权威指南
  2. (译) ltrace 是如何工作的

与[转帖][译] strace 是如何工作的(2016)相似的内容:

[转帖][译] strace 是如何工作的(2016)

http://arthurchiao.art/blog/how-does-strace-work-zh/ 译者序 本文翻译自 2016 年的一篇英文博客 How Does strace Work 。如果能看懂英文,我建议你阅读原文,或者和本文对照看。 阅读本文之前,强烈建议先阅读这篇之前的文章: (

[转帖][译] Cilium 未来数据平面:支撑 100Gbit/s k8s 集群(KubeCon, 2022)

http://arthurchiao.art/blog/cilium-tomorrow-networking-data-plane-zh/ 作者写的非常好呢 基础支持的确非常重要呢. Published at 2022-11-12 | Last Update 2022-11-12 译者序 本文翻译自

[转帖][译] Cilium:基于 BPF+EDT+FQ+BBR 实现更好的带宽管理(KubeCon, 2022)

http://arthurchiao.art/blog/better-bandwidth-management-with-ebpf-zh/ Published at 2022-10-30 | Last Update 2022-10-30 译者序 本文翻译自 KubeCon+CloudNativeCo

[转帖][译] Linux Socket Filtering (LSF, aka BPF)(KernelDoc,2021)

http://arthurchiao.art/blog/linux-socket-filtering-aka-bpf-zh/ 译者序 本文翻译自 2021 年 Linux 5.10 内核文档: Linux Socket Filtering aka Berkeley Packet Filter (BP

[转帖][译] 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间(EDT)的演进(Google, 2018)

http://arthurchiao.art/blog/traffic-control-from-queue-to-edt-zh/ 译者序 本文组合翻译了 Google 2018 年两篇分享中的技术部分,二者讲的同一件事情,但层次侧重不同: Netdev 2018: Evolving from AF

[转帖][译] Linux 网络栈监控和调优:接收数据(2016)

http://arthurchiao.art/blog/tuning-stack-rx-zh/ 注意:本文内容已经太老,基于 kernel 3.13 和 1Gbps 网卡驱动 igb,建议移步 kernel 5.10 + 25Gbps 驱动版: Linux 网络栈原理、监控与调优:前言 Linux

[转帖][译] Linux 系统调用权威指南(2016)

http://arthurchiao.art/blog/system-call-definitive-guide-zh/ 译者序 本文翻译自 2016 年的一篇英文博客 The Definitive Guide to Linux System Calls 。如果能看懂英文,我建议你阅读原文,或者和本

[转帖][译] RFC 1180:朴素 TCP/IP 教程(1991)

http://arthurchiao.art/blog/rfc1180-a-tcp-ip-tutorial-zh/ 译者序 本文翻译自 1991 年的一份 RFC(1180): A TCP/IP Tutorial。 本文虽距今将近 20 年,但内容并未过时,这不禁让人惊叹于 TCP/IP 协议栈生命

[转帖][译] 大规模微服务利器:eBPF + Kubernetes(KubeCon, 2020)

http://arthurchiao.art/blog/ebpf-and-k8s-zh/ Published at 2020-09-06 | Last Update 2021-02-21 译者序 本文翻译自 2020 年 Daniel Borkmann 在 KubeCon 的一篇分享: eBPF a

[转帖][译] Linux 网络栈监控和调优:发送数据(2017)

http://arthurchiao.art/blog/tuning-stack-tx-zh/ 译者序 本文翻译自 2017 年的一篇英文博客 Monitoring and Tuning the Linux Networking Stack: Sending Data。如果能看懂英文,建议阅读原文,