前言
gdb 的核心技术就是使用 ptrace 系统调用。
ptrace
NAME
ptrace - process trace
SYNOPSIS
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
DESCRIPTION
The ptrace() system call provides a means by which one process (the "tracer") may observe and control the execution of another process (the "tracee"),
and examine and change the tracee's memory and registers. It is primarily used to implement breakpoint debugging and system call tracing.
ptrace() 系统调用提供了一种方法,通过它,一个进程(“tracer”)可以观察和控制另一个进程(“tracee”)的执行,并且可以检查和改变 tracee 的内存和寄存器。它主要用于实现断点调试和系统调用跟踪。
ptrace() 的第一个参数 request
是枚举类型,其值和含义如下
PTRACE_TRACEME, 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程
PTRACE_PEEKTEXT, 从内存地址中读取一个字节,内存地址由addr给出
PTRACE_PEEKDATA, 同上
PTRACE_PEEKUSER, 可以检查用户态内存区域(USER area),从USER区域中读取一个字节,偏移量为addr
PTRACE_POKETEXT, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEDATA, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEUSER, 往USER区域中写入一个字节,偏移量为addr
PTRACE_GETREGS, 读取寄存器
PTRACE_GETFPREGS, 读取浮点寄存器
PTRACE_SETREGS, 设置寄存器
PTRACE_SETFPREGS, 设置浮点寄存器
PTRACE_CONT, 重新运行
PTRACE_SYSCALL, 重新运行
PTRACE_SINGLESTEP, 设置单步执行标志
PTRACE_ATTACH, 追踪指定pid的进程
PTRACE_DETACH, 结束追踪
这里主要介绍 PTRACE_TRACEME
和 PTRACE_ATTACH
,它们分别对应两种追踪模式。
PTRACE_TRACEME
ptrace(PTRACE_TRACEME, 0, 0, 0)
通常为被追踪者使用,用来指示此进程将由其父进程跟踪。示例如下:
被追踪进程 loop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
int i = 0;
printf("pid = %d\n", getpid());
while (1) {
printf("hello world %d\n", i++);
sleep(1);
}
return EXIT_SUCCESS;
}
ptrace6.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
int status;
if (argc < 2) {
fprintf(stderr, "Usage: %s <program> [args...]\n", argv[0]);
return 1;
}
pid = fork();
if (pid == 0) {
// Child process
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execvp(argv[1], argv + 1);
perror("execvp");
exit(1);
} else if (pid > 0) {
// Parent process
waitpid(pid, &status, 0);
printf("Child started, pid = %d\n", pid);
printf("chld got dignal:%s\n", strsignal(WSTOPSIG(status)));
printf("子进程暂停 2s\n");
sleep(2);
printf("子进程继续执行 5s\n");
ptrace(PTRACE_CONT, pid, NULL, NULL);
sleep(5);
printf("子进程暂停 3s\n");
// 任何发给子进程的信号 signal(SIGKILL 除外)将导致子进程暂停运行
kill(pid, SIGTRAP);
sleep(3);
printf("子进程继续执行 2s\n");
ptrace(PTRACE_CONT, pid, NULL, NULL);
sleep(2);
printf("杀死子进程\n");
kill(pid, SIGKILL);
} else {
perror("fork");
return 1;
}
return 0;
}
编译、运行
gcc loop.c -o loop.out
gcc ptrace6.c -o ptrace6.out -Wall
$ ./ptrace6.out ./loop.out
Child started, pid = 979394
chld got dignal:Trace/breakpoint trap
子进程暂停 2s
子进程继续执行 5s
pid = 979394
hello world 0
hello world 1
hello world 2
hello world 3
hello world 4
子进程暂停 3s
子进程继续执行 2s
hello world 5
hello world 6
hello world 7
杀死子进程
父进程 fork 出一个子进程,子进程调用 PTRACE_TRACEME,表明这个进程由它的父进程来跟踪。
任何发给子进程的信号 signal(SIGKILL 除外)将导致子进程暂停运行,而它的父进程会通过 wait() 获得通知。
另外,子进程调用 exec 会使操作系统产生一个 SIGTRAP 信号发送给子进程,这让父进程有机会在新程序开始执行之前获得对子进程的控制权。
PTRACE_ATTACH
此追踪模式将主动发送一个停止信号给目标进程(ATTACH 模式),使目标程序暂停下来,进而进行调试。
ptrace7.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
printf("请输入进程号:\n");
scanf("%d", &pid);
printf("子进程暂停 2s\n");
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
sleep(2);
printf("子进程继续执行 5s\n");
ptrace(PTRACE_CONT, pid, NULL, NULL);
sleep(5);
printf("子进程暂停 3s\n");
kill(pid, SIGTRAP);
sleep(3);
printf("子进程继续执行 2s\n");
ptrace(PTRACE_CONT, pid, NULL, NULL);
sleep(2);
printf("结束对子进程的控制\n");
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return 0;
}
编译、运行
gcc ptrace7.c -o ptrace7.out -Wall
番外
t+
当子进程被追踪并且暂停时,处于 t+
状态,t
表示被跟踪状态(TASK_TRACED),+
表示位于前台的进程组。
liyongj+ 980243 0.0 0.0 2496 576 pts/93 t+ 17:59 0:00 ./loop.out
Q:TASK_TRACED 状态的进程会被 CPU 调度吗?
A:TASK_TRACED 状态的进程不会被CPU调度。在 Linux 中,当一个进程被设置为 TASK_TRACED 状态时,它会暂停执行,并等待调试器或其它进程唤醒。操作系统会将该进程从可调度进程队列中移除,直到被唤醒后再次加入可调度队列。
S+
当子进程运行时,处于 S+
状态,S
表示可中断的睡眠状态。
liyongj+ 980243 0.0 0.0 2496 576 pts/93 S+ 17:59 0:00 ./loop.out