目录
一、基础介绍
二、PTRACE_TRACE 实现原理
三、代码实现
四、总结
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.Agnu_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 = datachild->jobctl &= ~JOBCTL_TRACEDwake_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_execvedo_execveat_commonbprm_execveexec_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_modelocal_daif_maskdo_notify_resume {if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))do_signal {get_signalptrace_signalptrace_stop(exit_code = signo, why = CLD_TRAPPED, 0, info) /* Stop tracee itself, and notify parent tracer */{current->last_siginfo = infocurrent->exit_code = exit_codedo_notify_parent_cldstop(current, true, why) {info.si_signo = SIGCHLDinfo.si_code = whyinfo.si_status = tsk->exit_code & 0x7fsend_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串口,对被调试程序进行一系列调试操作。