linux ptrace 图文详解(三) PTRACE_ATTACH 跟踪程序
目录
一、gdb attach 调试程序
二、PTARCE_ATTACH 实现原理
三、gdb attach多线程程序
四、代码实现
五、总结
(代码:linux 6.3.1,架构:arm64)
One look is worth a thousand words. —— Tess Flanders
相关链接:
linux ptrace 图文详解(一)基础介绍
linux ptrace 图文详解(二) PTRACE_TRACEME 跟踪程序
一、gdb attach 调试程序
上篇文章中,介绍了使用gdb加载并调试APP的方式,不过在实际应用场景下,很多程序是已经在系统中处于运行状态的。此时,若需要对正在运行的程序进行调试的话,就需要采用gdb attach的方式。
通过 ptrace(PTRACE_ATTACH) 系统调用, 允许 gdb 附加到一个正在运行的进程上进行调试,这种功能特别适用于调试已经在运行的程序,例如在生产环境中对服务进行问题排查。
使用实例如下:
1)获取被调试程序pid
2)gdb attach 指定pid,并附加到被调试程序
需要注意的是:使用 PTRACE_ATTACH 时可能遇到权限问题,例如:一个非root用户无法附加到一个以root用户运行的进程上。这种情况下,需要使用root用户权限来执行gdb attach 命令。
二、PTARCE_ATTACH 实现原理
可以看到,gdb attach正在运行的程序进行调试的实现原理,就是依靠的ptrace系统调用,并且以PTRACE_ATTACH为参数。其实现原理如下图所示:
1)gdb调用ptrace(PTRACE_ATTACH)系统调用陷入内核;
2)根据被调试程序的pid,找到内核中该程序的task_struct对象,并为其置位PT_PTRACED,代表该进程处于被跟踪状态;
3)将gdb设置为被调试程序的养父;
4)在内核中给被调试程序发送一个SIGSTOP信号,然后gdb的ptrace系统调用返回;
5)被调试程序接收到信号,在内核态返回用户态前夕,执行信号处理流程;
6)被调试程序发现自身处于PTRACED状态,于是调用ptrace_signal函数,给养父进程(gdb)发送SIGCHLD信号,并唤醒养父进程的wait系统调用;
7)最后被调试程序将自己挂起,等待后续养父gdb将自己唤醒;
三、gdb attach多线程程序
大部分情况下,被调试程序是一个多线程进程,那么gdb attach调试该进程时,需要针对每个线程都调用ptrace(PTRACE_ATTACH)系统调用,用于在内核中给每个被调试线程置位PT_PTRACED标志。
上述过程的实现原理:gdb会通过遍历proc文件系统中pid对应进程中的所有线程,并针对每个线程都调用一次ptrace(PTRACE_ATTACH),大致代码如下:
linux_nat_target::attach {
pid_t pid = parse_pid_to_attach (args)
ptid_t ptid = ptid_t (inferior_ptid.pid (), inferior_ptid.pid ())
/* Add the initial process as the first LWP to the list. */
lp = add_initial_lwp (ptid)
/* We must attach to every LWP. If /proc is mounted, use that to find them now. */
linux_proc_attach_tgid_threads(pid_t pid = lp->ptid.pid (), linux_proc_attach_lwp_func attach_lwp = attach_proc_task_lwp_callback) {
xsnprintf (pathname, sizeof (pathname), "/proc/%ld/task", (long) pid)
dir = opendir (pathname)
for (iterations = 0; iterations < 2; iterations++) {
while ((dp = readdir (dir)) != NULL)
attach_lwp(ptid)
A.K.A
attach_proc_task_lwp_callback {
lp = find_lwp_pid (ptid)
int lwpid = ptid.lwp ()
ptrace(PTRACE_ATTACH, lwpid, 0, 0) { // <<<<<<< 对每个thread都调用ptrace(PTRACE_ATTACH)
/* 1) notify PM to ptrace_link */
/* 2) trap into kernel and send SIGSTOP to taregt task */
child = pid_to_task(pid)
ptrace_attach(ktask_t *task = child, long request) {
task->restore_state_flags = task->state_flags
task->state_flags = K_TASK_INTERRUPTIBLE
### ptrace_attach就是给当前任务发送SIGSTOP信号
if (request != PTRACE_SEIZE) {
task->ptrace = PT_PTRACED
_tkill(task->pid, _SIGSTOP)
}
}
}
lp = add_lwp (ptid)
add_thread (linux_target, lp->ptid)
}
}
closedir (dir)
}
}
四、代码实现
1、PTRACE_ATTACH实现原理
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
unsigned long, data)
{
struct task_struct *child
child = find_get_task_by_vpid(pid)
if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) {
ptrace_attach(struct task_struct *task = child, request, addr, data) {
flags = PT_PTRACED
task_lock(task)
retval = __ptrace_may_access(task, PTRACE_MODE_ATTACH_REALCREDS)
task_unlock(task)
write_lock_irq(&tasklist_lock)
/* 1) 将被调试任务的task置上PT_PTRACED标志, 代表该任务处于被跟踪状态 */
task->ptrace = flags
/* 2) 设置gdb作为被调试程序的养父 */
ptrace_link(struct task_struct *child = task, struct task_struct *new_parent = current) {
__ptrace_link(child, new_parent, const struct cred *ptracer_cred = current_cred()) {
list_add(&child->ptrace_entry, &new_parent->ptraced)
child->parent = new_parent // <<<<<<<<<
child->ptracer_cred = get_cred(ptracer_cred)
}
}
/* 3) 发送SIGSTOP信号给被调试任务, 让其响应信号停下来并通知养父gdb */
if (!seize)
send_sig_info(SIGSTOP, SEND_SIG_PRIV, task)
spin_lock(&task->sighand->siglock)
if (task_is_stopped(task) && task_set_jobctl_pending(task, JOBCTL_TRAP_STOP | JOBCTL_TRAPPING)) {
task->jobctl &= ~JOBCTL_STOPPED
signal_wake_up_state(task, __TASK_STOPPED)
}
spin_unlock(&task->sighand->siglock)
write_unlock_irq(&tasklist_lock)
return retval
}
goto out_put_task_struct
}
...
out_put_task_struct:
put_task_struct(child)
return ret
}
2、被调试程序处理SIGSTOP并将自己挂起
exit_to_user_mode {
prepare_exit_to_user_mode
local_daif_mask
do_notify_resume {
if (thread_flags & _TIF_NEED_RESCHED)
schedule()
if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
do_signal {
get_signal
ptrace_signal /* Detail see below */
}
}
}
ptrace_signal(int signr, kernel_siginfo_t *info) {
ptrace_stop(int exit_code = signr,
int why = CLD_TRAPPED,
int clear_code = 0,
kernel_siginfo_t *info = info) {
set_special_state(TASK_TRACED) // (1) set current task state to TRACED
current->__state = TASK_TRACED
A.K.A
current->__state = TASK_WAKEKILL | __TASK_TRACED
spin_unlock_irq(¤t->sighand->siglock)
read_lock(&tasklist_lock)
if (likely(current->ptrace)) { /* Tracer is alive */
do_notify_parent_cldstop(struct task_struct *tsk =current,
bool for_ptracer = true, why = CLD_TRAPPED) {
struct kernel_siginfo info
if (for_ptracer)
parent = tsk->parent // here parent is tracer
info.si_signo = SIGCHLD // send SIGCHLD signal
info.si_code = why // A.K.A CLD_TRAPPED
info.si_status = tsk->exit_code & 0x7f
struct sighand_struct *sighand = parent->sighand
spin_lock_irqsave(&sighand->siglock, flags)
if (sighand->action[SIGCHLD-1].sa.sa_handler != SIG_IGN) {
__group_send_sig_info(SIGCHLD, &info, parent) // (2) notify tracer about every stop of tracee
send_signal
}
/* Even if SIGCHLD is not generated, we must wake up wait4 calls. */
__wake_up_parent(struct task_struct *p = tsk, // (3) wake up parent if it's waiting
struct task_struct *parent = parent) {
__wake_up_sync_key(struct wait_queue_head *wq_head = &parent->signal->wait_chldexit,
unsigned int mode = TASK_INTERRUPTIBLE,
void *key = p)
__wake_up_common_lock(wq_head, mode, 1, WF_SYNC, key)
__wake_up_common
}
spin_unlock_irqrestore(&sighand->siglock, flags)
}
/* Don't want to allow preemption here, because sys_ptrace() needs this task to be inactive. */
preempt_disable()
read_unlock(&tasklist_lock)
preempt_enable_no_resched()
freezable_schedule()
schedule // (4) tracee will be scheduled out
} else { /* Tracer is gone */
__set_current_state(TASK_RUNNING)
read_unlock(&tasklist_lock)
}
spin_lock_irq(¤t->sighand->siglock)
recalc_sigpending_tsk(current) { /* Queued signals ignored us while we were stopped for tracing. */
if (PENDING(&t->pending, &t->blocked),
PENDING(&t->signal->shared_pending, &t->blocked)) {
set_tsk_thread_flag(t, TIF_SIGPENDING)
}
}
}
}
五、总结
PTRACE_ATTACH 的主要作用就是给被调试任务的内核task_struct对象置位PT_PTRACED标志。这样一来,后续该任务在内核中执行一些指定行为(接受信号、系统调用、处理断点异常等)时,都会判断自己处于被跟踪状态后,通知养父进程gdb,并将自己挂起。
不论是TRACEME的方式,还是ATTACH的方式,最终目的都是将被调试任务内核task对象置位PT_PTRACED标志,并建立gdb与被调试程序之间养父的关联。
至此,gdb 跟踪被调试程序的两种方式(TRACEME、ATTACH)的实现原理就都清楚了,我们知道GDB是如何跟被调试程序之间建立关联的。后续就可以继续讲述gdb是如何给被调试程序打软断点、硬断点、观察点,如何跟踪程序系统调用,如何跟踪程序fork、exec,如何进行单步调试等一系列底层实现原理了。