目录
三、通过ptrace(PTRACE_POKETEXT) 设置软断点
(代码:linux 6.3.1,架构:arm64)
One look is worth a thousand words. —— Tess Flanders
相关链接:
linux ptrace 图文详解(二) PTRACE_TRACEME 跟踪程序
linux ptrace 图文详解(三) PTRACE_ATTACH 跟踪程序
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指令。