Linux x86_64 C语言实现gdb断点机制

文章目录

  • 前言
  • 一、trap指令简介
  • 二、调用ptrace
  • 三、创建breakpoints
  • 四、CONT 和 SINGLESTEP
  • 五、完整代码演示
  • 六、增加参数检测
  • 参考资料

前言

本文参考文章:Implementing breakpoints on x86 Linux

一、trap指令简介

将通过在断点地址向目标进程的内存中插入一条新的CPU指令来实现断点。此指令应暂停目标进程的执行,并将控制权交还给操作系统,或者说将目标进程的控制权转移给其他进程,通过是调试进程。

有很多方法可以将控制权返回到操作系统,但希望最大限度地减少对正在进行热修补的代码的干扰。x86提供的int3指令,编码为单字节0xCC:在这里插入图片描述

当CPU执行int3时,它将停止它正在做的事情,并跳到内核函数do_int3函数服务例程,这是操作系统内核中的一段代码。在Linux上,此例程将向当前进程(即目标进程)发送信号SIGTRAP。

备注:除了int3将向当前进程发送信号SIGTRAP信号外,调试器给目标进程发送PTRACE_SYSCALL和PTRACE_SINGLESTEP这个两个ptrace请求时,目标进程看起来也可以看作接收到了一个SIGTRAP信号而停止执行。调试器可以在目标进程停止时进行进一步的检查或操作。
因此调试器可以在三种情况下检查目标进程:

断点  -- int3 
单步执行指令 -- PTRACE_SINGLESTEP
系统调用 -- PTRACE_SYSCALL

前两者用于调试器,比如gdb,后者用于strace。

由于我们将int3放入目标的代码中,因此目标将收到一个SIGTRAP。在正常情况下,这将调用目标的SIGTRAP处理程序,该处理程序通常会杀死进程。相反,我们希望跟踪过程拦截该信号,并将其解释为目标击中断点。我们将通过ptrace系统调用来实现这一点。

关于int3指令可以参考:GDB 源码分析 – 断点源码解析

定义trap指令:

#include 

#define REGISTER_IP RIP
#define TRAP_LEN    1
#define TRAP_INST   0xCC
#define TRAP_MASK   0xFFFFFFFFFFFFFF00

常量RIP定义在sys/reg.h文件中,用于标识保存指令指针的机器寄存器。

#ifdef __x86_64__
/* Index into an array of 8 byte longs returned from ptrace for
   location of the users' stored general purpose registers.  */
......
# define RIP	16
......

trap instruction存储为整数TRAP_INST,其字节长度为TRAP_LEN。这些在32位和64位x86上是相同的。陷阱指令是一个单字节,但我们将以一个机器字为增量读取和写入目标的内存,即32或64位。因此,我们将读取4或8个字节的机器代码,用TRAP_MASK清除第一个字节,并替换0xCC。由于x86是一个小端序体系结构,内存中的第一个字节是整数机器字的最低有效字节。

二、调用ptrace

所有各种ptrace请求都是通过一个名为ptrace的系统调用发出的。第一个参数指定请求的类型,第二个参数几乎总是目标的进程ID。

NAME
       ptrace - process trace

SYNOPSIS
       #include 

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

ptrace是Linux操作系统提供的一个系统调用,用于实现进程间的跟踪和调试功能。通过ptrace系统调用,一个进程(称为追踪器)可以监视和控制另一个进程(称为被追踪进程)的执行。
以下是ptrace系统调用的一些常见用法和功能:

(1)进程跟踪:追踪器可以使用ptrace系统调用启动对一个进程的追踪。追踪器可以监视被追踪进程的系统调用、信号传递、执行状态等,并在需要时对其进行控制。

(2)单步执行:通过使用ptrace系统调用的PTRACE_SINGLESTEP选项,追踪器可以实现单步执行功能,逐条执行被追踪进程的指令并进行调试和分析。

