前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Linux ptrace 的实现

Linux ptrace 的实现

作者头像
theanarkh
发布于 2021-12-09 06:46:16
发布于 2021-12-09 06:46:16
1.6K00
代码可运行
举报
文章被收录于专栏:原创分享原创分享
运行总次数:0
代码可运行

前言:ptrace 是 Linux 内核提供的非常强大的系统调用,通过 ptrace 可以实现进程的单步调试和收集系统调用情况。比如 strace 和 gdb 都是基于 ptrace 实现的,strace 可以显示进程调用了哪些系统调用,gdb 可以实现对进程的调试。本文介绍这些工具的底层 ptrace 是如何实现的。这里选用了 1.2.13 的早期版本,原理是类似的,新版内核代码过多,没必要陷入过多细节中。

1 进程调试

ptrace 系统调用的实现中包含了很多功能,首先来看一下单步调试的实现。通过 ptrace 实现单步调试的方式有两种。

1. 父进程执行 fork 创建一个子进程,通过 ptrace 设置子进程为 PF_PTRACED 标记,然后执行 execve 加载被调试的程序。

2. 通过 ptrace attach 到指定的 pid 完成对进程的调试(控制)。

首先看一下第一种的实现。

1.1 方式1

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pid_t pid = fork();// 子进程if (pid == 0) {
    ptrace(PTRACE_TRACEME,0,NULL,NULL);
    // 加载被调试的程序
    execve(argv[1], NULL, NULL);}

执行 fork 创建子进程后,通过 ptrace 的 PTRACE_TRACEME 指示操作系统设置子进程为被调试(设置 PF_PTRACED 标记)。来看一下这一步操作系统做了什么事情。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
asmlinkage int sys_ptrace(long request, long pid, long addr, long data){
    if (request == PTRACE_TRACEME) {
        current->flags |= PF_PTRACED;
        return 0;
    }}

这一步非常简单,接着看 execve 加载程序到内存执行时又是如何处理的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs) {
    // 加载程序
    for (fmt = formats ; fmt ; fmt = fmt->next) {
        int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
        retval = fn(&bprm, regs);
    }}

do_execve 逻辑非常复杂,不过我们只关注需要的就好。do_execve 通过钩子函数加载程序,我们看看 formats 是什么。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct linux_binfmt {
    struct linux_binfmt * next;
    int *use_count;
    int (*load_binary)(struct linux_binprm *, struct  pt_regs * regs);
    int (*load_shlib)(int fd);
    int (*core_dump)(long signr, struct pt_regs * regs);};
static struct linux_binfmt *formats = &aout_format;int register_binfmt(struct linux_binfmt * fmt){    struct linux_binfmt ** tmp = &formats;

    if (!fmt)
        return -EINVAL;
    if (fmt->next)
        return -EBUSY;
    while (*tmp) {
        if (fmt == *tmp)
            return -EBUSY;
        tmp = &(*tmp)->next;
    }
    *tmp = fmt;
    return 0;   
}

可以看到 formats 是一个链表。可以通过 register_binfmt 函数注册节点。那么谁调用了这个函数呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct linux_binfmt elf_format = {
    NULL, NULL, load_elf_binary, load_elf_library, NULL};int init_module(void) {
    register_binfmt(&elf_format);
    return 0;}

所以最终调用了 load_elf_binary 函数加载程序。同样我们只关注相关的逻辑。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (current->flags & PF_PTRACED)
        send_sig(SIGTRAP, current, 0);

load_elf_binary 中会判断如果进程设置了 PF_PTRACED 标记,那么会给当前进程发送一个 SIGTRAP 信号。接着看信号处理函数的相关逻辑。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if ((current->flags & PF_PTRACED) && signr != SIGKILL) {
    current->exit_code = signr;
    // 修改当前进程(被调试的进程)为暂停状态
    current->state = TASK_STOPPED;
    // 通知父进程
    notify_parent(current);
    // 调度其他进程执行
    schedule();}

所以程序被加载到内存后,根本没有机会执行就直接被修改为暂停状态了,接下来看看 notify_parent 通知父进程干什么。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void notify_parent(struct task_struct * tsk){   
    // 给父进程发送 SIGCHLD 信号
    if (tsk->p_pptr == task[1])
        tsk->exit_signal = SIGCHLD;
    send_sig(tsk->exit_signal, tsk->p_pptr, 1);
    wake_up_interruptible(&tsk->p_pptr->wait_chldexit);}

