ptrace系统调用的实现

部署运行你感兴趣的模型镜像

最近遇到这样一个问题,机器跑着跑着画面冻结了,打开top看到Xorg的cpu占用率100%。想用gdb挂上去看一下,结果gdb一直卡着挂不上去。后来又换用perf分析,结果发现进程99%的时间花在了一个ioctl调用。这个ioctl操作的是nvidia显卡,进程实际上是卡在了nvidia的驱动中。

我对gdb挂不上去这件事感到很好奇,之前除了因为进程已经被另一个gdb调试而导致gdb挂不上去之外,还没有遇到过这种情况,所以看了内核源码分析了一下。

先说结论,为什么此时gdb挂不上去呢?简单的来说就是:调试器(tracer)是基于ptrace实现的,使用ptrace连接(attach)被调试进程(tracee)时,会向tracee发送一个SIGSTOP信号,让tracee停下来,后续的调试工作要等tracee停下来才能继续进行。而tracee能够停下来又要依赖对信号的响应,但是进程只有在从内核空间返回用户空间时,才会检查是否有待处理的信号,并进行响应。而如果进程一直卡在内核空间的话,就无法返回用户空间,所以就无法响应信号,导致进程无法停止。这样的话,tracer就只能一直等着tracee,无法进行调试。

上面是概要的结论,下面对ptrace系统调用的基本流程分析一下,由于ptrace的实现细节非常多,所以此处只是其大致框架。我使用的内核版本是4.16,系统架构为64位x86。为了与文档和代码中的术语保持一致,不再使用“调试”这个词,而是使用“跟踪”(trace)。(“跟踪”实际上是手段,“调试”是目的)

ptrace操作的环境

除了PTRACE_TRACEMEPTRACE_ATTACHPTRACE_SEIZE这些用于本身就用于建立ptrace操作环境的请求之外,其他请求在执行之前都会检查ptrace环境是否已经建立。

SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
		unsigned long, data)
{
......
	ret = ptrace_check_attach(child, request == PTRACE_KILL ||
				  request == PTRACE_INTERRUPT);
	if (ret < 0)
		goto out_put_task_struct;
......
}

ptrace_check_attach函数就是用来检查ptrace环境的。ptrace环境有两部分:一部分是要求tracer和tracee有跟踪关系,另一部分是要求tracee处于被跟踪的停止状态。后一部分在请求为PTRACE_KILL或者PTRACE_INTERRUPT时不需要保证。

static int ptrace_check_attach(struct task_struct *child, bool ignore_state)
{
	int ret = -ESRCH;

	/*
	 * We take the read lock around doing both checks to close a
	 * possible race where someone else was tracing our child and
	 * detached between these two checks.  After this locked check,
	 * we are sure that this is our traced child and that can only
	 * be changed by us so it's not changing right after this.
	 */
	read_lock(&tasklist_lock);
	if (child->ptrace && child->parent == current) {
		WARN_ON(child->state == __TASK_TRACED);
		/*
		 * child->sighand can't be NULL, release_task()
		 * does ptrace_unlink() before __exit_signal().
		 */
		if (ignore_state || ptrace_freeze_traced(child))
			ret = 0;
	}
	read_unlock(&tasklist_lock);

	if (!ret && !ignore_state) {
		if (!wait_task_inactive(child, __TASK_TRACED)) {
			/*
			 * This can only happen if may_ptrace_stop() fails and
			 * ptrace_stop() changes ->state back to TASK_RUNNING,
			 * so we should not worry about leaking __TASK_TRACED.
			 */
			WARN_ON(child->state == __TASK_TRACED);
			ret = -ESRCH;
		}
	}

	return ret;
}

上面的child->ptrace && child->parent == current用来保证tracer和tracee的跟踪关系,而ptrace_freeze_traced函数用来测试tracee是否处于期望的状态,即tracee的状态有__TASK_TRACED标志位,且当前没有未处理的SIGKILL信号。

static bool ptrace_freeze_traced(struct task_struct *task)
{
	bool ret = false;

	/* Lockless, nobody but us can set this flag */
	if (task->jobctl & JOBCTL_LISTENING)
		return ret;

	spin_lock_irq(&task->sighand->siglock);
	if (task_is_traced(task) && !__fatal_signal_pending(task)) {
		task->state = __TASK_TRACED;
		ret = true;
	}
	spin_unlock_irq(&task->sighand->siglock);

	return ret;
}

下面来分析如何建立ptrace环境。

tracer和tracee的跟踪状态

