linux ptrace 图文详解(二) PTRACE_TRACEME 跟踪程序
目录
一、基础介绍
二、PTRACE_TRACE 实现原理
三、代码实现
四、总结
(代码:linux 6.3.1,架构:arm64)
One look is worth a thousand words. —— Tess Flanders
一、基础介绍
GDB(GNU Debugger)是 Linux 系统中功能强大的调试工具,用于调试 C、C++ 等编程语言编写的程序。GDB 支持两种主要的调试方式:"gdb主动加载被调试程序" 和 "gdb通过 attach
调试正在运行的程序"。这两种方式各有特点,适用于不同的调试场景。
1)在 主动加载被调试程序 的方式中:GDB 通过 fork
和 exec
系统调用启动目标程序,并使用 ptrace
对其进行控制。这种方式适用于从头开始调试程序,开发者可以在程序启动时设置断点、单步执行或观察变量的初始状态。
2)在 通过 attach
调试正在运行的程序 的方式中:GDB 通过 ptrace
附加到目标进程的 PID,直接接管其执行流程。这种方式适用于调试已经运行的程序,尤其是当程序出现崩溃、死锁或性能问题时,开发者可以实时分析程序的状态、调用栈和内存信息。
两种调试方式的对比:
二、PTRACE_TRACE 实现原理
以上就是 gdb 加载“被调试程序”启动阶段时的完整实现流程:
1)gdb 调用 fork 创建子进程,用作后续的被调试程序,fork执行完毕后,gdb就调用wait系统调用等待在子进程上;
2)子进程调用ptrace系统调用,请求类型为PTRACE_TRACEME,在内核中给子进程的task_struct对象置上PT_PTRACED标志;
3)子进程调用exec,加载被调试程序的ELF文件;
4)内核中调用 load_elf_binary 完成ELF加载工作;
5)在内核的 exec 末期,发现自身置位了PT_PTRACED标志,于是调用ptrace_notify,给子进程自身发送一个SIGTRAP信号(用于后续将子进程挂起);
6)子进程exec系统调用执行完毕后,在返回用户态前夕检查信号,发现自身有一个SIGTRAP信号需要处理,于是走信号处理流程;
7)子进程在内核的信号处理流程中,发送SIGCHLD信号给父进程gdb,并唤醒父进程gdb,同时将自身挂起;
8)gdb被唤醒后,控制权交给用户,用户可以对被调试程序进行一系列操作(如:打断点、观察点等);
9)用户操作完毕后,输入 continue指令,让目标程序继续运行。该指令实际会调用ptrace(PTRACE_CONT) 系统调用,在内核中该系统调用会将子进程唤醒;
10)子进程被唤醒后,重新返回到用户态,开始执行第一条指令!
三、代码实现
1、gdb 加载 被调试程序
start_command {
run_command_1 {
run_target->create_inferior (exec_file, current_inferior ()->args (),
current_inferior ()->environment.envp (), from_tty)
{
fork_inferior(exec_file, allargs, env, void (*traceme_fun) () = gnu_ptrace_me, NULL, NULL, NULL, NULL) {
pid = fork ()
if (pid == 0) { /* 子进程(被调试程序) */
(*traceme_fun) ()
A.K.A
gnu_ptrace_me {
ptrace (PTRACE_TRACEME)
}
execvp (argv[0], &argv[0]) // 加载被调试程序ELF
}
return pid /* 父进程(GDB): 返回子进程pid */
}
...
}
}
}
2、PTRACE_TRACEME 内核实现
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
unsigned long, data)
{
if (request == PTRACE_TRACEME) {
ptrace_traceme() {
write_lock_irq(&tasklist_lock)
if (!current->ptrace) {
ret = security_ptrace_traceme(struct task_struct *parent = current->parent) {
return cap_ptrace_traceme(parent)
}
if (!ret && !(current->real_parent->flags & PF_EXITING)) {
current->ptrace = PT_PTRACED // <<<<<< 子进程标记自己处于“PTRACED状态” <<<<<<
ptrace_link(current, current->real_parent)
}
}
write_unlock_irq(&tasklist_lock)
}
}
...
}
3、PTRACE_CONT 内核实现
ptrace_request(struct task_struct *child, long request, unsigned long addr, unsigned long data) {
switch (request) {
case PTRACE_CONT:
return ptrace_resume(child, request, data) {
... /* PTRACE跟踪syscall、单步调试等处理 */
spin_lock_irq(&child->sighand->siglock)
child->exit_code = data
child->jobctl &= ~JOBCTL_TRACED
wake_up_state(child, __TASK_TRACED) {
return try_to_wake_up(p, state, 0) // <<<<< 尝试唤醒被调试程序 <<<<<
}
spin_unlock_irq(&child->sighand->siglock)
}
}
}
4、内核 exec执行完毕后,返回用户态前夕,发送SIGCHLD给父进程gdb
// gdb通过fork创建出来的子进程, 调用exec加载被调试程序, 并给自己发送SIGTRAP信号
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
do_execve
do_execveat_common
bprm_execve
exec_binprm {
search_binary_handler {
list_for_each_entry(fmt, &formats, lh) {
retval = fmt->load_binary(bprm) // <<<<< 加载ELF程序 主体函数 <<<<<
}
}
### 给自身发送SIGTRAP信号, 在exec系统调用执行完毕返回用户态前夕, 处理该信号
ptrace_event(PTRACE_EVENT_EXEC, old_vpid) {
if ((current->ptrace & (PT_PTRACED|PT_SEIZED)) == PT_PTRACED)
send_sig(SIGTRAP, current, 0)
}
}
}
// 子进程exec执行完毕返回用户态前夕, 处理自身的SIGTRAP信号, 发送信号给GDB, 并将其唤醒, 随后自身挂起
exit_to_user_mode(regs) {
prepare_exit_to_user_mode
local_daif_mask
do_notify_resume {
if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
do_signal {
get_signal
ptrace_signal
ptrace_stop(exit_code = signo, why = CLD_TRAPPED, 0, info) /* Stop tracee itself, and notify parent tracer */
{
current->last_siginfo = info
current->exit_code = exit_code
do_notify_parent_cldstop(current, true, why) {
info.si_signo = SIGCHLD
info.si_code = why
info.si_status = tsk->exit_code & 0x7f
send_signal_locked(SIGCHLD, &info, parent, PIDTYPE_TGID) // <<<<<< 发送信号给父进程GDB
__wake_up_parent(tsk, parent) // <<<<<< 唤醒父进程GDB
}
schedule() // <<<<<< 被调试程序自身挂起
}
}
}
}
四、总结
gdb加载 “被调试程序” 进行调试的模式,主要依赖 PTRACE_TRACEME请求类型的ptrace系统调用,给子进程置上ptraced标记,后续子进程调用exec加载被调试程序ELF时给自己发送一个SIGTRAP信号,最后exec系统调用执行完毕并返回用户态前夕,在信号处理流程中,将自身挂起并唤醒GDB,让用户可以接管GDB串口,对被调试程序进行一系列调试操作。