ptrace系統調用的實現

最近遇到這樣一個問題,機器跑着跑着畫面凍結了,打開top看到Xorg的cpu佔用率100%。想用gdb掛上去看一下,結果gdb一直卡着掛不上去。後來又換用perf分析,結果發現進程99%的時間花在了一個ioctl調用。這個ioctl操作的是nvidia顯卡,進程實際上是卡在了nvidia的驅動中。

我對gdb掛不上去這件事感到很好奇,之前除了因爲進程已經被另一個gdb調試而導致gdb掛不上去之外,還沒有遇到過這種情況,所以看了內核源碼分析了一下。

先說結論,爲什麼此時gdb掛不上去呢?簡單的來說就是:調試器(tracer)是基於ptrace實現的,使用ptrace連接(attach)被調試進程(tracee)時,會向tracee發送一個SIGSTOP信號,讓tracee停下來,後續的調試工作要等tracee停下來才能繼續進行。而tracee能夠停下來又要依賴對信號的響應,但是進程只有在從內核空間返回用戶空間時,纔會檢查是否有待處理的信號,並進行響應。而如果進程一直卡在內核空間的話,就無法返回用戶空間,所以就無法響應信號,導致進程無法停止。這樣的話,tracer就只能一直等着tracee,無法進行調試。

上面是概要的結論,下面對ptrace系統調用的基本流程分析一下,由於ptrace的實現細節非常多,所以此處只是其大致框架。我使用的內核版本是4.16,系統架構爲64位x86。爲了與文檔和代碼中的術語保持一致,不再使用“調試”這個詞,而是使用“跟蹤”(trace)。(“跟蹤”實際上是手段,“調試”是目的)

ptrace操作的環境

除了PTRACE_TRACEMEPTRACE_ATTACHPTRACE_SEIZE這些用於本身就用於建立ptrace操作環境的請求之外,其他請求在執行之前都會檢查ptrace環境是否已經建立。

SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
		unsigned long, data)
{
......
	ret = ptrace_check_attach(child, request == PTRACE_KILL ||
				  request == PTRACE_INTERRUPT);
	if (ret < 0)
		goto out_put_task_struct;
......
}

ptrace_check_attach函數就是用來檢查ptrace環境的。ptrace環境有兩部分:一部分是要求tracer和tracee有跟蹤關係,另一部分是要求tracee處於被跟蹤的停止狀態。後一部分在請求爲PTRACE_KILL或者PTRACE_INTERRUPT時不需要保證。

static int ptrace_check_attach(struct task_struct *child, bool ignore_state)
{
	int ret = -ESRCH;

	/*
	 * We take the read lock around doing both checks to close a
	 * possible race where someone else was tracing our child and
	 * detached between these two checks.  After this locked check,
	 * we are sure that this is our traced child and that can only
	 * be changed by us so it's not changing right after this.
	 */
	read_lock(&tasklist_lock);
	if (child->ptrace && child->parent == current) {
		WARN_ON(child->state == __TASK_TRACED);
		/*
		 * child->sighand can't be NULL, release_task()
		 * does ptrace_unlink() before __exit_signal().
		 */
		if (ignore_state || ptrace_freeze_traced(child))
			ret = 0;
	}
	read_unlock(&tasklist_lock);

	if (!ret && !ignore_state) {
		if (!wait_task_inactive(child, __TASK_TRACED)) {
			/*
			 * This can only happen if may_ptrace_stop() fails and
			 * ptrace_stop() changes ->state back to TASK_RUNNING,
			 * so we should not worry about leaking __TASK_TRACED.
			 */
			WARN_ON(child->state == __TASK_TRACED);
			ret = -ESRCH;
		}
	}

	return ret;
}

上面的child->ptrace && child->parent == current用來保證tracer和tracee的跟蹤關係,而ptrace_freeze_traced函數用來測試tracee是否處於期望的狀態,即tracee的狀態有__TASK_TRACED標誌位,且當前沒有未處理的SIGKILL信號。

static bool ptrace_freeze_traced(struct task_struct *task)
{
	bool ret = false;

	/* Lockless, nobody but us can set this flag */
	if (task->jobctl & JOBCTL_LISTENING)
		return ret;

	spin_lock_irq(&task->sighand->siglock);
	if (task_is_traced(task) && !__fatal_signal_pending(task)) {
		task->state = __TASK_TRACED;
		ret = true;
	}
	spin_unlock_irq(&task->sighand->siglock);

	return ret;
}

下面來分析如何建立ptrace環境。

tracer和tracee的跟蹤狀態

進程tracer首先要成爲進程tracee的跟蹤進程,即要滿足:tracee->ptrace && tracee->parent == tracer。如何做到呢?有兩種方式:一種情況是tracer是tracee的父進程,進程tracee主動調用ptrace(PTRACE_TRACEME, 0, 0, 0),另外一種是進程tracer調用ptrace(PTRACE_ATTACH, tracee->pid, 0, 0)。我們來看一下這兩種方式的流程。

ptrace(PTRACE_TRACEME, 0, 0, 0)

ptrace系統調用中,當請求爲PTRACE_TRACEME時,會直接調用ptrace_traceme

SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
		unsigned long, data)
{
	......
	if (request == PTRACE_TRACEME) {
		ret = ptrace_traceme();
		......
	}
    ......
}

