Linux ptrace系统调用

文章目录

  • [一、ptrace 简介](#一、ptrace 简介)
  • [二、ptrace 参数request](#二、ptrace 参数request)
    • [2.1 PTRACE_TRACEME](#2.1 PTRACE_TRACEME)
    • [2.2 PTRACE_PEEKTEXT, PTRACE_PEEKDATA](#2.2 PTRACE_PEEKTEXT, PTRACE_PEEKDATA)
    • [2.3 PTRACE_PEEKUSER](#2.3 PTRACE_PEEKUSER)
    • [2.4 PTRACE_POKETEXT, PTRACE_POKEDATA](#2.4 PTRACE_POKETEXT, PTRACE_POKEDATA)
    • [2.5 PTRACE_POKEUSER](#2.5 PTRACE_POKEUSER)
    • [2.6 PTRACE_GETREGS, PTRACE_GETFPREGS](#2.6 PTRACE_GETREGS, PTRACE_GETFPREGS)
    • [2.7 PTRACE_GETREGSET](#2.7 PTRACE_GETREGSET)
    • [2.8 PTRACE_SETREGS, PTRACE_SETFPREGS](#2.8 PTRACE_SETREGS, PTRACE_SETFPREGS)
    • [2.9 PTRACE_SETREGSET](#2.9 PTRACE_SETREGSET)
    • [2.10 PTRACE_GETSIGINFO](#2.10 PTRACE_GETSIGINFO)
    • [2.11 PTRACE_SETSIGINFO](#2.11 PTRACE_SETSIGINFO)
    • [2.12 PTRACE_SETOPTIONS](#2.12 PTRACE_SETOPTIONS)
    • [2.13 PTRACE_CONT](#2.13 PTRACE_CONT)
    • [2.14 PTRACE_SYSCALL, PTRACE_SINGLESTEP](#2.14 PTRACE_SYSCALL, PTRACE_SINGLESTEP)
    • [2.15 PTRACE_ATTACH](#2.15 PTRACE_ATTACH)
    • [2.16 PTRACE_SEIZE](#2.16 PTRACE_SEIZE)
    • [2.17 PTRACE_DETACH](#2.17 PTRACE_DETACH)
  • [三、Stopped states](#三、Stopped states)
    • [3.1 简介](#3.1 简介)
    • [3.2 ptrace-stop](#3.2 ptrace-stop)
    • [3.3 ptrace-stop内核源码](#3.3 ptrace-stop内核源码)
  • 四、Signal-delivery-stop
  • 五、Group-stop
  • [六、PTRACE_EVENT stops](#六、PTRACE_EVENT stops)
  • 七、Syscall-stops
  • [八、 execve under ptrace](#八、 execve under ptrace)

一、ptrace 简介

c 复制代码
NAME
       ptrace - process trace

SYNOPSIS
       #include <sys/ptrace.h>

       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

ptrace()系统调用提供了一种方法,通过这种方法,一个进程(the "tracer" -- 跟踪器)可以观察和控制另一个进程的执行(the "tracee" -- 被跟踪的进程),并检查和更改tracee的内存和寄存器。它主要用于实现断点、单步调试和系统调用跟踪。

ptrace()系统调用是 gdb 和 strace 实现的基础。

首先需要将tracee附加到 the tracer。附加和后续命令是按线程执行的:在多线程进程中,每个线程都可以单独附加到(可能不同的)tracer,或者不附加,因此不进行调试。因此,"tracee"总是指"(一个)线程",而不是"(可能是多线程的)进程"。

这也是gdb可以调试多线程的原理。

GDB(GNU调试器)进行多线程调试的一些命名:

c 复制代码
info threads:在GDB中运行此命令可以查看当前正在运行的线程列表。它将显示每个线程的唯一标识符(线程ID)和当前所在的位置。

thread <thread-id>:使用该命令可以切换到指定的线程。通过提供线程ID作为参数,你可以选择要调试的特定线程。

thread apply <thread-range> <command>:这个命令可以对一组线程执行指定的GDB命令。线程范围可以是线程ID的列表或范围。例如,thread apply 1-3 bt将对线程1、2和3执行bt命令(打印线程栈回溯)。

set scheduler-locking off:启用此选项可以禁用调度器锁定,允许所有线程并发运行。这在调试多线程应用程序时可能很有用,因为默认情况下,GDB会通过锁定调度器以确保只有一个线程处于活动状态。

set scheduler-locking on:通过设置此选项,可以重新启用调度器锁定,使只有一个线程处于活动状态。这在需要集中调试某个特定线程时可能很有用。

ptrace命令总是使用以下形式的调用发送到特定的tracee:

c 复制代码
ptrace(PTRACE_foo, pid, ...)

其中pid是相应Linux线程的线程ID。

进程可以通过调用fork并让生成的子进程执行PTRACE_TRACEME,然后(通常)执行一个execve来启动跟踪。或者,一个进程可以使用PTRACE_ATTACH或PTRACE_SEIZE开始跟踪另一个进程。

(1)子进程执行PTRACE_TRACEME,子进程主动进入被跟踪状态 -- gdb 调试原理

(2)一个进程 tracer 使用PTRACE_ATTACH开始跟踪另一个进程 tracee ,tracee 是被动进入被跟踪状态。

在跟踪过程中,每次传递信号时,跟踪都会停止,即使信号被忽略。(SIGKILL是一个例外,它具有通常的效果。)tracer 将在下一次调用waitpid(或一个相关的"wait"系统调用)时得到通知;该调用将返回一个状态值,该值包含指示tracee中停止原因的信息。当tracee停止时,tracer 可以使用各种ptrace请求来检查和修改tracee。跟踪器然后使跟踪继续,可选地忽略传递的信号(或者甚至传递不同的信号)。

如果PTRACE_O_TRACEEXEC选项无效,则被跟踪进程对execve的所有成功调用都将导致向其发送SIGTRAP信号,从而使父进程有机会在新程序开始执行之前获得控制权。

当tracer完成跟踪时,它可以通过PTRACE_DETACH结束跟踪,使 tracee 继续以正常的、未被跟踪的模式执行。

二、ptrace 参数request

ptrace系统调用第一个参数的 request 值决定要执行的操作:

2.1 PTRACE_TRACEME

该值仅tracee使用,指示此进程将由其父进程跟踪。如果进程的父进程不希望跟踪它,那么它可能不应该发出此请求。

PTRACE_TRACEME请求仅由tracee使用;剩下的request值仅由tracer使用。在以下请求中,pid指定要执行操作的 tracee 的线程ID。对于除PTRACE_ATTACH、PTRACE_SEIZE、PTRACE_INTERRUPT和PTRACE_KILL之外的请求,必须停止 tracee 。

以下request值仅由tracer使用

2.2 PTRACE_PEEKTEXT, PTRACE_PEEKDATA

PTRACE_PEEKTEXT和PTRACE_PEEKDATA是用于在被跟踪进程的内存中读取数据的ptrace系统调用的请求选项。

在Linux中,由于没有单独的文本和数据地址空间,因此这两个请求目前是等效的。

PTRACE_PEEKTEXT:通过使用ptrace系统调用的PTRACE_PEEKTEXT请求选项,可以读取被跟踪进程内存中给定地址处的一个字(word)并作为ptrace调用的结果返回。这个请求用于读取代码段(text segment)中的数据。

PTRACE_PEEKDATA:同样,PTRACE_PEEKDATA请求选项也用于读取被跟踪进程内存中给定地址处的一个字(word)。在Linux中,由于没有明确的数据段和代码段的区分,因此使用PTRACE_PEEKDATA请求选项也可以读取数据段(data segment)中的数据。

2.3 PTRACE_PEEKUSER

PTRACE_PEEKUSER是ptrace系统调用的一个请求选项,用于从被跟踪进程的USER区域中读取一个字(word)的数据。

在被跟踪进程的USER区域中存储着进程的寄存器和其他关于进程的信息。具体的结构和定义可以在<sys/user.h>头文件中找到。通过使用PTRACE_PEEKUSER选项,可以读取USER区域中指定偏移量(offset)处的一个字,并将其作为ptrace调用的结果返回。

c 复制代码
#ifdef __x86_64__
/* Index into an array of 8 byte longs returned from ptrace for
   location of the users' stored general purpose registers.  */

# define R15	0
# define R14	1
# define R13	2
# define R12	3
# define RBP	4
# define RBX	5
# define R11	6
# define R10	7
# define R9	8
# define R8	9
# define RAX	10
# define RCX	11
# define RDX	12
# define RSI	13
# define RDI	14
# define ORIG_RAX 15
# define RIP	16
# define CS	17
# define EFLAGS	18
# define RSP	19
# define SS	20
# define FS_BASE 21
# define GS_BASE 22
# define DS	23
# define ES	24
# define FS	25
# define GS	26

2.4 PTRACE_POKETEXT, PTRACE_POKEDATA

PTRACE_POKETEXT和PTRACE_POKEDATA是用于向被跟踪进程的内存写入数据的ptrace系统调用的请求选项。

在Linux中,由于没有单独的文本和数据地址空间,因此这两个请求目前是等效的。

PTRACE_POKETEXT:通过使用ptrace系统调用的PTRACE_POKETEXT请求选项,可以将指定的字(word)数据写入到被跟踪进程内存中的给定地址。这个请求用于写入代码段(text segment)中的数据。

PTRACE_POKEDATA:同样,PTRACE_POKEDATA请求选项也用于将指定的字(word)数据写入到被跟踪进程内存中的给定地址。在Linux中,由于没有明确的数据段和代码段的区分,因此使用PTRACE_POKEDATA请求选项也可以写入数据段(data segment)中的数据。

2.5 PTRACE_POKEUSER

PTRACE_POKEUSER是ptrace系统调用的一个请求选项,用于将一个字(word)的数据复制到被跟踪进程的USER区域的指定偏移量(offset)处。

在被跟踪进程的USER区域中存储着进程的寄存器和其他关于进程的信息。通过使用PTRACE_POKEUSER选项,可以将指定的字数据复制到USER区域的偏移量处。通常,偏移量需要按字对齐。

但需要注意的是,为了维护内核的完整性,一些对USER区域的修改是不允许的。

2.6 PTRACE_GETREGS, PTRACE_GETFPREGS

PTRACE_GETREGS和PTRACE_GETFPREGS是ptrace系统调用的两个请求选项,用于将被跟踪进程的通用寄存器(general-purpose registers)或浮点寄存器(floating-point registers)的值复制到跟踪进程中的指定地址。

PTRACE_GETREGS:通过使用ptrace系统调用的PTRACE_GETREGS请求选项,可以将被跟踪进程的通用寄存器的值复制到跟踪进程中的指定地址。可以使用<sys/user.h>头文件中定义的结构体来解析和访问复制的寄存器数据。

PTRACE_GETFPREGS:使用ptrace系统调用的PTRACE_GETFPREGS请求选项,可以将被跟踪进程的浮点寄存器的值复制到跟踪进程中的指定地址。浮点寄存器通常用于处理浮点数和浮点运算。同样,可以使用<sys/user.h>头文件中定义的结构体来解析和访问复制的寄存器数据。

2.7 PTRACE_GETREGSET

TRACE_GETREGSET是自Linux 2.6.34版本引入的ptrace系统调用的一个请求选项,用于读取被跟踪进程的寄存器信息。通过指定addr参数,在与体系结构相关的方式下,可以指定要读取的寄存器类型。

通常情况下,使用NT_PRSTATUS(数值为1)作为addr的值可以读取通用寄存器的值。如果CPU具有浮点寄存器和/或向量寄存器等其他类型的寄存器,可以通过将addr设置为相应的NT_foo常量来检索它们。

data参数指向一个struct iovec结构体,描述了目标缓冲区的位置和长度。在返回时,内核会修改iov.len以指示实际返回的字节数。

2.8 PTRACE_SETREGS, PTRACE_SETFPREGS

PTRACE_SETREGS和PTRACE_SETFPREGS是ptrace系统调用的两个请求选项,用于从跟踪进程中的指定地址修改被跟踪进程的通用寄存器(general-purpose registers)或浮点寄存器(floating-point registers)的值。

PTRACE_SETREGS:通过使用ptrace系统调用的PTRACE_SETREGS请求选项,可以从跟踪进程中的指定地址修改被跟踪进程的通用寄存器的值。被修改的寄存器数据必须符合特定的格式,具体取决于体系结构。对于某些操作系统,可能会限制对某些特定的通用寄存器进行修改。

PTRACE_SETFPREGS:使用ptrace系统调用的PTRACE_SETFPREGS请求选项,可以从跟踪进程中的指定地址修改被跟踪进程的浮点寄存器的值。被修改的浮点寄存器数据必须符合特定的格式,具体取决于体系结构。

2.9 PTRACE_SETREGSET

PTRACE_SETREGSET是自Linux 2.6.34版本引入的ptrace系统调用的请求选项,用于修改被跟踪进程的寄存器信息。addr和data的含义与PTRACE_GETREGSET类似。

与PTRACE_GETREGSET类似,PTRACE_SETREGSET请求选项允许以体系结构相关的方式指定要修改的寄存器类型。addr参数用于指定要修改的寄存器类型,而data参数指向一个struct iovec结构体,描述了包含要写入被跟踪进程寄存器信息的源缓冲区的位置和长度。

2.10 PTRACE_GETSIGINFO

PTRACE_GETSIGINFO是自Linux 2.3.99-pre6版本引入的ptrace系统调用的请求选项,用于从跟踪进程中获取导致进程停止的信号的相关信息。它将一个siginfo_t结构体(参见sigaction(2))从被跟踪进程复制到跟踪进程的指定地址中(data参数)。

2.11 PTRACE_SETSIGINFO

PTRACE_SETSIGINFO是自Linux 2.3.99-pre6版本引入的ptrace系统调用的请求选项,用于设置信号信息。它将一个siginfo_t结构体从跟踪进程的指定地址(data参数)复制到被跟踪进程中。这将仅影响通常会传递给被跟踪进程并被跟踪进程捕获的信号。

2.12 PTRACE_SETOPTIONS

PTRACE_SETOPTIONS是ptrace系统调用的一个请求选项,用于设置跟踪选项(trace options)。它允许在调用ptrace时指定一组选项,以控制跟踪进程的行为。

具体参考 man 手册。

2.13 PTRACE_CONT

PTRACE_CONT是ptrace系统调用的一个请求选项,用于重新启动被停止的被跟踪进程。当ptrace调用使用PTRACE_CONT选项时,被跟踪进程将继续执行。

以下是PTRACE_CONT请求选项的使用方式:

c 复制代码
ptrace(PTRACE_CONT, pid, NULL, data);

pid是被跟踪进程的进程ID。

data参数如果非零,表示要发送给被跟踪进程的信号编号;如果为零,表示不发送任何信号。

使用PTRACE_CONT选项,跟踪进程可以控制是否向被跟踪进程发送信号。

2.14 PTRACE_SYSCALL, PTRACE_SINGLESTEP

PTRACE_SYSCALL和PTRACE_SINGLESTEP是ptrace系统调用的两个请求选项,用于重新启动被停止的被跟踪进程,并在特定条件下再次停止。

PTRACE_SYSCALL:重新启动被跟踪进程,并在下一次进入或退出系统调用时停止。当使用PTRACE_SYSCALL选项时,被跟踪进程将在下一次系统调用的入口或出口处停止执行,以供跟踪进程进行检查或操作。

PTRACE_SINGLESTEP:重新启动被跟踪进程,并在执行一条指令后停止。当使用PTRACE_SINGLESTEP选项时,被跟踪进程将在执行完一条指令后立即停止,以供跟踪进程进行单步调试或其他操作。

这两个选项都会使被跟踪进程看起来好像是接收到了一个SIGTRAP信号而停止执行。跟踪进程可以在被跟踪进程停止时进行进一步的检查或操作。

以下是这两个选项的使用方式:

c 复制代码
ptrace(PTRACE_SYSCALL, pid, NULL, data);
ptrace(PTRACE_SINGLESTEP, pid, NULL, data);

pid是被跟踪进程的进程ID。

data参数如果非零,表示要发送给被跟踪进程的信号编号;如果为零,表示不发送任何信号。

在停止时,被跟踪进程会看起来好像是接收到了一个SIGTRAP信号。

2.15 PTRACE_ATTACH

PTRACE_ATTACH是ptrace系统调用的一个请求选项,用于将指定的进程附加到调用进程中,使其成为调用进程的被跟踪进程。被附加的进程将收到一个SIGSTOP信号,但不一定会在此调用完成后立即停止执行,可以使用waitpid等待被跟踪进程停止。

以下是PTRACE_ATTACH选项的使用方式:

c 复制代码
ptrace(PTRACE_ATTACH, pid, NULL, NULL);

pid是要附加的进程的进程ID。

当使用PTRACE_ATTACH命令将跟踪器附加到目标进程时,会向目标进程的线程发送SIGSTOP信号。如果跟踪器希望这个SIGSTOP信号不产生任何效果,它需要将其抑制。需要注意的是,如果在附加过程中同时向该线程发送其他信号,则跟踪器可能会先看到跟踪对象进入 signal-delivery-stop 状态,并接收到其他信号!通常的做法是在看到SIGSTOP信号之前重新注入这些信号,然后抑制SIGSTOP信号的注入。这里的设计缺陷在于ptrace附加和同时传递的SIGSTOP信号可能存在竞争条件,导致同时传递的SIGSTOP信号可能会丢失。

当使用PTRACE_ATTACH命令附加到目标进程时,会发送SIGSTOP信号,而跟踪器通常会将其抑制。这可能会导致被跟踪进程中当前正在执行的系统调用出现意外的EINTR错误返回,就像在"Signal injection and suppression"部分中所描述的那样。

在一般情况下,当一个进程处于系统调用过程中,如果收到一个信号,系统调用可能会被中断,并返回一个EINTR错误。当跟踪器在附加过程中抑制了SIGSTOP信号时,如果被跟踪进程正好处于系统调用中,那么收到的SIGSTOP信号可能会导致系统调用被中断,从而返回一个EINTR错误。

这个情况可能会在跟踪过程中出现,并且需要被跟踪器适当处理。一种常见的方法是在收到SIGSTOP信号之前,跟踪器可以选择暂停被跟踪进程的执行,以确保不会在系统调用期间收到信号。然后,跟踪器可以继续附加和执行其他操作。

总之,由于SIGSTOP信号的抑制可能导致系统调用中断并返回EINTR错误,跟踪器在附加过程中应该注意处理这种情况,以确保跟踪的准确性。

2.16 PTRACE_SEIZE

PTRACE_SEIZE是自Linux 3.4版本引入的ptrace系统调用的一个请求选项。它用于将指定的进程附加到调用进程中,使其成为调用进程的被跟踪进程,但与PTRACE_ATTACH不同,PTRACE_SEIZE不会停止被跟踪进程的执行

以下是PTRACE_SEIZE选项的使用方式:

c 复制代码
ptrace(PTRACE_SEIZE, pid, NULL, data);

pid是要附加的进程的进程ID。

data参数是一个位掩码,用于激活要立即启用的ptrace选项。

使用PTRACE_SEIZE选项时,被跟踪进程不会被停止,而是直接附加到调用进程中。只有经过PTRACE_SEIZE附加的进程才能接受PTRACE_INTERRUPT和PTRACE_LISTEN命令。

addr参数必须为零。

PTRACE_INTERRUPT和PTRACE_LISTEN命令是自Linux 3.4版本引入的ptrace系统调用的请求选项。具体请参考man手册。

自从Linux 3.4版本以后,可以使用PTRACE_SEIZE命令来代替PTRACE_ATTACH。PTRACE_SEIZE在附加到进程时不会停止其执行。如果需要在附加后或任何其他时间停止进程而不发送任何信号,则可以使用PTRACE_INTERRUPT命令。

通过使用PTRACE_SEIZE,跟踪器可以在不立即停止进程执行的情况下附加到目标进程。这使得跟踪器可以在不中断进程正常流程的情况下对其进行操作。如果跟踪器希望在某个时刻停止进程,可以使用PTRACE_INTERRUPT命令,而无需向进程发送任何信号。

通过适当使用PTRACE_SEIZE和PTRACE_INTERRUPT,跟踪器可以更有控制地管理被附加进程的执行,并在需要时有选择地停止它,而无需依赖信号的发送。

2.17 PTRACE_DETACH

PTRACE_DETACH是ptrace系统调用的一个请求选项,用于从调用进程中分离(detach)被跟踪进程。它会在分离之前以类似于PTRACE_CONT的方式重新启动被跟踪进程。在Linux下,无论使用哪种方法进行跟踪,都可以使用PTRACE_DETACH选项将被跟踪进程分离。

以下是PTRACE_DETACH选项的使用方式:

c 复制代码
ptrace(PTRACE_DETACH, pid, NULL, NULL);

pid是要分离的进程的进程ID。

使用PTRACE_DETACH选项时,被跟踪进程会被重新启动,然后与调用进程分离。该选项会将被跟踪进程从跟踪状态中解放出来,使其恢复正常执行。

PTRACE_DETACH是一个重启操作,因此需要 tracee 处于ptrace-stop状态才能执行。如果 tracee 处于 signal-delivery-stop 状态,可以注入信号。否则,sig参数可能会被忽略。

如果跟 tracee 在 tracer 想要分离它时正在运行,通常的解决方案是发送SIGSTOP信号(使用tgkill确保信号发送到正确的线程),等待 tracee 在信号传递停止状态下停止接收SIGSTOP信号,然后分离它(抑制SIGSTOP注入)。这里存在一个设计缺陷,即与并发的SIGSTOP信号可能存在竞争条件。另一个复杂性在 tracee 可能进入其他ptrace-stop状态,并需要重新启动和等待,直到看到SIGSTOP信号。还有一个问题是要确保 tracee 没有处于已经ptrace-stop的状态,因为在此期间不会发生任何信号传递,即使是SIGSTOP信号也不会传递。

如果 tracer 终止,所有 tracees 都会自动分离并重新启动,除非它们处于组停止状态。目前对于从组停止状态重新启动的处理存在问题,但"按计划"的行为是保持进程停止,并等待SIGCONT信号。如果 tracee 从 signal-delivery-stop 状态重新启动,挂起的信号将被注入。

三、Stopped states

3.1 简介

被跟踪的进程可以处于两种状态:运行状态或停止状态。就ptrace而言,即使 tracee 在系统调用(比如read、pause等)中被阻塞,仍然被认为是运行状态,即使进程被长时间阻塞。在使用PTRACE_LISTEN之后,跟踪对象的状态有些模糊:它既不处于任何ptrace-stop状态(ptrace命令无法对其起作用,并且会向其发送waitpid通知),但也可能被视为"停止"状态,因为它不执行指令(不在调度中)。如果在PTRACE_LISTEN之前处于组停止状态,它在接收到SIGCONT信号之前将不会对信号做出响应。

当tracee停止时,有很多种状态,在ptrace讨论中,它们经常被混为一谈。因此,使用精确的术语很重要。

当运行的tracee进入 ptrace-stop 时,它会通知其 tracer 使用waitpid系统调用(或其他"wait"系统调用之一)。这里假定tracer调用 "wait"系统调用为:

c 复制代码
pid = waitpid(pid_or_minus_1, &status, __WALL);

跟踪器以返回值大于0且WIFSTOPPED(status)为真的形式报告处于ptrace-stop状态的跟踪对象。

__WALL标志不包括WSTOPPED和WEXITED标志,但隐含了它们的功能。

在调用waitpid时设置WCONTINUED标志并不推荐使用:"continued"状态是针对每个进程的,使用它可能会让跟踪对象的真实父进程产生困惑。

在调用waitpid时使用WNOHANG标志可能会导致返回0("尚无可用的等待结果"),即使跟踪器知道应该有通知。例如:

c 复制代码
 errno = 0;
 ptrace(PTRACE_CONT, pid, 0L, 0L);
 if (errno == ESRCH) {
     /* tracee is dead */
     r = waitpid(tracee, &status, __WALL | WNOHANG);
     /* r can still be 0 here! */
 }

存在以下几种类型的ptrace-stop:signal-delivery-stops、 group-stops、PTRACE_EVENT stops,、syscall-stops。它们都会通过waitpid报告为WIFSTOPPED(status)为真的返回值。可以通过检查status>>8的值来区分它们,如果该值有歧义,可以查询PTRACE_GETSIGINFO。(注意:不能使用WSTOPSIG(status)宏对其进行检查,因为它返回的是值(status>>8) & 0xff。)

3.2 ptrace-stop

大多数ptrace命令(除了PTRACE_ATTACH、PTRACE_SEIZE、PTRACE_TRACEME、PTRACE_INTERRUPT和PTRACE_KILL)要求被跟踪的进程处于 ptrace-stop 状态,否则它们将以ESRCH错误失败。

以下列举一些常用会进入ptrace-stop 状态的情况:

(1)被跟踪的进程触发int3 发送SIGTRAP信号-- 软件断点

(2)调试器发起PTRACE_SINGLESTEP请求

(3)调试器发起PTRACE_SYSCALL请求

当被跟踪的进程处于 ptrace-stop 状态时,跟踪器可以使用信息性命令向被跟踪的进程读取和写入数据。这些命令会使被跟踪的进程保持在ptrace停止状态,具体包括:

c 复制代码
ptrace(PTRACE_PEEKTEXT/PEEKDATA/PEEKUSER, pid, addr, 0);
ptrace(PTRACE_POKETEXT/POKEDATA/POKEUSER, pid, addr, long_val);
ptrace(PTRACE_GETREGS/GETFPREGS, pid, 0, &struct);
ptrace(PTRACE_SETREGS/SETFPREGS, pid, 0, &struct);
ptrace(PTRACE_GETREGSET, pid, NT_foo, &iov);
ptrace(PTRACE_SETREGSET, pid, NT_foo, &iov);
ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo);
ptrace(PTRACE_SETSIGINFO, pid, 0, &siginfo);
ptrace(PTRACE_GETEVENTMSG, pid, 0, &long_var);
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_flags);

3.3 ptrace-stop内核源码

内核源码中有一个ptrace_stop函数来处理 tracee 处于ptrace-stop的情况:

c 复制代码
// linux-3.10/kernel/signal.c

/*
 * This must be called with current->sighand->siglock held.
 *
 * This should be the path for all ptrace stops.
 * We always set current->last_siginfo while stopped here.
 * That makes it a way to test a stopped process for
 * being ptrace-stopped vs being job-control-stopped.
 *
 * If we actually decide not to stop at all because the tracer
 * is gone, we keep current->exit_code unless clear_code.
 */
static void ptrace_stop(int exit_code, int why, int clear_code, siginfo_t *info)
	__releases(&current->sighand->siglock)
	__acquires(&current->sighand->siglock)
{

	......
	/*
	 * We're committing to trapping.  TRACED should be visible before
	 * TRAPPING is cleared; otherwise, the tracer might fail do_wait().
	 * Also, transition to TRACED and updates to ->jobctl should be
	 * atomic with respect to siglock and should be done after the arch
	 * hook as siglock is released and regrabbed across it.
	 */
	set_current_state(TASK_TRACED);

	current->last_siginfo = info;
	current->exit_code = exit_code;

	/*
	 * If @why is CLD_STOPPED, we're trapping to participate in a group
	 * stop.  Do the bookkeeping.  Note that if SIGCONT was delievered
	 * across siglock relocks since INTERRUPT was scheduled, PENDING
	 * could be clear now.  We act as if SIGCONT is received after
	 * TASK_TRACED is entered - ignore it.
	 */
	if (why == CLD_STOPPED && (current->jobctl & JOBCTL_STOP_PENDING))
		gstop_done = task_participate_group_stop(current);

	/* any trap clears pending STOP trap, STOP trap clears NOTIFY */
	task_clear_jobctl_pending(current, JOBCTL_TRAP_STOP);
	if (info && info->si_code >> 8 == PTRACE_EVENT_STOP)
		task_clear_jobctl_pending(current, JOBCTL_TRAP_NOTIFY);

	/* entering a trap, clear TRAPPING */
	task_clear_jobctl_trapping(current);

	spin_unlock_irq(&current->sighand->siglock);
	read_lock(&tasklist_lock);
	if (may_ptrace_stop()) {
		/*
		 * Notify parents of the stop.
		 *
		 * While ptraced, there are two parents - the ptracer and
		 * the real_parent of the group_leader.  The ptracer should
		 * know about every stop while the real parent is only
		 * interested in the completion of group stop.  The states
		 * for the two don't interact with each other.  Notify
		 * separately unless they're gonna be duplicates.
		 */
		do_notify_parent_cldstop(current, true, why);
		if (gstop_done && ptrace_reparented(current))
			do_notify_parent_cldstop(current, false, why);

		/*
		 * Don't want to allow preemption here, because
		 * sys_ptrace() needs this task to be inactive.
		 *
		 * XXX: implement read_unlock_no_resched().
		 */
		preempt_disable();
		read_unlock(&tasklist_lock);
		preempt_enable_no_resched();
		freezable_schedule();
	} 
	......

}

ptrace_stop这个函数通常用于通知让调式器运行,子进程处于被跟踪状态和停止状态。

通知父进程进入调试状态:

c 复制代码
static void do_notify_parent_cldstop(struct task_struct *tsk,
				     bool for_ptracer, int why)
{
	......
	__group_send_sig_info(SIGCHLD, &info, parent);
	/*
	 * Even if SIGCHLD is not generated, we must wake up wait4 calls.
	 */
	__wake_up_parent(tsk, parent);
	......
}

四、Signal-delivery-stop

当一个(可能是多线程的)进程接收到除SIGKILL之外的任何信号时,内核会选择一个任意的线程来处理该信号。(如果使用tgkill生成信号,则调用者可以显式选择目标线程。)如果所选线程正在被跟踪,则进入 signal-delivery-stop 状态。此时,信号尚未传递给进程,并且跟踪器可以抑制该信号。如果跟踪器不抑制信号,则在下一个ptrace重启请求中将信号传递给跟踪对象。这里将信号传递的这第二步称为 signal injection 。注意,如果信号被阻塞,直到信号解除阻塞才会发生信号传递停止,通常的例外是SIGSTOP无法被阻塞。

跟踪器通过waitpid观察到 signal-delivery-stop ,以WIFSTOPPED(status)为真的形式返回,并返回由WSTOPSIG(status)返回的信号。如果信号是SIGTRAP,则可能是不同类型的ptrace-stop;有关详细信息,请参见下面的"Syscall-stops"和"execve"部分。如果WSTOPSIG(status)返回一个停止信号,则可能是 group-stop ;请参见下面的说明。

五、Group-stop

当一个(可能是多线程的)进程接收到停止信号时,所有线程都会停止运行。如果一些线程被跟踪,它们会进入一个组停止状态。需要注意的是,停止信号首先会导致信号传递停止(只在一个被跟踪的进程上),只有在由跟踪器注入该信号(或者发送给一个未被跟踪的线程后),组停止才会在多线程进程中的所有被跟踪进程中启动。通常情况下,每个被跟踪进程都会将其组停止状态单独报告给相应的跟踪器。

跟踪器通过waitpid返回WIFSTOPPED(status)为真来观察组停止,通过WSTOPSIG(status)获取停止信号。对于某些其他类别的ptrace停止,也会返回相同的结果,因此推荐的做法是执行以下调用:

c 复制代码
ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo)

如果信号不是SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU,可以避免调用该函数;这四个信号是停止信号。如果跟踪器看到其他信号,它就不能是组停止。否则,跟踪器需要调用PTRACE_GETSIGINFO。如果PTRACE_GETSIGINFO返回EINVAL,则明确是组停止。(可能存在其他失败代码,例如如果SIGKILL杀死了被跟踪进程,则返回ESRCH("没有这样的进程"))。

如果使用PTRACE_SEIZE连接跟踪进程,则通过PTRACE_EVENT_STOP指示组停止:status>>16 == PTRACE_EVENT_STOP。这样可以在无需额外的PTRACE_GETSIGINFO调用的情况下检测组停止。

从Linux 2.6.38开始,当跟踪器看到被跟踪进程的ptrace停止,并且在重新启动或终止它之前,被跟踪进程不会运行,并且不会向跟踪器发送通知(除非通过SIGKILL终止)。即使跟踪器进入另一个waitpid(2)调用,也是如此。

上述段落中描述的内核行为导致了对停止信号的透明处理问题。如果跟踪器在组停止后重新启动被跟踪进程,停止信号实际上会被忽略,被跟踪进程不会继续停止,而是继续运行。如果跟踪器在进入下一个waitpid(2)之前不重新启动被跟踪进程,将不会向跟踪器报告未来的SIGCONT信号;这将导致SIGCONT信号对被跟踪进程没有影响。

自Linux 3.4以来,有一种方法可以解决这个问题:可以使用PTRACE_LISTEN命令来重新启动被跟踪进程,它不会执行,而是等待一个新的事件,可以通过waitpid(2)报告该事件(例如,当它被SIGCONT重新启动时)。

六、PTRACE_EVENT stops

当跟踪器设置了PTRACE_O_TRACE_*选项时,被跟踪的进程将进入被称为PTRACE_EVENT停止的状态。

通过waitpid系统调用,跟踪器可以观察到PTRACE_EVENT停止。waitpid的返回值将指示进程被停止,WIFSTOPPED(status)返回true,WSTOPSIG(status)返回SIGTRAP。在状态字的高字节中,还会设置一个额外的位,其中status>>8的值为:

c 复制代码
(SIGTRAP | PTRACE_EVENT_foo << 8)

以下是可用的PTRACE_EVENT停止类型:

PTRACE_EVENT_VFORK:在从vfork或clone系统调用中返回之前,跟踪器会停止该进程。停止发生时,使用了CLONE_VFORK标志。当该进程在此停止后继续执行时,它将等待子进程退出或执行新程序,然后再恢复执行。这个行为模拟了通常的vfork(2)语义。

PTRACE_EVENT_FORK:在从fork或clone系统调用中返回之前,跟踪器会停止该进程。退出信号被设置为SIGCHLD。

PTRACE_EVENT_CLONE:在从clone系统调用中返回之前,跟踪器会停止该进程。

PTRACE_EVENT_VFORK_DONE:在从vfork或clone系统调用中返回之前,跟踪器会停止该进程,使用了CLONE_VFORK标志,并且在子进程通过退出或执行新程序之后。

七、Syscall-stops

如果被跟踪的进程通过PTRACE_SYSCALL重新启动,那么在进入任何系统调用之前,该进程将进入系统调用进入停止状态(syscall-enter-stop)。如果跟踪器使用PTRACE_SYSCALL重新启动被跟踪进程,当系统调用完成或被信号中断时,被跟踪的进程将进入系统调用退出停止状态(syscall-exit-stop)。也就是说,在系统调用进入停止状态(syscall-enter-stop)和系统调用退出停止状态(syscall-exit-stop)之间不会发生信号传递停止(signal-delivery-stop);信号传递停止会在系统调用退出停止状态之后发生。

其他可能的情况是,被跟踪的进程可能会在PTRACE_EVENT停止中停止、退出(如果进入了_exit(2)或exit_group(2))、被SIGKILL终止,或者默默死亡(如果它是线程组的领导者,execve(2)发生在另一个线程中,并且该线程不受相同跟踪器的跟踪;此情况将在后面讨论)。

跟踪器通过waitpid观察到系统调用进入停止状态(syscall-enter-stop)和系统调用退出停止状态(syscall-exit-stop),此时WIFSTOPPED(status)为true,WSTOPSIG(status)返回SIGTRAP。如果跟踪器设置了PTRACE_O_TRACESYSGOOD选项,那么WSTOPSIG(status)将返回(SIGTRAP | 0x80)。

可以通过查询PTRACE_GETSIGINFO来区分系统调用停止(syscall-stops)和带有SIGTRAP的信号传递停止(signal-delivery-stop)。具体情况如下:

c 复制代码
si_code <= 0:SIGTRAP是由用户空间操作(例如tgkill(2),kill(2),sigqueue(3)等)触发的,或者由POSIX定时器到期、POSIX消息队列状态变化或异步I/O请求完成引起的。
si_code == SI_KERNEL (0x80):SIGTRAP是由内核发送的。
si_code == SIGTRAP或si_code == (SIGTRAP | 0x80):这是一个系统调用停止(syscall-stop)。

然而,系统调用停止(syscall-stops)非常频繁(每个系统调用两次),对于每个系统调用停止都执行PTRACE_GETSIGINFO可能会有一些性能开销。

一些体系结构允许通过检查寄存器来区分这些情况。例如,在x86上,syscall-enter-stop时rax == -ENOSYS。由于SIGTRAP(像任何其他信号一样)总是在syscall-exit-stop之后发生,并且此时rax几乎不会包含-ENOSYS,SIGTRAP看起来像是"不是syscall-enter-stop的syscall-stop";换句话说,它看起来像是"错误的syscall-exit-stop",可以通过这种方式检测到。但这种检测是脆弱的,最好避免使用。

使用PTRACE_O_TRACESYSGOOD选项是区分系统调用停止和其他类型ptrace停止的推荐方法,因为它可靠且不会带来性能损失。

对于跟踪器来说,系统调用进入停止状态(syscall-enter-stop)和系统调用退出停止状态(syscall-exit-stop)在外观上是无法区分的。跟踪器需要跟踪ptrace停止的顺序,以避免将系统调用进入停止状态错误地解释为系统调用退出停止状态或反之。规则是syscall-enter-stop后始终紧随syscall-exit-stop、PTRACE_EVENT停止或被跟踪进程的终止;在其间不会发生其他类型的ptrace停止。

如果在syscall-enter-stop之后,跟踪器使用除PTRACE_SYSCALL之外的重新启动命令,则不会生成syscall-exit-stop。

在syscall-stops上使用PTRACE_GETSIGINFO将返回si_signo为SIGTRAP,si_code设置为SIGTRAP或(SIGTRAP | 0x80)。

八、 execve under ptrace

请参考man手册

相关推荐
qq_433618441 分钟前
shell 编程(三)
linux·运维·服务器
Tlzns22 分钟前
Linux网络——UDP的运用
linux·网络·udp
码农土豆28 分钟前
PaddlePaddle飞桨Linux系统Docker版安装
linux·docker·paddlepaddle
Hacker_xingchen33 分钟前
天融信Linux系统安全问题
linux·运维·系统安全
张暮笛44 分钟前
抓取手机HCI日志
linux
大猫和小黄1 小时前
Windows、CentOS环境下搭建自己的版本管理资料库:GitBlit
linux·服务器·windows·git
Joyner20181 小时前
【Linux】ubuntu通过远程命令行启动桌面应用
linux·服务器·ubuntu
ghostwritten1 小时前
Linux Swap: 深入解析 mkswap, mkfs.swap, 和 swapon
linux·运维·服务器
我是唐青枫1 小时前
Linux xargs 命令使用教程
linux·运维·服务器
Schwertlilien1 小时前
图像处理-Ch5-图像复原与重建
c语言·开发语言·机器学习