linux ptrace 图文详解(四) gdb设置软断点

目录

一、gdb 断点类型

二、通过proc文件系统 设置软断点

三、通过ptrace(PTRACE_POKETEXT) 设置软断点

四、软断点触发后的处理流程

五、代码实现

六、总结


 (代码:linux 6.3.1,架构:arm64)

One look is worth a thousand words.  —— Tess Flanders

相关链接:

linux ptrace 图文详解(一)基础介绍

linux ptrace 图文详解(二) PTRACE_TRACEME 跟踪程序

​​​​​​linux ptrace 图文详解(三) PTRACE_ATTACH 跟踪程序

linux ptrace 图文详解(五) gdb设置硬断点、观察点

linux ptrace 图文详解(六) gdb单步调试

linux ptrace 图文详解(七) gdb、strace跟踪系统调用

在阅读本文内容之前,我们先思考下以下几个问题:

1)gdb设置软断点,是否有数量限制?

2)目标程序触发软断点并暂停后,gdb是如何得知被调试程序是因为gdb设置的软断点停下来的?

一、gdb 断点类型

        gdb中,最常用的一个调试手段就是打断点。不过,断点分为两种类型:软断点、硬断点。

  •         软断点

        通过软件的方式,将断点处的指令内容替换成 brk异常指令,当程序执行到断点指令处,会触发一个同步中断,最终将被调试程序暂停下来;

  •         硬断点

        通过配置CPU相关硬断点寄存器,无需改变被调试程序代码段,当程序执行到断点指令地址处时,CPU会主动触发一个断点异常,最终让被调试程序暂停下来;

  •         两者对比
软断点(software breakpoints)硬断点(hardware breakpoints)
实现方式通过在目标代码段中替换断点指令通过设置CPU调试寄存器实现
数量限制无限制调试寄存器数量有限,取决于CPU架构
性能开销较高,每次触发断点时需要额外处理较低,由硬件直接支持
适用范围仅适用于代码断点适用于代码断点、数据断点
设置方式通过gdb在目标代码处插入brk指令通过gdb设置CPU调试寄存器
触发机制执行到brk断点指令时,触发同步断点异常CPU访问到指定地址时,触发断点异常
适用场景需要再多个位置设置断点,无需监控数据访问需要监控特定内存地址的数据访问

        接下来,笔者将介绍下gdb中两种设置软断点的方式,由于篇幅限制,硬断点的实现原理后续文章会再作介绍。

二、通过proc文件系统 设置软断点

        通过proc文件系统中,被调试进程的mem文件,设置软断点的流程如下:

        1)被调试程序被暂停后,唤醒父进程gdb;

        2)用户在gdb中设置软断点;

        3)gdb中打开目标调试程序在proc文件系统中对应的 /proc/PID/mem 文件;

        4) 通过mem文件,修改进程虚拟地址空间中软断点所在指令内容,设置为brk指令(关于brk指令的一些介绍,可以参考笔者之前的这篇文章);

三、通过ptrace(PTRACE_POKETEXT) 设置软断点

        通过ptrace(PTRACE_POKETEXT) 设置软断点:

        1)当目标调试程序暂停后,唤醒gdb父进程开始运行;

        2)用户通过break设置软断点;

        3)gdb中,调用ptrace系统调用,传递软断点在目标程序中的虚拟地址、以及替换的指令内容(brk指令);

        4)陷入内核后,最终会调用copy_to_user_page,替换目标程序代码段内存中指定断点位置处的指令,替换成brk指令;

        5)设置完软断点后,用户执行continue唤醒调试程序继续运行;

        至此,两种设置软断点的实现原理介绍完毕,接下来,笔者将分析软断点触发后的内核处理流程。

四、软断点触发后的处理流程

        被调试程序,执行到断点处后,触发断点异常后的处理流程、以及gdb一些相关流程如下:

        1)程序执行到软断点处,由于断点处的指令被替换为brk指令,于是触发同步异常陷入内核;

        2)CPU跳转到内核的同步异常入口;

        3)在同步异常的处理函数中,通过检查esr_el1.EC(exception class)字段,发现当前同步异常的类型是BRK64类型,于是调用el0_dbg函数处理debug类型的异常;

        4)进一步调用到brk_handler去处理brk指令触发的同步异常;

        5)在brk_handler中,会调用send_user_sigtrap函数,给当前被调试程序发送SIGTRAP信号;

        6)在发送信号的同时,还会将当前程序触发异常的地址,记录到siginfo中,最终该siginfo对象会被保存到被调试程序的task_struct对象中,待后续gdb使用!

        7)上述流程执行完后,就从同步异常处理流程中返回,并在返回用户态的前夕,发现当前被调试程序有待处理的SIGTRAP信号,于是调用do_signal进行处理;

        8)由于当前程序属于PT_PTRACED状态,于是会走ptarce_signal处理流程,发送信号给父进程gdb,并唤醒父进程;

        9)gdb被唤醒后,会调用ptrace(PTRACE_GETSIGINFO),获取目标调试程序内核task_struct对象中保存的siginfo,进一步获取到被调试程序触发同步异常(即:软断点)时的代码段地址,用于与gdb内部维护的断点信息作比较,由此确定调试程序是触发了设置的软断点后暂停下来的!(解释了文章开头的 问题2)

        10)确定目标程序是因为触发了设置好的软断点暂停后,gdb将控制权交给用户;

五、代码实现

1、gdb通过proc文件系统设置软断点

set_raw_breakpoint_at {
	the_target->insert_point (bp->raw_type, bp->pc, bp->kind, bp)
	A.K.A
	linux_process_target::insert_point (enum raw_bkpt_type type, CORE_ADDR addr, int size, raw_breakpoint *bp) {
		if (type == raw_bkpt_type_sw) {
			insert_memory_breakpoint (struct raw_breakpoint *bp) {
				unsigned char buf[MAX_BREAKPOINT_LEN]
				
				read_inferior_memory (bp->pc, buf, bp_size (bp))
				
				memcpy (bp->old_data, buf, bp_size (bp))
				
				err = the_target->write_memory (bp->pc, bp_opcode(bp), bp_size(bp))
				A.K.A
				linux_process_target::write_memory (CORE_ADDR memaddr = bp->pc, const unsigned char *myaddr = bp_opcode(bp), int len) {
					proc_xfer_memory (memaddr, nullptr, myaddr, len) {
						process_info *proc = current_process()
						
						int fd = proc->priv->mem_fd
						
						while (len > 0) {
							lseek (fd, memaddr, SEEK_SET)
							write (fd, writebuf, len)
							
							memaddr += bytes
							writebuf += bytes
							
							len -= bytes
						}
					}
				}
			}
		}
	}
}

2、gdb通过ptrace(PTRACE_POKETEXT) 设置软断点

ptrace_request(struct task_struct *child, long request, unsigned long addr, unsigned long data) {
	switch (request) {
	case PTRACE_POKETEXT:
		return generic_ptrace_pokedata(struct task_struct *tsk = child, addr, data) {
			copied = ptrace_access_vm(tsk, addr, void *buf = &data, len = sizeof(data), gup_flags = FOLL_FORCE | FOLL_WRITE) {
				struct mm_struct *mm
				
				mm = get_task_mm(tsk)
				
				if (!tsk->ptrace || (current != tsk->parent) || ((get_dumpable(mm) != SUID_DUMP_USER) && !ptracer_capable(tsk, mm->user_ns))) {
					mmput(mm)
					return 0
				}
				
				ret = __access_remote_vm(mm, addr, buf, len, gup_flags) {
					struct vm_area_struct *vma
					int write = gup_flags & FOLL_WRITE
					
					while (len) {
						get_user_pages_remote(mm, addr, 1, gup_flags, &page, &vma, NULL)
						bytes = len
						offset = addr & (PAGE_SIZE-1)
						
						maddr = kmap(page)
						if (write) {
							copy_to_user_page(vma, page, addr, maddr + offset, buf, bytes)		// <<<<<<< 设置软断点
							set_page_dirty_lock(page)
						} else {
							copy_from_user_page(vma, page, addr, buf, maddr + offset, bytes)
						}
						kunmap(page)
						put_page(page)
						
						len -= bytes
						buf += bytes
						addr += bytes
					}
					return buf - old_buf
				}//__access_remote_vm
				
				mmput(mm)
				return ret
			}//ptrace_access_vm
			
			return (copied == sizeof(data)) ? 0 : -EIO
		}//generic_ptrace_pokedata
	}
}

3、被调试程序触发软断点后的处理流程

// arch/arm64/kernel/entry-common.c
el0t_64_sync_handler {
	unsigned long esr = read_sysreg(esr_el1)
	switch (ESR_ELx_EC(esr))
	case ESR_ELx_EC_BRK64:
	el0_dbg {
		/* (1) 处理BRK64同步异常, 强制给自己发送SIGTRAP信号 */
		do_debug_exception {
			const struct fault_info *inf = esr_to_debug_fault_info(esr)
				return debug_fault_info + DBG_ESR_EVT(esr)
			inf->fn(addr_if_watchpoint, esr, regs)
			A.K.A
			brk_handler {
				call_break_hook(regs, esr) // used for kprobes
				
				if (user_mode(regs))
				send_user_sigtrap(si_code = TRAP_BRKPT)
					arm64_force_sig_fault(SIGTRAP, si_code, instruction_pointer(regs), "User debug trap")
						force_sig_fault(signo, code, (void __user *)far)
							force_sig_fault_to_task(sig, code, addr, ___ARCH_SI_IA64(imm, flags, isr), current) {
								struct kernel_siginfo info;

								clear_siginfo(&info);
								info.si_signo = sig;		// A.K.A: SIGTRAP
								info.si_errno = 0;
								info.si_code  = code;		// A.K.A: TRAP_BRKPT
								info.si_addr  = addr;		// A.K.A: far
								return force_sig_info_to_task(&info, t, HANDLER_CURRENT);
							}
			}
		}
		/* (2) 返回用户态前夕处理信号, 发送SIGCHLD给parent, 若parent处于block wait就wake它, 并将自己挂起 */
		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
									ptrace_stop
						}
				}
		}
	}
}

六、总结

        本文介绍了gdb中两种设置软断点的实现方式,以及程序触发软断点后的内核处理流程。

        软断点的本质:修改断点代码处的指令内容,替换成brk指令。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值