当前位置: 首页 > article >正文

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串口,对被调试程序进行一系列调试操作。


http://www.kler.cn/a/585970.html

相关文章:

  • 解决Docker Desktop中ext4.vhdx文件过大的问题
  • 【Java 进阶实战】一 学习成果检验
  • 《大语言模型》学习笔记(一)
  • 初探大模型开发:使用 LangChain 和 DeepSeek 构建简单 Demo
  • Apache Doris中都用了哪些开发语言,编译过程中用到了哪些编译器,以及用到了哪些成熟的技术框架
  • 支持Linux、鸿蒙的工业AIOT智能终端在钢铁厂的应用
  • JavaScript性能优化实战:从8s到0.8s的极致提升
  • 钉钉(excel)能让表格中不是‘北京’的字符串自动加亮显示(方便查看)以及隔行填充严颜色是斑马色(方便查看)嘛
  • Vue 3 事件总线详解:构建组件间高效通信的桥梁
  • FPGA初级项目10——基于SPI的DAC芯片进行数模转换
  • Vlog 片头制作
  • Excel 数据转换为SQL语句
  • 通过Geopandas进行地理空间数据可视化
  • 分布式系统日志排查综合场景
  • 第P7周:马铃薯病害识别(VGG-16复现)
  • 今日所学——Word批量给图片插入题注,设置题注起始编号
  • Linux第三次作业
  • 行为模式---状态模式
  • 目标跟踪之DeepSort算法(4)
  • 从DeepSeek探讨大语言模型在建筑及能源行业的应用趋势和技术方法(52页PPT)