Debugger底层原理

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

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

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

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

ptrace的方法声明如下:

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

#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

#include <stdio.h>

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

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


image.png

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

意料之外,情理之中。

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

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

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

single_print_asm.s

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编译成可执行文件:

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


image.png

除了通过指令个数验证,我们其实还有一个更准确的方案:

通过ptrace打印IP寄存器的指令,可以查看程序的执行过程。

       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);

在计算机体系结构中,IP 和 PC 是两个重要的寄存器,它们在不同的体系结构中有不同的名称和角色,但它们的基本功能是相似的:都用于指向当前正在执行的指令地址。

指令指针 (Instruction Pointer, IP)

  • IP 寄存器是 x86 架构中的一个寄存器,它包含当前或下一条将被执行的指令的地址。
    在 x86 架构中,IP 是一个 16 位寄存器,EIP 是一个 32 位寄存器,RIP 是一个 64 位寄存器。它们分别用于16位、32位和64位模式下的指令地址存储。

程序计数器 (Program Counter, PC)

  • PC 寄存器是大多数其他体系结构(如 ARM、MIPS、PowerPC、RISC-V 等)中的一个寄存器,功能与 x86 架构中的 IP 寄存器相似。
    PC 寄存器包含当前或下一条将被执行的指令的地址。
    在 ARM 架构中,PC 是一个 32 位寄存器(在 64 位模式下,它是 64 位的),通常称为 R15。

打印结果如下:


image.png

我们可以再反汇编single_print_asm可执行文件,查看重定位之后的指令地址:

objdump -d single_print_asm
image.png

可以看到,CPU实际执行过程和可执行文件里的过程是一致的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容