(3)寄存器访问:追踪器可以使用ptrace系统调用的PTRACE_GETREGS和PTRACE_SETREGS选项来读取和修改被追踪进程的寄存器状态,以实现寄存器级别的调试和修改。

(4)内存访问:通过ptrace系统调用的PTRACE_PEEKDATA和PTRACE_POKEDATA选项,追踪器可以读取和写入被追踪进程的内存数据,以进行内存级别的调试和修改。

(5)信号控制:追踪器可以使用ptrace系统调用的PTRACE_GETSIGINFO和PTRACE_SETSIGINFO选项来获取和修改被追踪进程收到的信号信息,以实现对信号的控制和处理。

(6)进程控制:通过ptrace系统调用的PTRACE_ATTACH和PTRACE_DETACH选项,追踪器可以附加到一个正在运行的进程并开始追踪,或者从被追踪进程中分离出来。

在我们可以调式目标进程之前,我们需要附加到它:

void breakfast_attach(pid_t pid) {
  int status;

  ptrace(PTRACE_ATTACH, pid);

  //调用waitpid等待子进程停止的通知
  waitpid(pid, &status, 0);

  //使用ptrace系统调用和PTRACE_SETOPTIONS选项来设置追踪器的选项,可以获取子进程的退出码和信号信息。
  //PTRACE_SETOPTIONS用于设置追踪器的选项
  //pid是要追踪的子进程的进程ID
  //PTRACE_O_TRACEEXIT是用于追踪子进程退出的特殊选项。
  ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACEEXIT);

}

ptrace(PTRACE_ATTACH, pid) 来使指定进程号为pid的进程进入被追踪模式,这是一种使进程号为pid的进程被动进入被追踪模式。

PTRACE_ATTACH请求将使用SIGSTOP停止目标进程。我们等待目标进程接收到这个信号。

PTRACE_ATTACH
        Attach  to  the process specified in pid, making it a tracee of the calling process.  The tracee is sent a SIGSTOP, but will not necessarily have stopped by the com‐
        pletion of this call; use waitpid(2) to wait for the tracee to stop.

附加到在PID中指定的进程,使其成为调用进程的跟踪对象(tracee)。调用进程 tracer 给 tracee发送一个SIGSTOP信号,但不一定会在此调用完成时停止;使用waitid等待tracee停止。

请注意,ptrace和waitpid可能会以各种方式失败。在实际应用程序中,需要检查返回值和/或errno。为了简洁起见,在本文中省略了这些检查。

使用另一个ptrace请求来获取目标的指令指针的值:

target_addr_t breakfast_getip(pid_t pid) {
  long v = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*REGISTER_IP);
  //恢复执行时,将返回并执行用陷阱覆盖的原始指令。减去TRAP_LEN,得到下一条指令的真实地址
  return (target_addr_t) (v - TRAP_LEN);
}
PTRACE_PEEKUSER
       Read a word at offset addr in the tracee's USER area, which holds the registers and other information about the process (see <sys/user.h>).  The word is returned  as
       the result of the ptrace() call.

读取Tracee用户区中偏移量addr处的一个word ,该用户区保存寄存器和有关该过程的其他信息(参见)。该word 将作为ptrace()调用的结果返回。

由于目标进程已挂起,因此它不会在任何CPU上运行,并且它的指令指针也不会存储在实际的CPU寄存器中。相反,它被保存到内核内存中的“用户区域”。我们使用PTRACE_PEEKUSER请求以指定的字节偏移量从该区域读取机器字。sys/regs.h中的常量给出了寄存器的出现顺序,因此我们只需乘以sizeof(long)。
x86_64平台下一个寄存器八个字节。

#ifdef __x86_64__
/* Index into an array of 8 byte longs returned from ptrace for
   location of the users' stored general purpose registers.  */