进程tracer首先要成为进程tracee的跟踪进程,即要满足:tracee->ptrace && tracee->parent == tracer。如何做到呢?有两种方式:一种情况是tracer是tracee的父进程,进程tracee主动调用ptrace(PTRACE_TRACEME, 0, 0, 0),另外一种是进程tracer调用ptrace(PTRACE_ATTACH, tracee->pid, 0, 0)。我们来看一下这两种方式的流程。

ptrace(PTRACE_TRACEME, 0, 0, 0)

ptrace系统调用中,当请求为PTRACE_TRACEME时,会直接调用ptrace_traceme

SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
		unsigned long, data)
{
	......
	if (request == PTRACE_TRACEME) {
		ret = ptrace_traceme();
		......
	}
    ......
}

ptrace_traceme函数中,首先要检查现在没有其他进程跟踪当前进程,然后进行权限检查。都没有问题的话,将当前进程ptrace字段置为PT_PTRACED,然后调用ptrace_link,而ptrace_link最终调用了__ptrace_link,最终将父进程设为了跟踪进程。

static int ptrace_traceme(void)
{
	int ret = -EPERM;

	write_lock_irq(&tasklist_lock);
	/* Are we already being traced? */
	if (!current->ptrace) {
		ret = security_ptrace_traceme(current->parent);
		/*
		 * Check PF_EXITING to ensure ->real_parent has not passed
		 * exit_ptrace(). Otherwise we don't report the error but
		 * pretend ->real_parent untraces us right after return.
		 */
		if (!ret && !(current->real_parent->flags & PF_EXITING)) {
			current->ptrace = PT_PTRACED;
			ptrace_link(current, current->real_parent);
		}
	}
	write_unlock_irq(&tasklist_lock);

	return ret;
}

__ptrace_link首先将tracee放到tracer的ptraced列表中,然后将tracee的parent字段置为tracer。(请注意区分parent字段和real_parent字段)。

void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
		   const struct cred *ptracer_cred)
{
	BUG_ON(!list_empty(&child->ptrace_entry));
	list_add(&child->ptrace_entry, &new_parent->ptraced);
	child->parent = new_parent;
	child->ptracer_cred = get_cred(ptracer_cred);
}

ptrace(PTRACE_ATTACH, tracee->pid, 0, 0)

tracer跟踪tracee的另一种方式是使用PTRACE_ATTACH(或PTRACE_SEIZE)请求。与处理PTRACE_TRACEME请求的ptrace_traceme函数类似,处理PTRACE_ATTACH请求时会调用ptrace_attach函数。ptrace_attach代码行数较ptrace_traceme长不少,因为要做大量的合法性检查:tracer和tracee可能是同一个进程,tracee可能是个内核线程,也可能是其他用户的进程,等等有各种意外情况。但如果没问题的话,最终也是调用了ptrace_link将tracer设置为tracee的跟踪进程,在此不再赘述。有一个不一样的地方是,处理PTRACE_ATTACH请求时会向tracee发送一个SIGSTOP信号。

至此,tracer就成为了tracee的跟踪进程,但此时tracer还不能立即对tracee使用ptrace请求,还有另一个前提条件:tracee已经停止运行,即state字段中__TASK_TRACED被置位。

使tracee停止运行

内核何时对tracee的state字段的__TASK_TRACED标志位进行置位呢?是在tracee处理除SIGKILL信号以外的任何信号时做的。

进程在从内核空间返回用户空间时会检查是否有挂起信号,如果有的话,调用do_signal进行处理。

static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
......
		/* deal with pending signal delivery */
		if (cached_flags & _TIF_SIGPENDING)
			do_signal(regs);
......
}

do_signal需要调用get_signal来填充一个ksignal结构体。

void do_signal(struct pt_regs *regs)
{
......
	if (get_signal(&ksig)) {
		/* Whee! Actually deliver the signal.  */
		handle_signal(&ksig, regs);
		return;
	}
......
}

get_signal过程中,会检查当前进程是否处于被ptrace跟踪的状态,如果是的话,且当前信号不是SIGKILL,则会调用ptrace_signal

int get_signal(struct ksignal *ksig)
{
......
		if (unlikely(current->ptrace) && signr != SIGKILL) {
			signr = ptrace_signal(signr, &ksig->info);
			if (!signr)
				continue;
		}
......
}

ptrace_signal调用ptrace_stop来使当前进程停下来。

static int ptrace_signal(int signr, siginfo_t *info)
{
......
	ptrace_stop(signr, CLD_TRAPPED, 0, info);
......
}

