How debuggers work: Part 1 - Basics学习笔记
Linux debugging - ptrace
《How debuggers work》写到ptrace是linux实现调试器中很重要的系统调用,以前在gdb调试报错的时候就能看到ptrace这个函数。
ptrace的man手册https://man7.org/linux/man-pages/man2/ptrace.2.html
ptrace的介绍
ptrace 提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心image。它主要用于实现断点调试。一个被跟踪的进程运行中,直到发生一个信号。则进程被中止,并且通知其父进程。在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。
ptrace的函数详解
函数原型
1 | |
-
参数request:请求ptrace执行的操作
-
参数pid:目标进程的ID
-
参数addr:目标进程的地址值
-
参数data:作用则根据request的不同而变化,如果需要向目标进程中写入数据,data存放的是需要写入的数据;如果从目标进程中读数据,data将存放返回的数据
request参数决定了CODE的行为以及后续的参数是如何被使用的,参数request的常用的值如下:

示例说明
main函数
1 | |
run_target函数
1 | |
这里的execl函数有点不懂,查了一下,文档上是这么写的“This, as the highlighted part explains, causes the OS kernel to stop the process just before it begins executing the program in execl and send a signal to the parent.”,应该是调execl函数会停止execl之前的进程,然后执行新的程序,进程空间被新的程序占有,并会向父进程发出信号。
exec函数说明
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
其实有六种以exec开头的函数,统称exec函数:
1 | |
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。不带字母p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"或"./a.out",而不能是"ls"或"a.out"。对于带字母p的函数:
- 如果参数中包含/,则将其视为路径名。
- 否则视为不带路径的程序名,在
PATH环境变量的目录列表中搜索这个程序。
带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有...,...中的最后一个可变参数应该是NULL,起sentinel的作用。对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。
对于以e(表示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。
run_debugger函数
1 | |
然后创建一个helloworld的C程序,然后用上面的程序运行它
1 | |
实际上指令计数器icounter最后的结果高达1W多,主要是程序的初始化,还有printf是个很复杂的函数,导致实际的指令并非只有上面那几条。
新的run_debugger函数
1 | |
编译的时候一定要加-m32,不然会报错说没有eip
创建一个新的汇编程序
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!', 0xa
len equ $ - msg
上面的代码在我的机器一直编译不过去,查了一下gcc用的是AT&T的语法,下面是我改过的,其实还是有问题,但不妨碍理解
# asm.s
.code32
.section .data
msg:
.ascii "Hello world!\n"
len:
.long 13
.section .text
.globl _start
_start:
movl len, %edx
movl $msg, %ecx
movl $1, %ebx
movl $4 , %eax
/* 这里学到一点
符号常数
符号常数直接引用 如
value: .long 0x12a3f2de
movl value , %ebx
指令执行的结果是将常数0x12a3f2de装入寄存器ebx。
引用符号地址在符号前加符号$, 如“movl $value, % ebx”则是将符号value的地址装入寄存器ebx。*/
int $0x80
movl $1, %eax
int $0x80
用as –32 asm.s -o asm.o,ld -m elf_i386 asm.o -o asm编译成32位程序
然后用新的run_debugger运行调试它
1 | |
下面是我的结果


instr是EIP的操作符和操作数,可以用objdump -d对照上面的结果是否正确
1 | |