# define R15	0
# define R14	1
# define R13	2
# define R12	3
# define RBP	4
# define RBX	5
# define R11	6
# define R10	7
# define R9	8
# define R8	9
# define RAX	10
# define RCX	11
# define RDX	12
# define RSI	13
# define RDI	14
# define ORIG_RAX 15
# define RIP	16
# define CS	17
# define EFLAGS	18
# define RSP	19
# define SS	20
# define FS_BASE 21
# define GS_BASE 22
# define DS	23
# define ES	24
# define FS	25
# define GS	26

在我们遇到断点后,保存的IP指向陷阱指令之后的指令。当我们恢复执行时,我们将返回并执行用陷阱覆盖的原始指令。所以我们减去TRAP_LEN,得到下一条指令的真实地址。

三、创建breakpoints

关于断点,我们需要记住两件事:我们替换的代码的地址和最初存在于那里的原始代码。

struct breakpoint {
  target_addr_t addr;   //替换的代码的地址
  long orig_code;		//原始代码指令
};

要启用断点,我们保存原始代码并插入陷阱指令:

static void enable(pid_t pid, struct breakpoint *bp) {
  //read bp->addr -->获取原始指令
  long orig = ptrace(PTRACE_PEEKTEXT, pid, bp->addr);
  //write 0xCC into bp->addr -->插入陷阱指令:0xCC
  ptrace(PTRACE_POKETEXT, pid, bp->addr, (orig & TRAP_MASK) | TRAP_INST);
  //保存原始指令
  bp->orig_code = orig;
}

PTRACE_PEEKTEXT请求从目标的代码地址空间读取一个机器字,由于历史原因,该地址空间被命名为“text”。PTRACE_POKETEXT写入该空间。在x86 Linux上,代码空间和数据空间实际上没有区别,因此PTRACE_PEEKDATA和PTRACE_POKEDATA也可以正常工作。

PTRACE_PEEKTEXT, PTRACE_PEEKDATA
       Read  a  word  at the address addr in the tracee's memory, returning the word as the result of the ptrace() call.  Linux does not have separate text and data address
       spaces, so these two requests are currently equivalent. 
PTRACE_POKETEXT, PTRACE_POKEDATA
       Copy the word data to the address addr in the tracee's memory.  As for PTRACE_PEEKTEXT and PTRACE_PEEKDATA, these two requests are currently equivalent.

创建断点非常简单:

struct breakpoint *breakfast_break(pid_t pid, target_addr_t addr) {
  struct breakpoint *bp = malloc(sizeof(*bp));
  bp->addr = addr;
  //启用断点 --> 插入陷阱指令:0xCC
  enable(pid, bp);
  return bp;
}

要禁用断点,我们只需写回保存的word(原始指令):

//写回保存的原始指令
static void disable(pid_t pid, struct breakpoint *bp) {
  ptrace(PTRACE_POKETEXT, pid, bp->addr, bp->orig_code);
}

四、CONT 和 SINGLESTEP

一旦我们连接到目标,它的执行就会停止。以下是如何恢复它:

static int run(pid_t pid, int cmd) {
  int status, last_sig = 0, event;
  while (1) {
    ptrace(cmd, pid, 0, last_sig);
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
      return 0;

    if (WIFSTOPPED(status)) {
      last_sig = WSTOPSIG(status);
      if (last_sig == SIGTRAP) {
        event = (status >> 16) & 0xffff;
        return (event == PTRACE_EVENT_EXIT) ? 0 : 1;
      }
    }
  }
}

int breakfast_run(pid_t pid, struct breakpoint *bp) {
  if (bp) {
    ptrace(PTRACE_POKEUSER, pid, sizeof(long)*REGISTER_IP, bp->addr);

	//恢复原始指令
    disable(pid, bp);
    //单步执行原始指令
    //父进程通过PTRACE_SINGLESTEP以及子进程的id号来调用ptrace。
    //这么做是告诉操作系统——请重新启动子进程,但当子进程执行了下一条指令后再将其停止。
    if (!run(pid, PTRACE_SINGLESTEP))
      return 0;
    //重新启用断点
    enable(pid, bp);
  }
  return run(pid, PTRACE_CONT);
}

