Debugger底层原理

弄清除GDB的底层原理,首先需要了解一下ptrace这个系统调用:

ptrace 是一个用于在 Unix 和 Unix-like 操作系统上进行进程调试的系统调用。它允许一个进程(通常是调试器)监视和控制另一个进程(通常是被调试的程序)。通过 ptrace,调试器可以读取和写入被调试进程的内存、寄存器,以及拦截和处理信号。

ptrace 的基本用法可以概括为以下几步:

  1. 启动被调试进程:调试器启动被调试进程,并让它进入一个暂停状态,等待调试器的进一步操作。
  2. 附加到现有进程:调试器可以附加到一个已经在运行的进程上。
  3. 读取和写入内存和寄存器:调试器可以访问被调试进程的内存和寄存器。
  4. 控制进程执行:调试器可以单步执行被调试进程,或者继续其执行直到下一个断点或信号。
  5. 处理信号:调试器可以拦截被调试进程接收到的信号,并决定如何处理这些信号。

ptrace的方法声明如下:

arduino 复制代码
long int ptrace (enum __ptrace_request __request, ...) __THROW;

第一个参数为枚举,可以控制不同的功能,具体的操作符包括:

● PTRACE_TRACEME:使一个进程可以被其父进程跟踪。

● PTRACE_PEEKTEXT / PTRACE_PEEKDATA:读取被调试进程的内存。

● PTRACE_POKETEXT / PTRACE_POKEDATA:写入被调试进程的内存。

● PTRACE_GETREGS / PTRACE_SETREGS:读取和设置被调试进程的寄存器。

● PTRACE_CONT:继续执行被调试进程。

● PTRACE_SINGLESTEP:单步执行被调试进程。

话不多说,我们直接上手实践:

我们先写一个调试代码debugger_learn.c

arduino 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>


void run_target(char *command);
void run_debugger(pid_t child_pid);

int main(int argc, char *argv[])
{
    pid_t child_pid;
    if (argc < 2)
    {
        printf("Usage: %s <command>\n", argv[0]);
        return -1;
    }
    child_pid = fork();
    if (child_pid < 0)
    {
        perror("fork");
    }
    else if (child_pid == 0)
    {
        run_target(argv[1]);
    }
    else
    {
        run_debugger(child_pid);
    }
    return 0;
}

void run_target(char *command)
{
    printf("command = %s\n", command);
    if (ptrace(PTRACE_TRACEME) < 0)
    {
        perror("ptrace");
        return;
    }
    execl(command, NULL);
}

void run_debugger(pid_t child_pid)
{
    int wait_status;
    int count = 0;
    struct user_regs_struct regs;
    wait(&wait_status);
    while (WIFSTOPPED(wait_status))
    {
        ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
        unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.rip, 0);
        printf("icounter = %u.  IP = 0x%08x.  instr = 0x%08x\n",
               count, regs.rip, instr);
        if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0)
        {
            perror("ptrace");
            return;
        }
        count++;
        wait(&wait_status);
    }
    printf("count = %d\n", count);
}

这段代码主要是开启一个子进程,子进程先是等待调试器,连接成功或,通过execl执行传入的命令;父进程则先等待子进程暂停,并开启单步调试,这里每次执行指令都会停下来计数,最后统计指令执行的个数。

我们可以先写一个小的子命令去测试一下 single_print.c

arduino 复制代码
#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

这里我们简单打印一句Hello World,将上述两个文件分别编译生成两个对应的可执行文件 然后执行:

总共执行了9万多个指令。

意料之外,情理之中。

虽然只有一句简单的printf,但其实这个函数是库函数,用到了libc.so里的内容,加载共享库等操作其实非常复杂,有这么多指令调用也很正常。

但我们怎么验证我们的单步调试代码是正常的呢?

这里我们需要简化代码,简化到最底层的指令代码,直接上汇编代码:

single_print_asm.s

