Contents

GDB 速查表及原理简介

记录一些使用 GDB 时的常用操作:)

Starting

gdb
gdb <file>

Running and stopping

quit 					Exit gdb
run 					Run program
run 1 2 3 				Run program with command-line arguments 1 2 3
kill 					Stop the program
quit 					Exit gdb
Ctrl-d 					Exit gdb

Note: Ctrl-C does not exit from gdb, but halts the current gdb command

Breakpoints

break sum 				Set breakpoint at the entry to function sum
break *0x80483c3 		Set breakpoint at address 0x80483c3
delete 1 				Delete breakpoint 1
disable 1 				Disable the breakpoint 1
							(gdb numbers each breakpoint you create)
enable 1 				Enable breakpoint 1
delete 					Delete all breakpoints
clear sum 				Clear any breakpoints at the entry to function sum

Execution

stepi 					Execute one instruction
stepi 4 				Execute four instructions
nexti 					Like stepi, but proceed through function calls without stopping
step 					Execute one C statement
continue 				Resume execution until the next breakpoint
until 3 				Continue executing until program hits breakpoint 3
finish 					Resume execution until current function returns
call sum(1, 2) 			Call sum(1,2) and print return value

Examining code

disas 					Disassemble current function
disas sum 				Disassemble function sum
disas 0x80483b7 		Disassemble function around 0x80483b7
disas 0x80483b7 0x80483c7 	Disassemble code within specified address range
print /x $rip 			Print program counter in hex
print /d $rip 			Print program counter in decimal
print /t $rip 			Print program counter in binary

Examining data

print /d $rax 				Print contents of %rax in decimal
print /x $rax 				Print contents of %rax in hex
print /t $rax 				Print contents of %rax in binary
print /d (int)$rax 			Print contents of %rax in decimal after sign-extending lower 32-bits. You need this to print 32-bit, negative numbers stored in the lower 32 bits of %rax. For example, if the lower 32-bits of %rax store 0xffffffff, you will see 
						 (gdb) print $rax
						 $1 = 4294967295
						 (gdb) print (int)$rax
						 $2 = -1

print /d 0x100 				Print decimal representation of 0x100
print /x 555 				Print hex representation of 555
print /x ($rsp+8) 			Print (contents of %rsp) + 8 in hex
print *(int *) 0xbffff890 	Print integer at address 0xbffff890
print *(int *) ($rsp+8) 	Print integer at address %rsp + 8
print (char *) 0xbfff890 	Examine a string stored at 0xbffff890

x/w 0xbffff890 				Examine (4-byte) word starting at address 0xbffff890
x/w $rsp 					Examine (4-byte) word starting at address in $rsp
x/wd $rsp 					Examine (4-byte) word starting at address in $rsp. Print in decimal
x/2w $rsp 					Examine two (4-byte) words starting at address in $rsp
x/2wd $rsp 					Examine two (4-byte) words starting at address in $rsp. Print in decimal
x/g $rsp 					Examine (8-byte) word starting at address in $rsp.
x/gd $rsp 					Examine (8-byte) word starting at address in $rsp. Print in decimal
    						gef➤  x/g 0x6032d0
    						0x6032d0 <node1>:       4294967628
    						gef➤  x/2w 0x6032d0
							0x6032d0 <node1>:       332     1

x/a $rsp 					Examine address in $rsp. Print as offset from previous global symbol.
x/s 0xbffff890 				Examine a string stored at 0xbffff890
x/20b sum 					Examine first 20 opcode bytes of function sum
x/10i sum 					Examine first 10 instructions of function sum
 
Note: the format string for the ‘x’ command has the general form: 
x/[NUM][SIZE][FORMAT] where
 - NUM = number of objects to display
 - SIZE = size of each object (b=byte, h=half-word, w=word, g=giant (quad-word))
 - FORMAT = how to display each object (d=decimal, x=hex, o=octal, etc.)
If you don’t specify SIZE or FORMAT, either a default value, or the last value you specified in a previous ‘print’ or ‘x’ command is used. 

Useful information