ptrace_stop首先将进程状态设置为TASK_TRACED,这样的话,下次进行进程调度时就不会调度该进程了。设置完进程状态后,会发送SIGCHLD信号通知当前进程的parentreal_parent。最后调用freezable_schedule进行进程调度,将该进程停下来。

static void ptrace_stop(int exit_code, int why, int clear_code, siginfo_t *info)
	__releases(&current->sighand->siglock)
	__acquires(&current->sighand->siglock)
{
......
	set_current_state(TASK_TRACED);
......
		do_notify_parent_cldstop(current, true, why);
		if (gstop_done && ptrace_reparented(current))
			do_notify_parent_cldstop(current, false, why);
......
		freezable_schedule();
......
}

为了能让tracee停止运行,信号是必不可少的,无论是PTRACE_ATTACH请求、breakpoint或者是watchpoint,都依赖于给tracee发送一个信号,同时tracee还要有机会去检查当前有哪些挂起的信号。就像文章一开始提到的问题,进程一直卡在内核空间,没有机会在返回用户空间时检查当前挂起的信号,也就无法停止运行了。

至此ptrace请求的前置条件就完全满足了,可以使用其他ptrace请求了。

其他的ptrace请求无非是读写tracee的task_struct、内存和寄存器了,似乎没什么太特别的地方,也就不再分析了。

结束语

以上就是ptrace系统调用实现的大致原理。不过有一部分没有提到,就是tracer与tracee如何detach,主要是因为这部分和文章开头提到的问题不相干,所以就没有看这部分的源码,有兴趣的可以自己看一下。

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