父进程收到信号后,可以通过 sys_ptrace 控制子进程,sys_ptrace 还提供了很多功能,比如读取子进程的数据。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// pid 为子进程 id
num = ptrace(PTRACE_PEEKUSER, pid, ORIG_RAX * 8, NULL);

这个就不展开了,主要是内存的校验和数据读取。这里讲一下 PTRACE_SINGLESTEP 命令,这个命令控制子进程单步执行的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
case PTRACE_SINGLESTEP: {  /* set the trap flag. */
        long tmp;
        child->flags &= ~PF_TRACESYS;
        // 设置 eflags 的单步调试 flag
        tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) | TRAP_FLAG;
        put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);
        // 修改子进程状态为可执行
        child->state = TASK_RUNNING;
        child->exit_code = data;
        return 0;
}

PTRACE_SINGLESTEP 让子进程重新进入运行状态,但是有一个很关键的是,设置好了单步调试 flag。我们看看 trap flag 是什么。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
A trap flag permits operation of a processor in single-step mode. If such a flag is available, debuggers can use it to step through the execution of a computer program.

也就是说,子进程执行一个指令后,就会被中断,然后系统会给被调试进程发送 SIGTRAP 信号。同样,被调试进程在信号处理函数里,通知父进程,从而控制权又回到了父进程手中,如此循环。

1.2 方式2

除了开始时通过 ptrace 设置进程调试,也可以通过 ptrace 动态设置调试进程的能力,具体是通过 PTRACE_ATTACH 命令实现的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (request == PTRACE_ATTACH) {
        // 设置被调试标记
        child->flags |= PF_PTRACED;
        // 设置和父进程的关系
        if (child->p_pptr != current) {
            REMOVE_LINKS(child);
            child->p_pptr = current;
            SET_LINKS(child);
        }
        // 给被调试进程发送 SIGSTOP 信号
        send_sig(SIGSTOP, child, 1);
        return 0;
}

前面已经分析过,信号处理函数里会设置进程为暂停状态,然后通知主进程,主进程就可以控制子进程,具体和前面流程一样。

2 跟踪系统调用

ptrace 处理追踪进程执行过程之外,还可以实现跟踪系统调用。具体是通过 PTRACE_SYSCALL 命令实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
case PTRACE_SYSCALL:case PTRACE_CONT: {    long tmp;
    // 设置 PF_TRACESYS 标记
    if (request == PTRACE_SYSCALL)
        child->flags |= PF_TRACESYS;
    child->exit_code = data;
    child->state = TASK_RUNNING;
    // 清除 trap flag 标记
    tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) & ~TRAP_FLAG;
    put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);
    return 0;}

看起来很简单,就是设置了一个新的标记 PF_TRACESYS。看看这个标记有什么用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 调用 syscall_trace 函数1:  call _syscall_trace
    movl     movl ORIG_EAX(%esp),%eax
    // 调用系统调用
    call _sys_call_table(,%eax,4)
    movl %eax,EAX(%esp)     # save the return value
    movl _current,%eax
    movl errno(%eax),%edx
    negl %edx
    je 1f
    movl %edx,EAX(%esp)
    orl $(CF_MASK),EFLAGS(%esp) # set carry to indicate error
// 调用 syscall_trace 函数1:  call _syscall_trace

可以看到在系统调用的前后都有一个 syscall_trace 的逻辑,所以在系统调用前和后,我们都可以做点事情。来看看这个函数做了什么。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
asmlinkage void syscall_trace(void){
    // 暂停子进程,通知父进程,并调度其他进程执行
    current->exit_code = SIGTRAP;
    current->state = TASK_STOPPED;
    notify_parent(current);
    schedule();}

这里的逻辑就是把逻辑切换到主进程中,然后主进程就可以通过命令获取被调试进程的系统调用信息。下面是一个追踪进程所有系统调用的例子。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
  use ptrace to find all system call that call by certain process