我们要求ptrace继续执行——但如果我们从断点恢复,我们必须首先进行一些清理。我们回退指令指针,以便下一条要执行的指令在断点处。然后我们禁用断点,使目标只执行一条指令,单步执行断点处的原始指令。一旦我们通过了断点,我们就可以在下次重新启用它。如果目标退出,run将返回0,理论上这可能发生在我们的单个步骤中。

断点处理过程:

命中断点-->触发int3异常-->调试器观测目标进程-->调试完毕后,恢复原始指令(回退指令指针,回退一个字节)-->单步执行原始指令-->重新下断点0xcc-->目标进程继续运行

对于gdb调试器:
当断点命中中断到调试器时,调试器会把所在断点处的 int 3指令恢复成原始指令。因此,在用户发出了恢复执行命令后,此时断点处的指令已经是正常的原始指令了,因此要做一些处理,以至于下次还能继续命中该断点。调试器在通知系统真正恢复程序执行前,调试器需要将断点列表中的该断点位置重新启用该断点。但是对于刚才命中的这个断点需要特别对待,试想如果把这个断点处的指令也替换为int 3指令,那么程序一执行便又触发断点了。但是如果不替换,那么这个断点便没有被启动,程序下次执行到这里时就不会触发断点,而用户并不知道这一点。对于这个问题,大多数调试器的做法都是先单步执行一次,单步执行一条指令。也就是说,先设置单步执行标志,然后恢复执行,将断点所在位置的指令执行完。因为设置了单步标志,所以,CPU执行完断点位置的这条指令后会立刻再中断到调试器中,这一次调试器不会通知用户,会做一些内部操作后便立刻恢复程序执行,而且将该断点启动。

PTRACE_CONT
       Restart the stopped tracee process.  If data is nonzero, it is interpreted as the number of a signal to be delivered to the tracee; otherwise, no  signal  is  deliv‐
       ered.  Thus, for example, the tracer can control whether a signal sent to the tracee is delivered or not.

PTRACE_CONT是一个用于重新启动被停止的被追踪进程的ptrace系统调用选项。当tracer调用PTRACE_CONT时,被追踪的进程tracee将继续执行。
PTRACE_CONT的行为如下:

如果提供的data参数为非零值,则被解释为要发送给被追踪进程的信号编号。这意味着可以控制是否向被追踪进程发送信号。
如果data参数为零,则不向被追踪进程发送任何信号。
当调用PTRACE_CONT时,被追踪进程将从之前被停止的位置继续执行,并且可能会在之后再次被停止,具体取决于陷阱事件和追踪器的设置。
追踪器可以通过调用ptrace系统调用并使用PTRACE_CONT选项来控制被追踪进程的执行流程,包括在适当的时机发送信号以及决定是否重新启动进程。

PTRACE_CONT是一种ptrace系统调用选项,用于重新启动被停止的被追踪进程,并可选择发送信号给被追踪进程。通过使用PTRACE_CONT,追踪器可以对被追踪进程的执行进行控制和管理。

对于run函数:

