http://arthurchiao.art/blog/how-does-strace-work-zh/
本文翻译自 2016 年的一篇英文博客 How Does strace Work 。如果能看懂英文,我建议你阅读原文,或者和本文对照看。
阅读本文之前,强烈建议先阅读这篇之前的文章:
其中包含了本文所需的部分预备知识。
以下是译文。
本文介绍 strace
内部是如何工作的。我们会研究 strace
工具内部所依赖的 ptrace
系统调用,对其 API 层及内部实现进行分析,以弄清楚 strace
是如何获取被跟踪进程的(系统调用相关的)详细信息的。
ptrace
是什么ptrace
是一个系统调用,可以用来:
以上可以看出,ptrace
在跟踪和控制程序方面非常有用。strace
、 GDB等工具内部都用到了它。
可以通过它的 man page 查看更多信息。
本文将使用如下两个术语:
tracer 跟踪 tracee 的过程:
首先,attach 到 tracee 进程:调用 ptrace
,带 PTRACE_ATTACH
及 tracee 进程 ID 作为参数。
之后当 tracee 运行到系统调用函数时就会被内核暂停;对 tracer 来说,就像 tracee 收到了 SIGTRAP
信号而停下来一样。接下来 tracer 就可以查看这次系统调 用的参数,打印相关的信息。
然后,恢复 tracee 执行:再次调用 ptrace
,带 PTRACE_SYSCALL
和 tracee 进程 ID。 tracee 会继续运行,进入到系统调用;在退出系统调用之前,再次被内核暂停。
以上“暂停-采集-恢复执行”过程不断重复,tracer 就可以获取每次系统调用的信息,打印 出参数、返回值、时间等等。
以上就是 ptrace 跟踪其他系统调用的大致过程,接下来看它在内核中具体是如何工作的。
内核的 ptrace
系统调用是一个很好的起点。接下来的代码会基于内核 3.13,并提供 github 的代码连接。
整个 ptrace
系统的代码见 kernel/ptrace.c。
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;
}
ptrace_attach
这个函数做的事情:
ptrace
flags
变量__ptrace_may_access
做一些安全检查然后,将 flags
赋值给 tracee 进程的内核结构体变量上(struct task_struct *task
),并停止 tracee。
在我们的例子中,这个 flags
的值为 PT_PTRACED
。
函数结束后,执行回到 ptrace
。
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
。
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 被唤醒,直到它遇到下一个系统调用。
到目前为止,我们已经通过设置内核结构体变量 struct task_struct *task
的 TIF_SYSCALL_TRACE
来使内核跟踪指定进程的系统调用。
那么:设置的参数是何时被检查和使用的呢?
程序发起一个系统调用时,在系统调用执行之前,会执行一段 CPU 相关的内核代码。在 x86 平台上,这段代码位于 arch/x86/kernel/entry_64.S。
_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
。
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。
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
。
SIGTRAP
tracee 一旦收到 SIGTRAP 信号就停止执行,tracer 会收到通知说有信号待处理。接下来 tracer 就可以查看 tracee 的状态,打印寄存器的值、时间戳等等信息。
当你用 strace
工具跟踪进程时,屏幕上的输出就是这么来的。
syscall_trace_leave
退出系统调用的过程与此类似:
这就是 tracee 的系统调用完成时,tracer 如何获取返回值、时间戳等等信息以打印输出的。
ptrace
系统调用对调试器、跟踪器和其他的从进程中提取信息的程序非常有用,strace
主要就是基于 ptrace
实现的。
ptrace
内部略微有些复杂,因为执行过程在一些文件之间跳来跳去,但总体来说,实现还是挺简单直接的。
我建议你也看一看你最喜欢的调试器的源码,看它是如何基于 ptrace
来完成检查程序状态、修改寄存器和内存等工作的。
如果对本文感兴趣,那么你可能对我们的以下文章也感兴趣: