文章目录
- 一、原理分析
- 二、dmoe测试
- 2.1 hello.s
- 2.2 demo演示
- 参考资料
一、原理分析
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/user.h>int main(int argc, char ** argv)
{pid_t childPid;int status;childPid = fork();if (childPid == 0) {//设置子进程进入跟踪模式ptrace(PTRACE_TRACEME, 0, 0, 0);//待调试的程序,进入调试模式状态execvp(argv[1], argv + 1);return 0;} else {//阻塞在这里--等待子进程停止waitpid(childPid, 0, 0);//子进程进入停止状态,父进程开始调试程序//调用 ptrace 系统调用来调式程序//这里只进行一次ptrace 调用//获取子进程执行第一个系统调用的系统调用号 即exec系统调用号//execve系统调用号是59struct user_regs_struct regs;ptrace(PTRACE_GETREGS, childPid, 0, ®s);long syscallNumber = regs.orig_rax;printf("syscallNumber = %d\n", syscallNumber);ptrace(PTRACE_DETACH, childPid, 0, 0);}return 0;
}
这个例子没有实际意义,这里只是用来说明gdb调试进程的原理:
# ./a.out ./hello
syscallNumber = 59
Hello, World!
子进程调用 ptrace(PTRACE_TRACEME, 0, 0, 0),进入被跟踪模式,PTRACE_TRACEME该值仅tracee使用,指示此进程将由其父进程跟踪。
父进程是tracer,子进程是tracee.
接下来让我们分析其源码:
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,unsigned long, data)
{struct task_struct *child;long ret;if (request == PTRACE_TRACEME) {ret = ptrace_traceme();}......}
/*** ptrace_traceme -- helper for PTRACE_TRACEME** Performs checks and sets PT_PTRACED.* Should be used by all ptrace implementations for PTRACE_TRACEME.*/
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 字段的为PT_PTRACED:
/** Ptrace flags** The owner ship rules for task->ptrace which holds the ptrace* flags is simple. When a task is running it owns it's task->ptrace* flags. When the a task is stopped the ptracer owns task->ptrace.*/#define PT_PTRACED 0x00000001
struct task_struct {unsigned int ptrace;
}
current->ptrace = PT_PTRACED;
接下来子进程执行exec系列的函数调用来执行程序,exec系统调用请请参考:
Linux 进程启动 execve 系统调用内核源码解析
这里只介绍和调试相关的部分:
SYSCALL_DEFINE3(execve,const char __user *, filename,const char __user *const __user *, argv,const char __user *const __user *, envp)
{struct filename *path = getname(filename);int error = PTR_ERR(path);if (!IS_ERR(path)) {error = do_execve(path->name, argv, envp);putname(path);}return error;
}
do_execve()-->do_execve_common()-->search_binary_handler()-->ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
int search_binary_handler(struct linux_binprm *bprm)
{//获取父进程的pidpid_t old_vpid;rcu_read_lock();old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));rcu_read_unlock();//检查是否处于PT_PTRACED,处于PT_PTRACED状态ptrace_event(PTRACE_EVENT_EXEC, old_vpid);}
/*** ptrace_event - possibly stop for a ptrace event notification* @event: %PTRACE_EVENT_* value to report* @message: value for %PTRACE_GETEVENTMSG to return** Check whether @event is enabled and, if so, report @event and @message* to the ptrace parent.** Called without locks.*/
static inline void ptrace_event(int event, unsigned long message)
{if (unlikely(ptrace_event_enabled(current, event))) {current->ptrace_message = message;ptrace_notify((event << 8) | SIGTRAP);} else if (event == PTRACE_EVENT_EXEC) {/* legacy EXEC report via SIGTRAP */if ((current->ptrace & (PT_PTRACED|PT_SEIZED)) == PT_PTRACED)send_sig(SIGTRAP, current, 0);}
}
当一个进程执行 exec 系统调用的时候,会调用ptrace_event函数判断是否处于自己是否PT_PTRACED状态,即调试状态,如果处于PT_PTRACED状态,则给自己发送SIGTRAP信号,当一个进程收到SIGTRAP信号时,就会调用do_signal()内核函数对该信号进行处理:
当一个进程收到SIGTRAP信号后,如果当前进程处于被跟踪状态,即:
struct task_struct {unsigned int ptrace;
}current->ptrace && signr != SIGKILL
(1)设置其进程状态为TASK_TRACED:
struct task_struct {volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
}
#define set_current_state(state_value) \set_mb(current->state, (state_value))set_current_state(TASK_TRACED);
(2)给父进程即调试器发送SIGCHLD信号:
do_notify_parent_cldstop()
static void do_notify_parent_cldstop(struct task_struct *tsk,bool for_ptracer, int why)
{__group_send_sig_info(SIGCHLD, &info, parent);/** Even if SIGCHLD is not generated, we must wake up wait4 calls.*/__wake_up_parent(tsk, parent);
}
父进程收到SIGCHLD信号就知道子进程处于停止和被跟踪状态了,这样父进程就可以对子进程进行各种调试操作了。
SIGCHLD是一个由子进程发送给父进程的信号,用于通知父进程子进程的状态变化。具体来说,当一个子进程终止或停止时,它会向父进程发送SIGCHLD信号。
这里是指一个子进程停止时会向父进程发送SIGCHLD信号。
一个子进程发送SIGCHLD信号给父进程后,然后调用__wake_up_parent 唤醒父进程,在开头的测试例程中我们可以看到父进程 调用了waitpid函数,处于阻塞状态,因此这里唤醒父进程,这样父进程就可以开始调试子进程了。
void __wake_up_parent(struct task_struct *p, struct task_struct *parent)
{__wake_up_sync_key(&parent->signal->wait_chldexit,TASK_INTERRUPTIBLE, 1, p);
}
__wake_up_sync_key 函数用于唤醒等待队列中的进程。具体来说,通过调用该函数,父进程的等待队列被唤醒,以便其可以继续执行。
/*** __wake_up_sync_key - wake up threads blocked on a waitqueue.* @q: the waitqueue* @mode: which threads* @nr_exclusive: how many wake-one or wake-many threads to wake up* @key: opaque value to be passed to wakeup targets** The sync wakeup differs that the waker knows that it will schedule* away soon, so while the target thread will be woken up, it will not* be migrated to another CPU - ie. the two threads are 'synchronized'* with each other. This can prevent needless bouncing between CPUs.** On UP it can prevent extra preemption.** It may be assumed that this function implies a write memory barrier before* changing the task state if and only if any tasks are woken up.*/
void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode,int nr_exclusive, void *key)
{unsigned long flags;int wake_flags = WF_SYNC;......spin_lock_irqsave(&q->lock, flags);__wake_up_common(q, mode, nr_exclusive, wake_flags, key);spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL_GPL(__wake_up_sync_key);
__wake_up_sync_key 函数用于唤醒在等待队列上被阻塞的线程。
(3)发出调度请求,主动让出CPU:
freezable_schedule();
二、dmoe测试
2.1 hello.s
.section .data
message:.string "Hello, World!\n"
len = . - message.section .text
.globl _start
_start:# 调用 write() 函数输出 "Hello, World!"mov $1, %rax # 系统调用号为 1 表示 write()mov $1, %rdi # 文件描述符为 1 表示标准输出lea message(%rip), %rsi # 输出的字符串地址mov $len, %rdx # 输出的字符串长度syscall # 调用系统调用# 调用 exit() 函数退出程序mov $60, %rax # 系统调用号为 60 表示 exit()xor %rdi, %rdi # 返回值为 0syscall # 调用系统调用
可以看到一共有八条指令。
这段汇编代码是在标准输出上输出 “Hello, World!”,然后退出程序:
as -o hello.o hello.s
ld -o hello hello.o
# ./hello
Hello, World!
2.2 demo演示
/* Code sample: using ptrace for simple tracing of a child process.
**
** Note: this was originally developed for a 32-bit x86 Linux system; some
** changes may be required to port to x86-64.
**
** Eli Bendersky (https://eli.thegreenplace.net)
** This code is in the public domain.
*/
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <signal.h>
#include <syscall.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <unistd.h>
#include <errno.h>/* Print a message to stdout, prefixed by the process ID
*/
void procmsg(const char* format, ...)
{va_list ap;fprintf(stdout, "[%d] ", getpid());va_start(ap, format);vfprintf(stdout, format, ap);va_end(ap);
}void run_target(const char* programname)
{procmsg("target started. will run '%s'\n", programname);/* Allow tracing of this process */if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {perror("ptrace");return;}/* Replace this process's image with the given program */execl(programname, programname, 0);
}void run_debugger(pid_t child_pid)
{int wait_status;unsigned icounter = 0;procmsg("debugger started\n");/* Wait for child to stop on its first instruction */wait(&wait_status);while (WIFSTOPPED(wait_status)) {icounter++;struct user_regs_struct regs;ptrace(PTRACE_GETREGS, child_pid, 0, ®s);unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.rip, 0);procmsg("icounter = %u. rip = 0x%08x. instr = 0x%08x\n",icounter, regs.rip, instr);/* Make the child execute another instruction */if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {perror("ptrace");return;}/* Wait for child to stop on its next instruction */wait(&wait_status);}procmsg("the child executed %u instructions\n", icounter);
}int main(int argc, char** argv)
{pid_t child_pid;if (argc < 2) {fprintf(stderr, "Expected a program name as argument\n");return -1;}child_pid = fork();if (child_pid == 0)run_target(argv[1]);else if (child_pid > 0)run_debugger(child_pid);else {perror("fork");return -1;}return 0;
}
参考资料
https://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1/