*/#include <sys/ptrace.h>#include <unistd.h>#include <stdlib.h>#include <sys/wait.h>#include <stdio.h>#include <sys/reg.h>
int main(int argc, char *argv[]) {    pid_t pid = fork();
    if (pid < 0) {
        printf("fork failed");
        exit(-1);
    } else if (pid == 0) {
        // set state of child process to PTRACE
        ptrace(PTRACE_TRACEME,0,NULL,NULL);
        // child will change to stopped state when in execve call, then send the signal to parent
        execve(argv[1], NULL, NULL);
    } else {
        int status;
        int bit = 1;
        long num;
        long ret;
        // wait for child
        wait(&status);
        if(WIFEXITED(status))
            return 0;
        // this is for execve call which will not return, and for os of 64-it => ORIG_RAX * 8 or os of 32-it => ORIG_EAX * 4
        num = ptrace(PTRACE_PEEKUSER, pid, ORIG_RAX * 8, NULL);
        printf("system call num = %ld\n", num);
        ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
        while(1) {
            wait(&status);
            if(WIFEXITED(status))
                return 0;
            // for enter system call
            if(bit) {
                num = ptrace(PTRACE_PEEKUSER, pid, ORIG_RAX * 8, NULL);
                printf("system call num = %ld", num);
                bit = 0;
            } else { // for return of system call
                ret = ptrace(PTRACE_PEEKUSER, pid, RAX*8, NULL);
                printf("system call return = %ld \n", ret);
                bit = 1;
            }
            // let this child process continue to run until call next system call
            ptrace(PTRACE_SYSCALL,pid,NULL,NULL);
        }
    }}

总结

