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个指令

相关推荐
Dnelic-2 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen5 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年12 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿14 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神15 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛16 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法16 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter17 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快19 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl19 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5