static int run(pid_t pid, int cmd) {
  int status, last_sig = 0, event;
  while (1) {

    ptrace(cmd, pid, 0, last_sig);

    //父进程会调用waitpid来等待子进程的退出/停止,以便获取子进程的退出/停止状态,并进行相应的处理。
    
    //父进程通过waitpid正在等待子进程退出/停止这个事件发生。
    //当被调试进程被内核挂起时-- 停止,内核会向其父进程发送一个 SIGCHLD 信号,父进程可以通过调用 waitpid() 系统调用来捕获这个信息
    waitpid(pid, &status, 0);

    //当子进程退出/停止时,父进程通过 waitpid 来获取子进程的退出状态:status 

    //退出 -- 使用WIFEXITED宏来判断子进程是否正常退出
    //在正常运行这个跟踪程序时,会得到子进程正常退出(WIFEXITED会返回true)的信号。
    if (WIFEXITED(status))
      return 0;

    //增加一次额外的检查
    //停止 -- WIFSTOPPED宏定用于在处理子进程状态时判断子进程是否处于停止状态。
    //一旦子进程停止(如果子进程由于发送的信号而停止运行,WIFSTOPPED就返回true), 父进程就去检查这个事件
    if (WIFSTOPPED(status)) {

      //通过相关宏 WSTOPSIG 检查子进程停止运行的信号
      //WSTOPSIG宏定义用于从子进程的状态值中提取导致子进程停止的信号编号
      last_sig = WSTOPSIG(status);
      //在SIGTRAP的情况下,我们检查状态的位16-31的值PTRACE_EVENT_EXIT,它指示目标即将退出
      if (last_sig == SIGTRAP) {
        event = (status >> 16) & 0xffff;
        //如果状态的位16-31的值PTRACE_EVENT_EXIT,表明目标进程即将退出,也要返回0
        return (event == PTRACE_EVENT_EXIT) ? 0 : 1;
      }
    }
  }
}

cmd是PTRACE_CONT或PTRACE_SINGLESTEP。对于PTRACE_SINGLESTEP,OS将设置一个控制位,以使CPU在一条指令完成后引发int3,即单步调试功能。

PTRACE_SYSCALL, PTRACE_SINGLESTEP
       Restart the stopped tracee as for PTRACE_CONT, but arrange for the tracee to be stopped at the next entry to or exit from a system call, or after execution of a sin‐
       gle instruction, respectively.  (The tracee will also, as usual, be stopped upon receipt of a signal.)  From the tracer's perspective, the tracee will appear to have
       been  stopped  by  receipt  of  a  SIGTRAP.   So, for PTRACE_SYSCALL, for example, the idea is to inspect the arguments to the system call at the first stop, then do
       another PTRACE_SYSCALL and inspect the return value of the system call at the second stop.  The data argument is treated as for PTRACE_CONT.

用于单步执行被追踪进程的指令。当使用PTRACE_SINGLESTEP选项调用ptrace时,被追踪进程会在执行一条指令后停止。
以下是关于PTRACE_SINGLESTEP的一些要点:

当被追踪进程收到PTRACE_SINGLESTEP指令后,它会执行一条指令,并在执行完毕后立即停止。这样,追踪器就有机会检查指令的执行结果、寄存器状态或其他相关信息。
追踪器可以利用这个停止点来实现单步调试的功能,例如在每个步骤中检查变量的值、跟踪指令执行路径或进行其他调试操作。

在SIGTRAP的情况下,我们检查停止状态位16-31的值PTRACE_EVENT_EXIT,它指示目标进程即将退出。回想一下,我们通过设置选项PTRACE_O_TRACEEXIT请求了此通知。你可能会认为(至少,我是这么认为的)检查WIFEXITED就足够了。但我遇到了一个问题,向目标发送致命信号会使跟踪过程永远循环。我通过增加一次额外的检查来解决这个问题。
如果启用了 PTRACE_O_TRACEEXIT 选项,会在实际终止之前发生 PTRACE_EVENT_EXIT 事件。

五、完整代码演示

// breakfast.h

#ifndef _BREAKFAST_H
#define _BREAKFAST_H

#include   /* for pid_t */

typedef void *target_addr_t;
struct breakpoint;

void breakfast_attach(pid_t pid);
target_addr_t breakfast_getip(pid_t pid);
struct breakpoint *breakfast_break(pid_t pid, target_addr_t addr);
int breakfast_run(pid_t pid, struct breakpoint *bp);

#endif

// breakfast.c

#include 
#include 
#include 
#include 

#include "breakfast.h"

#define REGISTER_IP RIP
#define TRAP_LEN    1
#define TRAP_INST   0xCC
#define TRAP_MASK   0xFFFFFFFFFFFFFF00