<think>我们被要求详细解释ptrace系统调用的参数及其作用,并提供使用案例。根据引用[2]和引用[3],ptrace系统调用的函数原型是: ```c long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); ``` 它有四个参数:request, pid, addr, data。 下面我们将详细解释每个参数: 1. **request (枚举类型 __ptrace_request)**: 这是ptrace的核心操作指令,告诉ptrace执行什么操作。request的值决定了其他参数的意义。常见的request值包括(但不限于): - `PTRACE_TRACEME`: 表示本进程将被其父进程跟踪。通常由被跟踪进程(tracee)调用。此时,其他参数被忽略。 - `PTRACE_ATTACH` 或 `PTRACE_SEIZE`: 附加到指定pid的进程,使其成为被跟踪者(tracee)。 - `PTRACE_DETACH`: 分离tracee,使其恢复运行。 - `PTRACE_PEEKTEXT`, `PTRACE_PEEKDATA`: 从tracee的地址空间中读取内存(addr指定地址)。读取的数据通过ptrace的返回值返回,data参数被忽略。 - `PTRACE_POKETEXT`, `PTRACE_POKEDATA`: 将数据写入tracee的地址空间(写入到addr指定的地址,数据为data)。 - `PTRACE_GETREGS`, `PTRACE_GETFPREGS`: 获取tracee的通用寄存器或浮点寄存器的值。此时,addr被忽略,data指向一个缓冲区(用于存放寄存器值)。 - `PTRACE_SETREGS`, `PTRACE_SETFPREGS`: 设置tracee的寄存器值。 - `PTRACE_SYSCALL`, `PTRACE_CONT`: 让停止的tracee继续运行。`PTRACE_SYSCALL`会在进入和退出系统调用时停止,而`PTRACE_CONT`则是一直运行直到遇到下一个信号。 - `PTRACE_SINGLESTEP`: 单步执行一条指令。 - 更多请求如`PTRACE_KILL`(终止tracee)等。 注意:request的值在不同的系统上可能有所不同,具体需要参考系统头文件(<sys/ptrace.h>)和文档。 2. **pid (pid_t)**: 指定目标进程的进程ID。对于某些request(如`PTRACE_TRACEME`),pid参数被忽略。 3. **addr (void *)**: 这是一个内存地址,其含义取决于request。例如: - 在`PTRACE_PEEKTEXT`和`PTRACE_PEEKDATA`中,addr是要读取的内存的地址。 - 在`PTRACE_POKETEXT`和`PTRACE_POKEDATA`中,addr是要写入的内存的地址。 - 在`PTRACE_ATTACH`中,addr被忽略。 4. **data (void *)**: 这是一个指向数据的指针,其含义也取决于request。例如: - 在`PTRACE_POKE*`请求中,data是要写入的值(注意,当写入一个字时,data实际上被当作一个长整型值,而不是指针)。 - 在`PTRACE_SETREGS`中,data指向一个包含寄存器值的结构体。 - 在`PTRACE_CONT`中,data被解释为要传递给tracee的信号(通常为0表示有信号)。 **使用案例**: 下面我们通过两个简单的例子来说明ptrace的使用。 **案例1:跟踪子进程(使用PTRACE_TRACEME)** 父进程创建子进程,子进程调用`ptrace(PTRACE_TRACEME, 0, NULL, NULL)`,然后父进程可以监控子进程。 ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/user.h> int main() { pid_t child_pid = fork(); if (child_pid == 0) { // 子进程:请求被父进程跟踪 ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 之后,子进程执行的任何信号(除了SIGKILL)都会导致它停止,父进程会得到通知 execl("/bin/ls", "ls", NULL); // 执行一个程序 } else { // 父进程:等待子进程停止 int status; waitpid(child_pid, &status, 0); // 循环跟踪子进程 while (WIFSTOPPED(status)) { // 打印当前系统调用(需要根据具体系统实现) // 这里我们只是让子进程继续运行,并在每次系统调用时停止 ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL); waitpid(child_pid, &status, 0); } } return 0; } ``` **案例2:附加到一个运行中的进程(使用PTRACE_ATTACH)** 这个例子中,我们将附加到一个正在运行的进程,然后读取它的寄存器的值。 ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/user.h> int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <pid>\n", argv[0]); return 1; } pid_t pid = atoi(argv[1]); // 目标进程的pid // 附加到目标进程 if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) { perror("ptrace attach failed"); return 1; } // 等待目标进程停止 int status; waitpid(pid, &status, 0); // 读取目标进程的寄存器 struct user_regs_struct regs; if (ptrace(PTRACE_GETREGS, pid, NULL, &regs) == -1) { perror("ptrace getregs failed"); } else { // 打印指令指针 printf("Instruction pointer: 0x%llx\n", (long long)regs.rip); // 64位系统用rip,32位用eip } // 分离目标进程 ptrace(PTRACE_DETACH, pid, NULL, NULL); return 0; } ``` 注意:上面的代码是64位Linux系统的示例,32位系统需要使用`eip`,并且结构体名称可能为`struct user_regs_struct`(32位)或`struct user_regs`(视系统而定)。实际编程中需要根据系统调整。 **安全相关案例**: 引用[5]提到了seccomp与ptrace结合使用来hook系统调用。seccomp可以设置一个过滤器(BPF程序),在系统调用执行前进行过滤。而ptrace则可以在系统调用被拦截后进行更细致的操作(如修改参数、返回值等)。这种组合常用于沙箱和安全软件中。 例如,一个简单的seccomp+ptrace demo可能如下: 1. 父进程设置seccomp过滤器,只允许部分系统调用,对于不允许的系统调用,它会发送`SIGSYS`信号。 2. 父进程使用ptrace来跟踪子进程,当子进程触发了一个被seccomp禁止的系统调用时,它会停止(因为收到`SIGSYS`),父进程就可以通过ptrace修改系统调用的参数或返回值,从而模拟执行或拒绝。 由于这个案例相对复杂,这里不展开代码,但可以理解其原理。 **注意事项**: - 使用ptrace需要权限。非root用户只能跟踪自己的进程。 - ptrace是一个复杂的系统调用,使用不当可能导致程序崩溃。 - 不同架构(x86, ARM)和不同操作系统版本可能有所差异,编程时需注意可移植性。 最后,我们生成相关问题: 相关问题: 1. ptrace系统调用中,request参数`PTRACE_TRACEME`和`PTRACE_ATTACH`有什么区别? 2. 如何使用ptrace修改另一个进程的内存内容? 3. ptrace与seccomp结合使用有哪些典型场景? 4. 在调试器中(如gdb),如何利用ptrace实现断点功能?</think>### 详细解析 `ptrace` 系统调用 #### 一、函数原型与参数解析 ```c long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); ``` 1. **`request`(操作类型)** 控制跟踪行为的核心参数,支持以下主要操作类型: - `PTRACE_TRACEME`:被跟踪进程声明自身可被父进程追踪(`pid`/`addr`/`data` 忽略) - `PTRACE_ATTACH`:附加到运行中的进程(`pid` 为目标进程ID) - `PTRACE_DETACH`:分离跟踪进程 - `PTRACE_PEEKTEXT`:读取目标进程内存(`addr` 为内存地址) - `PTRACE_POKEDATA`:写入目标进程内存 - `PTRACE_GETREGS`:获取寄存器值(`data` 指向 `struct user_regs_struct`) - `PTRACE_SETREGS`:设置寄存器值 - `PTRACE_SYSCALL`:在系统调用入口/出口暂停 - `PTRACE_SINGLESTEP`:单步执行 2. **`pid`(目标进程ID)** 指定被跟踪进程的进程ID。当 `request=PTRACE_TRACEME` 时此参数被忽略。 3. **`addr`(内存地址)** 内存操作的目标地址,含义随 `request` 变化: - 内存读写操作:目标进程的虚拟地址 - 寄存器操作:通常忽略 - 系统调用追踪:系统调用号(如 `__NR_open`) 4. **`data`(数据指针)** 数据传输缓冲区,类型随 `request` 变化: - 内存写入:包含写入数据的指针 - 寄存器操作:指向 `struct user_regs_struct` 的指针 - 信号传递:信号值(如 `SIGSTOP`) - 其他操作:通常为 `NULL` #### 二、核心功能机制 1. **进程控制** 通过 `PTRACE_ATTACH/PTRACE_DETACH` 实现动态附加/分离进程,被附加进程会收到 `SIGSTOP` 信号暂停执行[^4]。 2. **内存访问** 使用 `PTRACE_PEEKTEXT` 读取内存示例: ```c long data = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); ``` 3. **寄存器操作** 获取/修改 CPU 寄存器实现调试: ```c struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, NULL, &regs); regs.rip += 2; // 修改指令指针 ptrace(PTRACE_SETREGS, pid, NULL, &regs); ``` 4. **系统调用拦截** `PTRACE_SYSCALL` 在系统调用前后暂停进程,结合 `seccomp` 可实现安全沙箱[^5]: ```c ptrace(PTRACE_SYSCALL, pid, NULL, NULL); waitpid(pid, &status, 0); ``` #### 三、典型应用案例 ##### 案例1:实现简易调试器 ```c #include <sys/ptrace.h> #include <sys/wait.h> void debugger(pid_t child_pid) { int status; waitpid(child_pid, &status, 0); // 等待子进程停止 while (WIFSTOPPED(status)) { // 获取当前系统调用 struct user_regs_struct regs; ptrace(PTRACE_GETREGS, child_pid, NULL, &regs); printf("系统调用: %lld\n", regs.orig_rax); // 单步执行 ptrace(PTRACE_SINGLESTEP, child_pid, NULL, NULL); waitpid(child_pid, &status, 0); } } ``` ##### 案例2:系统调用过滤(结合 seccomp) ```c #include <linux/seccomp.h> #include <linux/filter.h> // seccomp 规则:仅允许 read/write 系统调用 struct sock_filter filter[] = { BPF_STMT(BPF_LD|BPF_W|BPF_ABS, offsetof(struct seccomp_data, nr)), BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_read, 0, 1), BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW), BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_write, 0, 1), BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW), BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_TRACE), // 其他调用转给 ptrace }; // ptrace 处理 void handle_syscall(pid_t pid) { ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESECCOMP); while (1) { waitpid(pid, &status, 0); if (WSTOPSIG(status) == SIGTRAP) { // 检查并过滤危险系统调用 ptrace(PTRACE_SYSCALL, pid, NULL, NULL); } } } ``` #### 四、技术原理 1. **进程状态控制** ```mermaid graph TB A[跟踪进程调用 ptrace] --> B{request 类型} B -->|PTRACE_ATTACH| C[发送 SIGSTOP 给目标] B -->|PTRACE_DETACH| D[恢复目标执行] B -->|PTRACE_SYSCALL| E[在 syscall 入口暂停] E --> F[修改寄存器/内存] F --> G[恢复执行到 syscall 出口] ``` 2. **内存访问机制** ptrace 通过内核直接访问目标进程的页表,在 `copy_from_user()`/`copy_to_user()` 安全边界内操作内存[^3]。 #### 五、安全注意事项 1. **权限要求**: - 非 root 进程只能跟踪相同 UID 的进程 - `CAP_SYS_PTRACE` 能力可突破此限制 2. **性能影响**: - 每次系统调用产生 2 次上下文切换 - 实时程序慎用 3. **反调试对抗**: - 目标进程可通过 `PT_DENY_ATTACH`(macOS)或 `prctl(PR_SET_DUMPABLE, 0)`(Linux)抵抗 --- ### 相关问题 1. `ptrace` 与 `strace` 的实现关系是什么?[^2] 2. 如何通过 `ptrace` 实现进程内存热替换? 3. `ptrace` 在容器环境中有哪些权限限制? 4. 现代调试器(如 GDB)如何使用 `ptrace` 实现断点功能?[^2]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值