backtrace 					Print the current address and stack backtrace
where 						Print the current address and stack backtrace
info program 				Print current status of the program)
info functions 				Print functions in program
info stack 					Print backtrace of the stack)
info frame 					Print information about the current stack frame
info registers 				Print registers and their contents
info breakpoints 			Print status of user-settable breakpoints
display /FMT EXPR 			Print expression EXPR using format FMT every time GDB stops
undisplay 					Turn off display mode
help 						Get information about gdb

GDB 实现原理简介

GNU调试器(英语:GNU Debugger,缩写:GDB),是GNU软件系统中的标准调试器,此外GDB也是个具有移携性的调试器,经过移携需求的调修与重新编译,如今许多的类UNIX操作系统上都可以使用GDB,而现有GDB所能支持调试的编程语言有C、C++、Pascal以及FORTRAN。

GDB 使用一个名为 ptrace(名字是 “process trace” 的缩写)的系统调用,来观察和控制另一个进程的执行,以及检查和更改进程的内存和寄存器。

/images/GDB.asserts/gdb.png
gdb <-> ptrace

断点是通过将给定内存地址处的一个指令替换为另一个特殊指令来实现的。执行断点指令会导致 SIGTRAP。

ptrace

从名字可以看出,ptrace 是一个用于进程跟踪的系统调用。当一个进程调用 ptrace 来跟踪另一个进程时:

  • 调用 ptrace 的进程会变成被跟踪进程的父进程。在这种上下文中,我们通常称调用 ptrace 的进程为 “追踪器”,被追踪的进程为 “被追踪者”。
  • 被跟踪进程的状态将被标记为 TASK_TRACED。这是一个特殊的状态,表示该进程正在被另一个进程跟踪。
  • 发送给被跟踪进程的信号(除 SIGKILL 信号外)将被转发给父进程,而被跟踪的子进程会被阻塞。这意味着,当子进程收到一个信号时,它不会立即对该信号进行响应。相反,这个信号将被发送给追踪器,然后由追踪器决定如何处理这个信号。
  • 在收到信号后,追踪器可以对子进程进行检查和修改,然后让子进程继续执行。这为调试提供了可能,因为追踪器可以在子进程继续执行之前改变其状态,或者在子进程接收到信号后查看其状态。

简而言之,ptrace 系统调用提供了一种机制,使一个进程能够控制另一个进程的执行,查看和更改它的内存和寄存器。这是很多调试器,包括 GDB,在进行调试时所使用的主要工具。

PS: 信号

在操作系统中,信号是一种软件中断,用于向进程传达某些类型的信息。信号可以由用户(使用键盘等输入设备)、操作系统(如内存错误、定时器过期等)或其他进程(使用kill命令或调用kill()函数等)产生。

例如,当你在命令行界面按下 Ctrl+C,就会向前台进程发送一个 SIGINT(中断信号)。当进程接收到这个信号时,默认行为是结束进程。然而,进程也可以选择自定义这个信号的处理方式,比如忽略这个信号,或者执行特定的处理函数。

当我们谈到 “发送给被跟踪进程的信号(除 SIGKILL 信号外)将被转发给父进程”,这意味着如果其他进程试图通过发送信号来影响被跟踪的进程,这个信号会首先被追踪器(即父进程)接收到。然后,追踪器可以决定是否将这个信号传递给被追踪的进程,或者以何种方式对这个信号进行处理。

此外,SIGKILL是一个特殊的信号,它的默认行为是立即结束进程,并且不能被忽略或捕获。这就是为什么在上述描述中特别指出 “除 SIGKILL 信号外” 的原因。即使进程正在被跟踪,SIGKILL信号依然可以直接结束该进程。

man ptrace 中可以找到 ptrace 的定义原型:

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