void breakfast_attach(pid_t pid) {
  int status;
  ptrace(PTRACE_ATTACH, pid);
  waitpid(pid, &status, 0);
  ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACEEXIT);
}

target_addr_t breakfast_getip(pid_t pid) {
  long v = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*REGISTER_IP);
  return (target_addr_t) (v - TRAP_LEN);
}

struct breakpoint {
  target_addr_t addr;
  long orig_code;
};

static void enable(pid_t pid, struct breakpoint *bp) {
  long orig = ptrace(PTRACE_PEEKTEXT, pid, bp->addr);
  ptrace(PTRACE_POKETEXT, pid, bp->addr, (orig & TRAP_MASK) | TRAP_INST);
  bp->orig_code = orig;
}

static void disable(pid_t pid, struct breakpoint *bp) {
  ptrace(PTRACE_POKETEXT, pid, bp->addr, bp->orig_code);
}

struct breakpoint *breakfast_break(pid_t pid, target_addr_t addr) {
  struct breakpoint *bp = malloc(sizeof(*bp));
  bp->addr = addr;
  enable(pid, bp);
  return bp;
}

static int run(pid_t pid, int cmd) {
  int status, last_sig = 0, event;
  while (1) {
    ptrace(cmd, pid, 0, last_sig);
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
      return 0;

    if (WIFSTOPPED(status)) {
      last_sig = WSTOPSIG(status);
      if (last_sig == SIGTRAP) {
        event = (status >> 16) & 0xffff;
        return (event == PTRACE_EVENT_EXIT) ? 0 : 1;
      }
    }
  }
}

int breakfast_run(pid_t pid, struct breakpoint *bp) {
  if (bp) {
    ptrace(PTRACE_POKEUSER, pid, sizeof(long)*REGISTER_IP, bp->addr);

    disable(pid, bp);
    if (!run(pid, PTRACE_SINGLESTEP))
      return 0;
    enable(pid, bp);
  }
  return run(pid, PTRACE_CONT);
}
// test.c

#include 
#include 
#include 
#include 

#include 
#include 
#include 

#include "breakfast.h"

int fact(int n) {
  if (n <= 1)
    return 1;
  return n * fact(n-1);
}

void child() {
  //getpid()获取子进程的pid,给其发送 SIGSTOP 信号
  kill(getpid(), SIGSTOP);
  printf("fact(5) = %d\n", fact(5));
}

void parent(pid_t pid) {

  struct breakpoint *fact_break, *last_break = NULL;
  void *fact_ip = fact, *last_ip;

  //该pid是子进程的pid
  breakfast_attach(pid);

  fact_break = breakfast_break(pid, fact_ip);

  while (breakfast_run(pid, last_break)) {
    last_ip = breakfast_getip(pid);
    if (last_ip == fact_ip) {

      int arg = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*RDI);

      printf("Break at fact(%d)\n", arg);
      last_break = fact_break;

    } else {
      printf("Unknown trap at %p\n", last_ip);
      last_break = NULL;
    }
  }
}


int main() 
{
  pid_t pid = fork();

  if(pid == 0)
    //子进程
    child();
  else if(pid > 0)
    //父进程,pid是子进程pid
    parent(pid);
  else{
    printf("fork error\n");
    return -1;
  }
  return 0;
}

调用fork函数,返回一个父进程和一个子进程。子进程会做一些希望观察到的计算。这里计算一下著名的阶乘函数:

int fact(int n) {
  if (n <= 1)
    return 1;
  return n * fact(n-1);
}

void child() {
  //getpid()获取子进程的pid,给其发送 SIGSTOP 信号
  kill(getpid(), SIGSTOP);
  printf("fact(5) = %d\n", fact(5));
}

父进程将调用PTRACE_ATTACH 并发送子进程SIGSTOP,但子进程可能会在父进程有机会调用ptrace之前完成执行。所以让孩子自己停止自己。在附加到长时间运行的进程时,这不是问题。

