前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >吃土记之GDB调试原理

吃土记之GDB调试原理

作者头像
程序员小王
发布2023-04-03 18:44:27
9330
发布2023-04-03 18:44:27
举报
文章被收录于专栏:架构说架构说

目录

ptrace实现原理

本文使用的 Linux 2.4.16 版本的内核

看懂本文需要的基础:进程调度,内存管理和信号处理相关知识。

前言

青铜:

  • 你知道进程 Segmentation fault 回造成core

是因为操作系统--内存管理模块 检查异常,然后发出信号signal 11。

中断应用程序正常执行flow。执行信号处理函数。

  • 你知道 redis 通过 fork 管道 信号SIGCHLD方式完成持久化。
  • 你知道valgrind通过非侵式方式,在机器码层接管程序,通过跟踪汇编汇总寄存器 栈 来进程内存泄漏。启动注入代码方式检查死锁

然后这些东西不会融会贯通

吃土记录

像外行一样思考,像专家一样实践:

小王:遇到core怎么办?

老王:gdb调试呀

小王:gdb 为什么可以非侵入调试进程呀。

老王:这个我没想过。。。平时不考虑这个问题

  • gdb基本上大家都在用,你有没有想过它的实现原理是什么?
  • 为什么它可以控制程序执行、中断、访问内存甚至直接使程序流程改变?
  • 在使用gdb调试程序时,程序的进程状态是”T”,但又似乎并非接到了SIGSTOP信号, 那么这个”T”是什么呢?

查缺补漏

If you are thinking of using complex kernel programming to accomplish tasks, think again. Linux provides an elegant mechanism to achieve all of these things: the ptrace (Process Trace) system call.

ptrace provides a mechanism by which a parent process may observe and control the execution of another process. It can examine and change its core image and registers and is used primarily to implement breakpoint debugging and system call tracing.

如果您正在考虑使用复杂的内核编程来完成任务, PTRACE_TRACEME 请三思。Linux 提供了一种优雅的机制来实现所有这些功能: ptrace (进程跟踪)系统调用。

Ptrace 提供了一种机制,通过这种机制,父进程可以观察和控制另一个进程的执行。

它可以检查和更改其核心映像和寄存器,主要用于实现断点调试和系统调用跟踪。

他们幕后原理工作其实就是ptrace完成的。

  • gdb主要功能的实现依赖于一个系统函数ptrace,通过man手册可以了解到,

ptrace可以让父进程观察和控制其子进程的检查、执行,改变其寄存器和内存的内容,

可以使程序员在程序运行的时候观察程序在内存/寄存器中的使用情况

主要应用于打断点(也是gdb的主要功能)和打印系统调用轨迹。

GDB常用的使用方法有断点设置和单步跟踪

代码语言:javascript
复制
NAME
       ptrace - process trace

SYNOPSIS
       #include <sys/ptrace.h>

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

DESCRIPTION
       The  ptrace()  system  call  provides a means by which one process (the
       "tracer") may observe and control the execution of another process (the
       "tracee"),  and  examine  and change the tracee's memory and registers.
       It is primarily used to implement breakpoint debugging and system  call
       tracing.
       
DESCRIPTION
       The ptrace() system call provides a means by which a parent process may observe and control the execution of another process,
       and examine and change its core image and registers.  It is primarily used to implement breakpoint debugging and system  call
       tracing.
       

下面解释一下 ptrace() 各个参数的作用:

  • request:指定调试的指令,指令的类型很多, 如:PTRACE_TRACEME、PTRACE_PEEKUSER、PTRACE_CONT、PTRACE_GETREGS等等,下面会介绍不同指令的作用。

PTRACE_TRACEME 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。

PTRACE_SINGLESTEP 设置单步执行标志

PTRACE_ATTACH 跟踪指定pid 进程。

代码语言:javascript
复制
/ 在linux/kernel/ptrace.c文件中
SYSCALL_DEFINE4(ptrace, long, request, long, pid, long, addr, long, data)



asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    struct task_struct *child;
    struct user *dummy = NULL;
    int i, ret;

    ...

    read_lock(&tasklist_lock);
    child = find_task_by_pid(pid); // 获取 pid 对应的进程 task_struct 对象
    if (child)
        get_task_struct(child);
    read_unlock(&tasklist_lock);
    if (!child)
        goto out;

    if (request == PTRACE_ATTACH) {
        ret = ptrace_attach(child);
        goto out_tsk;
    }

    ...

    switch (request) {
    case PTRACE_PEEKTEXT:
    case PTRACE_PEEKDATA:
        ...
    case PTRACE_PEEKUSR:
        ...
    case PTRACE_POKETEXT:
    case PTRACE_POKEDATA:
        ...
    case PTRACE_POKEUSR:
        ...
    case PTRACE_SYSCALL:
    case PTRACE_CONT:
        ...
    case PTRACE_KILL: 
        ...
    case PTRACE_SINGLESTEP:
        ...
    case PTRACE_DETACH:
        ...
    }
out_tsk:
    free_task_struct(child);
out:
    unlock_kernel();
    return ret;
}

#define PTRACE_TRACEME         0

进入被追踪模式(PTRACE_TRACEME操作)
当要调试一个进程时,需要使进程进入被追踪模式,怎么使进程进入被追踪模式呢?

有两个方法:

被调试的进程调用 ptrace(PTRACE_TRACEME, ...) 来使自己进入被追踪模式。

调试进程(如GDB)调用 ptrace(PTRACE_ATTACH, pid, ...) 来使指定的进程进入被追踪模式。


#define PTRACE_PEEKTEXT        1
#define PTRACE_PEEKDATA        2
#define PTRACE_PEEKUSR         3
#define PTRACE_POKETEXT        4
#define PTRACE_POKEDATA        5
#define PTRACE_POKEUSR         6
#define PTRACE_CONT            7
#define PTRACE_KILL            8
#define PTRACE_SINGLESTEP      9
#define PTRACE_ATTACH       0x10
#define PTRACE_DETACH       0x11
#define PTRACE_SYSCALL        24
#define PTRACE_GETREGS        12
#define PTRACE_SETREGS        13
#define PTRACE_GETFPREGS      14
#define PTRACE_SETFPREGS      15
#define PTRACE_GETFPXREGS     18
#define PTRACE_SETFPXREGS     19
#define PTRACE_SETOPTIONS     21
  • pid:进程的ID(这个不用解释了)。
  • addr:进程的某个地址空间,可以通过这个参数对进程的某个地址进行读或写操作。
  • data:根据不同的指令

二、gdb使用ptrace的基本流程

  • gdb调试一个新进程:通过fork函数创建一个新进程,在子进程中执行ptrace(PTRACE_TRACEME, 0, 0, 0)函数,然后通过execv()调用准备调试的程序。
  • attach到已运行进程:将pid传递给gdb,然后执行ptrace(PTRACE_ATTACH, pid, 0, 0)。
  • 在使用参数为PTRACE_TRACEME或PTRACE_ATTACH的ptrace系统调用建立调试关系之后,交付给
  • 目标程序的任何信号(除SIGKILL之外)都将被gdb先行截获,gdb因此有机会对信号进行相应处 理, 并根据信号的属性决定在继续目标程序运行时是否将之前截获的信号实际交付给目标程序。

单步调试模式(PTRACE_SINGLESTEP)

单步调试是一个比较有趣的功能,当把被调试进程设置为单步调试模式后,被调试进程没执行一条CPU指令都会停止执行,并且向父进程(调试进程)发送一个 SIGCHLD 信号。

我们来看看 ptrace() 函数对 PTRACE_SINGLESTEP 操作的处理过程,代码如下:

代码语言:javascript
复制
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    ...
    switch (request) {
    case PTRACE_SINGLESTEP: {  /* set the trap flag. */
        long tmp;
        ...
        tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
        put_stack_long(child, EFL_OFFSET, tmp);
        child->exit_code = data;
        /* give it a chance to run. */
        wake_up_process(child);
        ret = 0;
        break;
    }
    ...
}


  process.
       D    Uninterruptible sleep (usually IO)
       R    Running or runnable (on run queue)
       S    Interruptible sleep (waiting for an event to complete)
       T    Stopped, either by a job control signal or because it is being traced.
       
       https://stackoverflow.com/questions/7844569/ptrace-single-step-in-the-kernel-from-process-context
       W    paging (not valid since the 2.6.xx kernel)
       X    dead (should never be seen)
       Z    Defunct ("zombie") process, terminated but not reaped by its parent.