参数的意义如下:

  1. request:指定了我们要使用 ptrace 的什么功能, 大致可以分为以下几类:

    • PTRACE_ATTACHPTRACE_TRACEME 建立进程间的跟踪关系;
      • PTRACE_TRACEME 是被跟踪子进程调用的, 表示让父进程来跟踪自己, 通常是通过 GDB 启动新进程的时候使用;
      • PTRACE_ATTACH 是父进程调用 attach 到已经运行的子进程中; 这个命令会有权限的检查, non-root 的进程不能 attach 到 root 进程中;
    • PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSR 等读取子进程内存/寄存器中保留的值;
    • PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSR 等修改被跟踪进程的内存/寄存器;
    • PTRACE_CONTPTRACE_SYSCALL, PTRACE_SINGLESTEP 控制被跟踪进程以何种方式继续运行;
      • PTRACE_SYSCALL 会让被调用进程在每次 进入/退出 系统调用时都触发一次 SIGTRAP; strace 就是通过调用它来实现的, 在每次进入系统调用的时候读取出系统调用参数, 在退出系统调用的时候读取出返回值;
      • PTRACE_SINGLESTEP 会在每执行完一条指令后都触发一次 SIGTRAP; GDB 的 nexti, next 命令都是通过它来实现的;

    补充:

    在这个上下文中,PTRACE_SYSCALLPTRACE_SINGLESTEPptrace 系统调用的两种请求方式,它们允许进程在特定的情况下接收 SIGTRAP 信号。

    1. PTRACE_SYSCALLptrace 请求的一种,它让被调试的进程在每次进入或退出系统调用时都发送一个 SIGTRAP 信号。SIGTRAP 是一种由进程状态改变或者调试事件所引发的信号。

      ptrace 收到 SIGTRAP 信号时,它就知道被调试的进程正在进入或退出一个系统调用。这就是 strace 工具如何工作的。strace 在每次进入系统调用时,会读取系统调用的参数,然后在系统调用退出时,读取出返回值。因此,strace 可以提供一份详细的系统调用报告,包括每个调用的参数和返回值。

    2. PTRACE_SINGLESTEPptrace 请求的另一种,它允许被调试的进程在执行完一条指令后发送一个 SIGTRAP 信号。

      这意味着调试器(如 GDB)可以在每次接收到 SIGTRAP 信号时,停止被调试的进程,查看或修改其状态,然后决定是否继续执行。这就是 GDB 的 nextinext 命令如何工作的。nextinext 命令会让被调试的程序执行下一条指令,然后暂停,这样你就可以看到每一步的结果。

    在这两种模式下,调试器都可以利用 SIGTRAP 信号来监控被调试进程的状态,以便在关键点进行检查或修改。

    • PTRACE_DETACH, PTRACE_KILL 脱离进程间的跟踪关系;
      • 当父进程在子进程之前结束时, trace 关系会被自动解除;
  2. pid:这是你想要跟踪的进程的进程ID。对于某些请求(如 PTRACE_TRACEME),这个参数被忽略。

  3. addr:对于一些 request 类型,这个参数表示一个地址,例如你希望读/写的内存地址,或者你希望获取/设置的寄存器。

  4. data:对于一些 request 类型,这个参数表示一个数据值,例如你希望写入内存或寄存器的值。

ptrace 函数的返回值根据 request 参数的不同而不同。通常,如果成功,它会返回请求的系统信息。如果出现错误,它会返回 -1 并设置 errno 以指示错误。

需要注意的是,对 ptrace 的调用可能会因操作系统的不同而有所不同。另外,因为 ptrace 可以访问和改变进程的内存和寄存器,所以只有具有足够权限的进程才能使用它。

GDB 断点的实现原理

在 GDB 中设置断点时,GDB 的确会将断点位置的指令替换为 int 3,这是一个触发 SIGTRAP 信号的特殊指令。此外,GDB 也会保存原来的指令和断点的信息,以便于在恢复执行时可以恢复原始的指令。

当调试的程序运行到断点处并执行 int 3 指令时,SIGTRAP 信号会被触发并发送给 GDB。因为 GDB 已经通过 ptrace 建立了与调试程序的跟踪关系,所以它能够接收到这个信号。GDB 会检查这个信号来判断是否由一个断点引起的。这一判断是通过比较触发 SIGTRAP 的代码位置和已经设置的断点的位置来完成的。

如果确定信号是由一个断点引起的,GDB 就会暂停程序执行,并等待用户输入命令来决定下一步操作。如果用户的命令是继续执行,GDB 就会先恢复断点位置的原始指令,然后让程序继续执行。