实际上,对于fork-跟踪子模式,应该使用PTRACE_TRACEME。
PTRACE_TRACEME – 被调试的进程调用 ptrace(PTRACE_TRACEME, …) 来使自己进入被追踪模式,是进程自己主动进入被追踪模式。gdb调试程序时便是采用此种模式。
我们这里只是做一个小的实验,选择用了PTRACE_ATTACH模式。

父进程用breakfast_break给子进程设置断点:

void parent(pid_t pid) {
  struct breakpoint *fact_break, *last_break = NULL;
  void *fact_ip = fact, *last_ip;
  breakfast_attach(pid);
  fact_break = breakfast_break(pid, fact_ip);
  while (breakfast_run(pid, last_break)) {
    last_ip = breakfast_getip(pid);
    if (last_ip == fact_ip) {
      printf("Break at fact()\n");
      last_break = fact_break;
    } else {
      printf("Unknown trap at %p\n", last_ip);
      last_break = NULL;
    }
  }
}

原则上,我们可以使用breakfast 来跟踪我们拥有的任何正在运行的进程,即使我们没有它的源代码。但我们仍然需要一种方法来找到有趣的断点地址。在这里,这是我们想要的最简单的方法:fork()通过一个进程(父进程)创建一个新进程(子进程),子进程是父进程的副本,因此子进程和父进程共享代码段,所以父子进程 fact function 地 址的一样。

# ./test
Break at fact()
Break at fact()
Break at fact()
Break at fact()
Break at fact()
fact(5) = 120

六、增加参数检测

计数函数调用的功能对于性能评测已经很有用了。但我们通常希望从停止的目标中获得更多信息。让我们看看我们是否能读懂传递给fact函数的参数。这部分将专门针对64位x86,尽管这个想法是通用的。

每个体系结构都定义了一个C调用约定,该约定指定了函数参数的传递方式,通常使用寄存器和堆栈槽的组合。在64位x86上,第一个参数在RDI寄存器中传递。可以通过运行objdump-d测试并查看反汇编的代码来验证这一点:
Linux x86_64 C语言实现gdb断点机制_第1张图片
由于fact函数的参数类型时int,因此用RDI寄存器的低32位即可,即EDI寄存器。

因此,我们将修改test.c以读取此寄存器:

void parent(pid_t pid) {

  struct breakpoint *fact_break, *last_break = NULL;
  void *fact_ip = fact, *last_ip;

  //该pid是子进程的pid
  breakfast_attach(pid);

  //设置断点
  fact_break = breakfast_break(pid, fact_ip);

  //breakfast_run函数中当执行断点原始指令后会重新启用断点
  //断点的流程:int3 --> 恢复原始指令 --> 单步执行原始指令 -->重新启用断点
  while (breakfast_run(pid, last_break)) {
    
    //子进程此时是stopped状态,指令指针寄存器的值保存到内核内存中的“用户区域”
    //来获取子进程指令指针的值
    last_ip = breakfast_getip(pid);
    if (last_ip == fact_ip) {

      //读取寄存器RDI的值 -- 函数调用时RDI用来传递第一个参数
      int arg = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*RDI);

      printf("Break at fact(%d)\n", arg);
      last_break = fact_break;

    } else {
      printf("Unknown trap at %p\n", last_ip);
      last_break = NULL;
    }
  }
}
# ./test
Break at fact(5)
Break at fact(4)
Break at fact(3)
Break at fact(2)
Break at fact(1)
fact(5) = 120

x86体系结构具有用于设置断点的专用寄存器,但受到各种限制。我们忽略了这个特性,而选择了更灵活的软件断点。硬件断点可以做一些这种技术做不到的事情,比如中断从特定内存地址读取的操作。

参考资料

Implementing breakpoints on x86 Linux
https://blog.csdn.net/dog250/article/details/106267041
https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints

你可能感兴趣的:(Linux,调试及其原理,linux,c语言)