D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。
R (TASK_RUNNING),进程执行中。
S (TASK_INTERRUPTIBLE),可中断的睡眠状态。
T (TASK_STOPPED),暂停状态。
t (TASK_TRACED),进程被追踪。
w (TASK_PAGING),进程调页中,2.6以上版本的内核中已经被移除。
X (TASK_DEAD – EXIT_DEAD),退出状态,进程即将被销毁。
Z (TASK_DEAD – EXIT_ZOMBIE),退出状态,进程成为僵尸进程。

3 ptrace是系统调用,什么是系统调用

Linux系统调用:使用int 0x80

代码语言:javascript
复制
系统调用的分类
系统调用大体上可分为5类:

进程控制
加载
执行
结束,中止
创建进程
结束进程
得到/设置进程属性
等待(时间、时间、信号)
内存的分配和去配
文件管理
文件的创建和删除
打开和关闭
读、写和重定位
得到/设置文件属性
设备管理
设备的请求和释放
读、写和重定位
得到/设置设备属性
设备的逻辑关联或去关联
信息维护
得到/设置时间或日期
得到/设置系统数据
得到/设置进程、文件或设备属性
通信
通信连接的创建和删除
发送、接收信息
转换状态信息
远程设备的关联或去关联

Linux系统调用:使用 int 0x80
Linux提供了200多个系统调用,通过汇编指令 int 0x80 实现,用系统调用号来区分入口函数。

Linux实现系统调用的基本过程是:

应用程序准备参数,发出调用请求;
C库封装函数引导。该函数在Linux提供的标准C库,即 glibc 中。对应的封装函数由下列汇编指令实现(以读函数调用为例):
; NASM
; read(int fd, void *buffer, size_t nbytes)
mov eax, 3          ; read系统调用号为3
mov ebx, fd
mov ecx, buffer
mov edx, nbytes
int 0x80            ; 触发系统调用

系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态, 而普通的函数调用由函数库或用户自己提供,运行于用户态。

在 i386体系结构上(本文中的所有代码都是 i386特定的) ,系统调用号码放在寄存器% eax 中。这个系统调用的参数按照这个顺序放入寄存器% ebx、% ecx、% edx、% esi 和% edi 中。例如,调用:

Linux 通过 软中断 实现从 用户态 到 内核态 的切换。用户态 与 内核态 是独立的执行流, 因此在切换时,需要准备 执行栈 并保存 寄存器 。

代码语言:javascript
复制
write(2, "Hello", 5)
roughly would translate into

大致可以理解为

movl   $4, %eax
movl   $2, %ebx
movl   $hello,%ecx
movl   $5, %edx
int    $0x80

例子

demo :使用PTRACE_ME实现父进程跟踪子进程

代码语言:javascript
复制
root@money:~/code/c++/temo# cat ptrace.c
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>

int main()
{   pid_t child;
    struct user_regs_struct regs;

    child = fork();  // 创建一个子进程
    if(child == 0) { // 子进程
        ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示当前进程进入被追踪状态
        execl("/bin/ls", "ls", NULL);          // 执行 `/bin/ls` 程序
    } 
    else { // 父进程
        wait(NULL); // 等待子进程发送一个 SIGCHLD 信号
        ptrace(PTRACE_GETREGS, child, NULL, &regs); // 获取子进程的各个寄存器的值
 
  printf("Register: rdi[%ld], rsi[%ld], rdx[%ld], rax[%ld], orig_rax[%ld]\n",
                regs.rdi, regs.rsi, regs.rdx,regs.rax, regs.orig_rax); // 打印寄存器的值
        ptrace(PTRACE_CONT, child, NULL, NULL); // 继续运行子进程
        sleep(1);
    }
    return 0;
}

root@money:~/code/c++/temo# ./ptrace 
Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
1.cpp  ptrace  ptrace.c

流程分析:

代码1 ptrace(PTRACE_TRACEME, 0, NULL, NULL); 子进程自己主动进入被追踪模式

下面我们主要介绍第一种进入被追踪模式的实现,就是 PTRACE_TRACEME 的操作过程,代码如下:

代码语言:javascript
复制
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    ...
    if (request == PTRACE_TRACEME) {
        if (current->ptrace & PT_PTRACED)
            goto out;
        current->ptrace |= PT_PTRACED; // 标志 PTRACE 状态
        ret = 0;
        goto out;
    }
    ...
}

从上面的代码可以发现,ptrace() 对 PTRACE_TRACEME 的处理就是把当前进程标志为 PTRACE 状态。

execl("/bin/ls", "ls", NULL); 当然事情不会这么简单,因为当一个进程被标记为 PTRACE 状态后,

  • 当调用 exec() 函数去执行一个外部程序时,将会暂停当前进程的运行,并且发送一个 SIGCHLD 给父进程。
  • 父进程接收到 SIGCHLD 信号后就可以对被调试的进程进行调试。
  • 我们来看看 exec() 函数是怎样实现上述功能的,exec() 函数的执行过程为 sys_execve() -> do_execve() -> load_elf_binary():
代码语言:javascript
复制
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
    ...
    if (current->ptrace & PT_PTRACED)
        send_sig(SIGTRAP, current, 0);
    ...
}

从上面代码可以看出,当进程被标记为 PTRACE 状态时,执行 exec() 函数后便会发送一个 SIGTRAP 的信号给当前进程。

我们再来看看,进程是怎么处理 SIGTRAP 信号的。信号是通过 do_signal() 函数进行处理的,而对 SIGTRAP 信号的处理逻辑如下:

代码语言:javascript
复制
int do_signal(struct pt_regs *regs, sigset_t *oldset) 
{
    for (;;) {
        unsigned long signr;

        spin_lock_irq(&current->sigmask_lock);
        signr = dequeue_signal(&current->blocked, &info);
        spin_unlock_irq(&current->sigmask_lock);

        // 如果进程被标记为 PTRACE 状态
        if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
            /* 让调试器运行  */
            current->exit_code = signr;
            current->state = TASK_STOPPED;   // 让自己进入停止运行状态
            notify_parent(current, SIGCHLD); // 发送 SIGCHLD 信号给父进程
            schedule();                      // 让出CPU的执行权限
            ...
        }
    }
}

上面的代码主要做了3件事:

如果当前进程被标记为 PTRACE 状态,那么就使自己进入停止运行状态。发送 SIGCHLD 信号给父进程。让出 CPU 的执行权限,使 CPU 执行其他进程。

调试进程

举一反三

  • GDB调试原理——ptrace系统调用
  • https://www.linuxjournal.com/article/6100
  • https://www.linuxjournal.com/article/6210
  • https://cloud.tencent.com/developer/article/1742878
  • ptrace实现代码注入
  • 原来gdb的底层调试原理这么简单 https://cloud.tencent.com/developer/article/1823077
  • linux-沙盒入门,ptrace从0到1 https://cloud.tencent.com/developer/article/1799705?from=article.detail.1742878

彩蛋:从不可能到可能

问题:看了解liunx内核 就是老虎吃天无从下嘴,

方案:看系统调用实现(fork,send),虽然很难,但是没有这么恐惧和拒绝了

内核--文件系统

  • /proc是一种伪文件系统

proc 放置的数据都是在内存当中, 例如系统内核、进程、外部设备的状态及网络状态等。因为这个目录下的数据都是在内存当中 ,所以本身不占任何硬盘空间。

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

本文分享自 Offer多多 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 前言
  • 吃土记录
  • 查缺补漏
    • 3 ptrace是系统调用,什么是系统调用
    • 例子
      • demo :使用PTRACE_ME实现父进程跟踪子进程
        • 流程分析:
          • 调试进程
          • 举一反三
          • 彩蛋:从不可能到可能
            • 问题:看了解liunx内核 就是老虎吃天无从下嘴,
              • 方案:看系统调用实现(fork,send),虽然很难,但是没有这么恐惧和拒绝了
              • 内核--文件系统
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档