ptrace_traceme函數中,首先要檢查現在沒有其他進程跟蹤當前進程,然後進行權限檢查。都沒有問題的話,將當前進程ptrace字段置爲PT_PTRACED,然後調用ptrace_link,而ptrace_link最終調用了__ptrace_link,最終將父進程設爲了跟蹤進程。

static int ptrace_traceme(void)
{
	int ret = -EPERM;

	write_lock_irq(&tasklist_lock);
	/* Are we already being traced? */
	if (!current->ptrace) {
		ret = security_ptrace_traceme(current->parent);
		/*
		 * Check PF_EXITING to ensure ->real_parent has not passed
		 * exit_ptrace(). Otherwise we don't report the error but
		 * pretend ->real_parent untraces us right after return.
		 */
		if (!ret && !(current->real_parent->flags & PF_EXITING)) {
			current->ptrace = PT_PTRACED;
			ptrace_link(current, current->real_parent);
		}
	}
	write_unlock_irq(&tasklist_lock);

	return ret;
}

__ptrace_link首先將tracee放到tracer的ptraced列表中,然後將tracee的parent字段置爲tracer。(請注意區分parent字段和real_parent字段)。

void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
		   const struct cred *ptracer_cred)
{
	BUG_ON(!list_empty(&child->ptrace_entry));
	list_add(&child->ptrace_entry, &new_parent->ptraced);
	child->parent = new_parent;
	child->ptracer_cred = get_cred(ptracer_cred);
}

ptrace(PTRACE_ATTACH, tracee->pid, 0, 0)

tracer跟蹤tracee的另一種方式是使用PTRACE_ATTACH(或PTRACE_SEIZE)請求。與處理PTRACE_TRACEME請求的ptrace_traceme函數類似,處理PTRACE_ATTACH請求時會調用ptrace_attach函數。ptrace_attach代碼行數較ptrace_traceme長不少,因爲要做大量的合法性檢查:tracer和tracee可能是同一個進程,tracee可能是個內核線程,也可能是其他用戶的進程,等等有各種意外情況。但如果沒問題的話,最終也是調用了ptrace_link將tracer設置爲tracee的跟蹤進程,在此不再贅述。有一個不一樣的地方是,處理PTRACE_ATTACH請求時會向tracee發送一個SIGSTOP信號。

至此,tracer就成爲了tracee的跟蹤進程,但此時tracer還不能立即對tracee使用ptrace請求,還有另一個前提條件:tracee已經停止運行,即state字段中__TASK_TRACED被置位。

使tracee停止運行

內核何時對tracee的state字段的__TASK_TRACED標誌位進行置位呢?是在tracee處理除SIGKILL信號以外的任何信號時做的。

進程在從內核空間返回用戶空間時會檢查是否有掛起信號,如果有的話,調用do_signal進行處理。

static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
......
		/* deal with pending signal delivery */
		if (cached_flags & _TIF_SIGPENDING)
			do_signal(regs);
......
}

do_signal需要調用get_signal來填充一個ksignal結構體。

void do_signal(struct pt_regs *regs)
{
......
	if (get_signal(&ksig)) {
		/* Whee! Actually deliver the signal.  */
		handle_signal(&ksig, regs);
		return;
	}
......
}

get_signal過程中,會檢查當前進程是否處於被ptrace跟蹤的狀態,如果是的話,且當前信號不是SIGKILL,則會調用ptrace_signal

int get_signal(struct ksignal *ksig)
{
......
		if (unlikely(current->ptrace) && signr != SIGKILL) {
			signr = ptrace_signal(signr, &ksig->info);
			if (!signr)
				continue;
		}
......
}

ptrace_signal調用ptrace_stop來使當前進程停下來。

static int ptrace_signal(int signr, siginfo_t *info)
{
......
	ptrace_stop(signr, CLD_TRAPPED, 0, info);
......
}

ptrace_stop首先將進程狀態設置爲TASK_TRACED,這樣的話,下次進行進程調度時就不會調度該進程了。設置完進程狀態後,會發送SIGCHLD信號通知當前進程的parentreal_parent。最後調用freezable_schedule進行進程調度,將該進程停下來。

static void ptrace_stop(int exit_code, int why, int clear_code, siginfo_t *info)
	__releases(&current->sighand->siglock)
	__acquires(&current->sighand->siglock)
{
......
	set_current_state(TASK_TRACED);
......
		do_notify_parent_cldstop(current, true, why);
		if (gstop_done && ptrace_reparented(current))
			do_notify_parent_cldstop(current, false, why);
......
		freezable_schedule();
......
}

爲了能讓tracee停止運行,信號是必不可少的,無論是PTRACE_ATTACH請求、breakpoint或者是watchpoint,都依賴於給tracee發送一個信號,同時tracee還要有機會去檢查當前有哪些掛起的信號。就像文章一開始提到的問題,進程一直卡在內核空間,沒有機會在返回用戶空間時檢查當前掛起的信號,也就無法停止運行了。

至此ptrace請求的前置條件就完全滿足了,可以使用其他ptrace請求了。

其他的ptrace請求無非是讀寫tracee的task_struct、內存和寄存器了,似乎沒什麼太特別的地方,也就不再分析了。

結束語

以上就是ptrace系統調用實現的大致原理。不過有一部分沒有提到,就是tracer與tracee如何detach,主要是因爲這部分和文章開頭提到的問題不相干,所以就沒有看這部分的源碼,有興趣的可以自己看一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章