ptrace 功能复杂而强大,理解它的原理对理解其他技术和工具都非常有意义,本文大概做了一个介绍,有兴趣的同学可以自行查看源码。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
GDB原理之ptrace实现原理
在程序出现bug的时候,最好的解决办法就是通过 GDB 调试程序,然后找到程序出现问题的地方。比如程序出现 段错误(内存地址不合法)时,就可以通过 GDB 找到程序哪里访问了不合法的内存地址而导致的。
用户7686797
2020/11/05
4.7K0
GDB原理之ptrace实现原理
Linux Hook 笔记
相信很多人对"Hook"都不会陌生,其中文翻译为"钩子”.在编程中, 钩子表示一个可以允许编程者插入自定义程序的地方,通常是打包好的程序中提供的接口. 比如,我们想要提供一段代码来分析程序中某段逻辑路径被执行的频率,或者想要在其中 插入更多功能时就会用到钩子. 钩子都是以固定的目的提供给用户的,并且一般都有文档说明. 通过Hook,我们可以暂停系统调用,或者通过改变系统调用的参数来改变正常的输出结果, 甚至可以中止一个当前运行中的进程并且将控制权转移到自己手上.
evilpan
2023/02/12
2.9K0
Linux Hook 笔记
Linux下进程的创建过程分析(_do_fork do_fork详解)--Linux进程的管理与调度(八)
Unix标准的复制进程的系统调用时fork(即分叉),但是Linux,BSD等操作系统并不止实现这一个,确切的说linux实现了三个,fork,vfork,clone(确切说vfork创造出来的是轻量级进程,也叫线程,是共享资源的进程)
233333
2018/10/09
2.7K0
Linux下进程的创建过程分析(_do_fork do_fork详解)--Linux进程的管理与调度(八)
一文看懂 | fork 系统调用
Unix标准的复制进程的系统调用时fork(即分叉),但是Linux,BSD等操作系统并不止实现这一个,确切的说linux实现了三个,fork,vfork,clone(确切说vfork创造出来的是轻量级进程,也叫线程,是共享资源的进程)
用户7686797
2021/10/20
2.6K0
linux-沙盒入门,ptrace从0到1
本文是在linux系统角度下,对ptrace反调试进行底层分析,使我们更清楚的看到一些底层原理的实现,更好的理解在逆向工程中的一些突破口,病毒怎么实现代码注入,本文还将列出一些常见的攻防手段,分析其原理,让我们一同见证见证茅与盾激情对决!内容很充实,建议躺着阅读!!!!!!!!
Gamma实验室
2021/03/10
4.4K1
linux-沙盒入门,ptrace从0到1
如何利用Ptrace拦截和模拟Linux系统调用
ptrace(2)这个系统调用一般都跟调试离不开关系,它不仅是类Unix系统中本地调试器监控实现的主要机制,而且它还是strace系统调用常用的实现方法。ptrace()系统调用函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法,它不仅可以监控系统调用,而且还能够检查和改变“tracee”进程的内存和寄存器里的数据,甚至它还可以拦截系统调用。
FB客服
2018/07/31
2K0
Linux kernel Namespace源码分析
学习一下linux kernel namespace的代码还是很有必要的,让你对docker容器的namespace隔离有更深的认识。我的源码分析,是基于Linux Kernel 4.4.19 (https://www.kernel.org/pub/linux/kernel/v4.x/patch-4.4.19.gz)版本的,由于namespace模块更新很少,因此其他相近版本之间雷同。User namespace由于与其他namespaces耦合在一起,比较难分析,我将在后续再作分析。 Kernel,Nam
Walton
2018/04/13
13.1K0
Linux kernel Namespace源码分析
linux内核进程创建fork源码解析
    平时写过多进程多线程程序,比如使用linux的系统调用fork创建子进程和glibc中的nptl包里的pthread_create创建线程,甚至在java里使用Thread类创建线程等,虽然使用问题不大,但需要知道底层原理。这次在自己写操作系统的时候,看了一遍linux内核的进程创建过程。算是有了比较深入的理解。
用户4415180
2022/06/23
8.8K0
linux内核进程创建fork源码解析
进程的描述和创建
进程在内核态运行时需要自己的堆栈信息,linux内核为每个进程都提供了一个内核栈。对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:
De4dCr0w
2019/02/27
9150
Linux进程启动过程分析do_execve(可执行程序的加载和运行)---Linux进程的管理与调度(十一)
我们前面提到了, fork, vfork等复制出来的进程是父进程的一个副本, 那么如何我们想加载新的程序, 可以通过execve来加载和启动新的程序。
233333
2018/10/09
4.1K0
Linux进程启动过程分析do_execve(可执行程序的加载和运行)---Linux进程的管理与调度(十一)
Linux 是否有 zombie thread?源码探究分析
系统编程课上遇到的一个问题:Linux下,如果一个 pthread_create 创建的线程没有被 pthread_join 回收,是否会和僵尸进程一样,产生“僵尸线程”?
Miigon
2022/11/25
1.8K0
do_fork 的实现
上面讲述了如何通过 fork, vfork, pthread_create 去创建一个进程,或者一个线程。通过分析最终 fork, vfork, pthread_create 最终都会通过系统调用 do_fork 去创建进程。
刘盼
2021/07/05
7680
do_fork 的实现
自己动手写一个 strace
用过 strace 的同学都知道,strace 是用来跟踪进程调用的 系统调用,还可以统计进程对 系统调用 的统计等。strace 的使用方式有两种,如下:
用户7686797
2020/11/20
5300
自己动手写一个 strace
do_fork实现--上
在前面几节中讲述了如何通过fork, vfork, pthread_create去创建一个进程,或者一个线程。通过分析最终fork, vfork, pthread_create最终会通过系统调用clone去创建进程。
DragonKingZhu
2020/03/24
1.4K0
do_fork实现--上
linux0.11系统调用过程和fork源码解析
所以执行fork函数就会执行system_call函数,但是在这之前,还有些事情需要做,就是保存现场。下面是操作系统执行系统调用前,在内核栈里保存的寄存器,这个压入的寄存器和iret中断返回指令出栈的寄存器是对应的。其中ip指向的是调用系统调用返回后的下一句代码。
theanarkh
2019/04/24
1.5K0
linux0.11系统调用过程和fork源码解析
Linux进程详解
程序是指储存在外部存储(如硬盘)的一个可执行文件, 而进程是指处于执行期间的程序, 进程包括 代码段(text section) 和 数据段(data section), 除了代码段和数据段外, 进程一般还包含打开的文件, 要处理的信号和CPU上下文等等.
用户7686797
2020/08/25
4.1K0
Linux进程详解
Ptrace使用
ptrace共有四个参数: long ptrace(enum __ptrace_request request,pid_t pid,void *addr,void *data); 其中第一个参数可以取如下内容:
Wilbur-L
2021/02/05
2K0
Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)
exit是c语言的库函数,他最终调用_exit。在此之前,先清洗标准输出的缓存,调用用atexit注册的函数等, 在c语言的main函数中调用return就等价于调用exit。
233333
2018/10/09
6.3K0
分析Linux系统的执行过程
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
matt
2022/10/25
1K0
分析Linux系统的执行过程
linux进程虚拟空间布局
首先看linux进程在32位处理器下的虚拟空间内存布局,以i386 32位机器为例
用户4415180
2022/06/23
2.5K0
linux进程虚拟空间布局
相关推荐
GDB原理之ptrace实现原理
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验