修改子进程内存

我们通过下面的例子来演示父进程如何修改子进程的内存:

  • 父进程创建子进程, 并先让子进程 sleep 一段时间以保证父进程能更早运行;
  • 父进程通过 PTRACE_ATTACH 来和子进程建立跟踪关系;
  • 父进程修改子进程的内存数据;
  • 父进程通过调用 PTRACE_CONT 让子进程恢复执行;

完整代码如下:

#include <sys/ptrace.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define SHOW(call) ({ int _ret = (int)(call); printf("%s -> %d\n", #call, _ret); if (_ret < 0) { perror(NULL); }}) 
// 定义一个宏来显示系统调用的结果,并在调用失败时打印错误信息

char changeme[] = "This is  a test"; // 定义一个全局变量

int main (void) {
    pid_t pid = fork(); // 创建子进程
    int ret;
    int i;
    union {
        char cdata[8];
        int64_t data;
    } u = { "Hijacked" }; // 定义一个联合体,用于存储要写入子进程内存的字符串

    switch (pid) {
        case 0: /* child */
            sleep(2); // 让子进程先睡眠2秒,这样父进程就有时间修改内存
            printf("Children Message: %s\n", changeme); // 子进程打印修改后的消息
            exit(0);

        case -1:
            perror("fork"); // 创建子进程失败时打印错误信息并退出
            exit(1);
            break;

        default: /* parent */
            SHOW(ptrace(PTRACE_ATTACH, pid, 0, 0)); // 父进程通过 ptrace 附加到子进程
            SHOW(ptrace(PTRACE_POKEDATA, pid, changeme, u.data)); // 将新的字符串写入子进程的内存
            SHOW(ptrace(PTRACE_CONT, pid, 0, 0)); // 继续执行子进程
            printf("Parent Message: %s\n", changeme); // 父进程打印未被修改的消息
            wait(NULL); // 等待子进程退出
            break;
    }

    return 0;
}

(base) czy@czy-307-thinkcentre-m720q-n000:CSAPP/bomb $ ./changeme 
ptrace(PTRACE_ATTACH, pid, 0, 0) -> 0
ptrace(PTRACE_POKEDATA, pid, changeme, u.data) -> 0
ptrace(PTRACE_CONT, pid, 0, 0) -> 0
Parent Message: This is  a test
Children Message: Hijacked a test

可以看出子进程中的字符串已经被修改了, 而父进程中的字符串依旧保持不变.

在调用 ptrace(PTRACE_POKEDATA, pid, addr, data) 时,第四个参数 data 的类型被假定为一个长整数 (long),也就是说,系统默认 datalong 类型的数据。所以,不论在32位系统还是64位系统上,该参数都会以机器字长(word)的长度来处理。在64位系统中,long 类型通常就是64位,所以参数 data 被处理为64位。

代码使用一个联合体 u 来存储字符串 "Hijacked",然后通过 u.dataint64_t 类型传递这个字符串到 ptrace() 函数。这样,你就能够将8个字符(即64位数据)一次性写入目标进程的内存。这比逐个字符写入要高效。

需要注意的是,字符串 "Hijacked" 必须是8个字符或少于8个字符,否则会溢出,导致 data 的数据不正确。同时,由于 ptrace() 是以机器字为单位进行操作,如果要写入的数据不足一个机器字的长度,需要进行适当的填充或处理。

// example
union {
    char cdata[8];
    int64_t data;
} u;

// 将所有字节先初始化为0
memset(u.cdata, 0, sizeof(u.cdata));

// 再拷贝"Hijack"字符串
strncpy(u.cdata, "Hijack", sizeof("Hijack"));

Reference

  1. http://csapp.cs.cmu.edu/2e/docs/gdbnotes-x86-64.pdf
  2. https://en.wikipedia.org/wiki/GNU_Debugger
  3. https://hiberabyss.github.io/2018/04/04/gdb-internal/
  4. https://blog.csdn.net/edonlii/article/details/8717029
  5. GPT-4