ini 复制代码
section    .text
    ; The _start symbol must be declared for the linker (ld)
    global _start

_start:

    ; Prepare arguments for the sys_write system call:
    ;   - eax: system call number (sys_write)
    ;   - ebx: file descriptor (stdout)
    ;   - ecx: pointer to string
    ;   - edx: string length
    mov    edx, len
    mov    ecx, msg
    mov    ebx, 1
    mov    eax, 4

    ; Execute the sys_write system call
    int    0x80

    ; Execute sys_exit
    mov    eax, 1
    int    0x80

section   .data
msg db    'Hello, world from asm single', 0xa
len equ    $ - msg

简单解释一下这段汇编代码,只有7个指令,使用系统调用(syscall)将一条消息输出到标准输出(stdout),然后退出。

以下是对每一部分代码的解释:

.text 段

.text段通常包含可执行代码。在这里,定义了程序的入口点和执行的指令。

● section .text:声明一个代码段。

● global _start:声明一个全局符号_start,这是程序的入口点。

_start 标签

_start是程序的入口点,在程序启动时,控制权会被转移到这里。

准备和执行sys_write系统调用

Linux系统调用sys_write用于向文件描述符写入数据。

● mov edx, len:将字符串的长度存入edx寄存器。

● mov ecx, msg:将字符串的地址存入ecx寄存器。

● mov ebx, 1:将文件描述符1(stdout,标准输出)存入ebx寄存器。

● mov eax, 4:将系统调用号4(sys_write)存入eax寄存器。

● int 0x80:触发中断0x80,执行系统调用。此时,寄存器eax、ebx、ecx和edx中的值会被传递给内核,内核根据eax中的值确定执行的系统调用(这里是sys_write),并使用ebx、ecx和edx中的值作为参数。

执行sys_exit系统调用

sys_exit系统调用用于退出程序。

● mov eax, 1:将系统调用号1(sys_exit)存入eax寄存器。

● int 0x80:触发中断0x80,执行系统调用。此时,内核会根据eax中的值确定执行sys_exit系统调用,从而终止程序。

.data 段

.data段通常包含已初始化的数据。

● section .data:声明一个数据段。

● msg db 'Hello, world from asm single', 0xa:定义一个字符串,内容为"Hello, world from asm single",并以换行符(0x0a)结尾。db是"定义字节"(define byte)的缩写,用于定义一个字节数组。

len equ $ - msg:计算字符串的长度。$表示当前位置的地址,$ - msg表示从当前位置到字符串起始位置的字节数,即字符串的长度。equ是"等同于"(equate)的缩写,用于定义常量。

将single_print_asm.s编译成可执行文件:

arduino 复制代码
nasm -f elf64 single_print_asm.s -o single_print_asm.o

gcc -nostartfiles -nostdlib -static single_print_asm.o -o single_print_asm

最后成功验证单步调试成功,总共执行了7个指令

相关推荐
命运之手2 小时前
【Android】自定义换肤框架01之皮肤包制作
android·skin·skinner·换肤框架·不重启换肤
练习本2 小时前
android perfetto使用技巧梳理
android
GitLqr3 小时前
Android - 云游戏本地悬浮输入框实现
android·开源·jitpack
周周的Unity小屋3 小时前
Unity实现安卓App预览图片、Pdf文件和视频的一种解决方案
android·unity·pdf·游戏引擎·webview·3dwebview
单丽尔5 小时前
Gemini for China 大更新,现已上架 Android APP!
android
JerryHe6 小时前
Android Camera API发展历程
android·数码相机·camera·camera api
Synaric7 小时前
Android与Java后端联调RSA加密的注意事项
android·java·开发语言
程序员老刘·8 小时前
如何评价Flutter?
android·flutter·ios
JoyceMill10 小时前
Android 图像效果的奥秘
android
想要打 Acm 的小周同学呀11 小时前
ThreadLocal学习
android·java·学习