目录
(代码:linux 6.3.1,架构:arm64)
One look is worth a thousand words. —— Tess Flanders
相关链接:
Linux uprobe 图文详解(二)uretprobe实现原理
Linux uprobe 图文详解(三)fork/spawn/exec针对uprobe的处理
Linux uprobe 图文详解(四)uretprobe 针对 longjmp/setjmp 的处理
一、uprobe基本介绍
uprobe(user space probe),是Linux内核提供的一种动态探测用户态代码的机制,用于debug、动态跟踪、性能分析。被uprobe打桩的函数,在其函数入口、函数返回时都会执行对应的钩子函数,可以用于获取函数入参、返回值、执行时长等信息。

我们知道,linux中一个ELF文件,可以对应多个可执行程序,通俗的讲,就是同一个ELF可执行程序,可以加载运行多次,在系统中可以同时存在同一个ELF可执行文件对应的多个独立的进程。 因此,uprobe可以针对某个ELF文件对应的所有可执行程序,进行实时探测打桩。
以下介绍一下uprobe的基本使用方式,假设有一个ELF程序(名为test),其代码如下所示:

该ELF文件反汇编后,func_1、func_2函数对应的汇编指令如下:

在func_1、func_2入口处,放置探测点,然后后台运行程序:(其中0x620、0x644分别是func_1、func_2在test文件中的offset)

更详细的uprobe使用介绍,可以参考以下链接:
Uprobe-tracer: Uprobe-based Event Tracing — The Linux Kernel documentation
二、uprobe打桩时如何确定offset
相信细心的小伙伴看完上面的示例后会有一个疑惑:objdump反汇编出来的函数func_1的地址是 0x400620,而为何 uprobe打桩时 设置的 函数func_1 在ELF文件中的 offset 却是 0x620 ?
直接上结论:
ELF文件objdump出来的所有汇编地址,都是编译地址(也可以简单理解为程序运行时的虚拟地址),并非是函数在文件中的offset。一个ELF文件,可能是静态链接 或 动态链接的,对于这两种场景,函数在ELF文件中的offset计算方式有所不同。
1)对于动态链接的ELF文件
对于动态链接的可执行文件,其函数相对ELF文件的偏移,就是 objdump -d 出来的地址。

2)对于静态链接的ELF文件
对于静态链接的可执行文件,其函数相对ELF文件的偏移,需要通过 objdump -d/-t 和 proc/task/maps 两者的信息来共同确认。


因为是静态编译,所以objdump出来的函数地址,就是运行时在虚拟内存中的地址(编译地址),所以需要减去代码段的起始地址。

三、uprobe 注册流程

uprobe注册的过程中,有几个关键点:
1)根据page cache找到所有可被打桩的进程信息(build_map_info函数)
根据 ELF文件 对应inode对象中的i_mmap成员,以及打桩函数在ELF文件中的offset,定位到当前系统中哪些进程地址空间中,映射了该打桩函数,并将所有映射了该函数的进程信息,记录到一系列map_info对象中;
/*【build_map_info函数功能】
* 收集map_info信息, 记录相应进程的mm和对应虚拟地址vaddr
*/
info = build_map_info(struct address_space *mapping = uprobe->inode->i_mapping, uprobe->offset, is_register) {
unsigned long pgoff = offset >> PAGE_SHIFT
struct vm_area_struct *vma
struct map_info *curr = NULL
struct map_info *prev = NULL
struct map_info *info
vma_interval_tree_foreach(vma, &mapping->i_mmap, pgoff, pgoff) {
if (!valid_vma(vma, is_register))
continue
if (!prev && !more) {
prev = kmalloc(sizeof(struct map_info), GFP_NOWAIT | __GFP_NOMEMALLOC | __GFP_NOWARN)
if (prev)
prev->next = NULL
}
if (!mmget_not_zero(vma->vm_mm))
continue
info = prev
prev = prev->next
info->next = curr
curr = info
info->mm = vma->vm_mm // 记录mm信息和vaddr信息
info->vaddr = offset_to_vaddr(vma, offset) // 计算文件偏移offset的内容在该vma中对应的虚拟地址 !!!!!
}
return curr // 返回收集好的map_info链表头结点地址
}

这一过程,主要依赖page cache机制,每个被映射的文件都会有一个唯一的address_space对象与之对应,表示文件页缓存(代表一个文件在内存中的映射),其中“i_mmap字段”是一个红黑树,用来保存这个文件的所有内存映射关系,每个节点对应一个进程的地址空间(指向一个vma对象)。
进程 VMA 挂载到 inode->address_space->i_mmap 树上的时机:在进程加载过程中,映射ELF可加载段到进程虚拟地址空间时,会将对应的进程vma挂载到文件页page cache的红黑树上。

2)filter筛选需要打桩的进程
在uprobe注册过程中,当找到所有映射了被打桩函数的进程并记录到map_info后,会根据consumer->filter()函数判断对应进程是否需要被uprobe打桩。这么做的目的是,仅针对需要探测的进程进行uprobe实时打桩,而不去影响那些不关注的进程。
举例说明:假设要对 write 这个用户态函数进行打桩,可能当前系统中大部分进程的虚拟地址空间中有映射了fork对应的代码段page cache,但是并非需要探测所有进程,比方仅对pid为100的进程中的write函数进行打桩,那么这里的consumer->filter()就派上用处了。
3)uprobe替换指令打桩的实现细节
通过给定的vaddr 和 进程mm_struct,获取对应的old_page,然后分配新的page,将old page内容拷贝给new page,最后替换new page中被打桩指令,并与 old page 进行交换。
set_swbp(&uprobe->arch, mm, vaddr) // Note: 该函数既用于安装软断点, 也用于restore original instruction
uprobe_write_opcode(auprobe, mm, vaddr, opcode = UPROBE_SWBP_INSN)
// 1) 通过给定的vaddr和进程mm_struct, 获取对应page指定数量的用户空间页面,并将其映射到内核空间.
get_user_pages_remote(mm, vaddr, 1, gup_flags, &old_page, &vma, NULL)
verify_opcode // 检查被探测指令地址上的内容是否为UPROBE_SWBP_INSN
// 2) 为vma对象分配一个匿名VMA对象,并将其与vma对象关联
anon_vma_prepare(vma)
// 3) 为指定的VMA分配一个页面并返回页面指针
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vaddr)
// 4) 将一个页面的uptodate标志位置为1,表示页面已经被更新
__SetPageUptodate(new_page)
// 5) 将一个页面的数据从旧页面复制到新页面
copy_highpage(new_page, old_page)
// 6) 用于将数据从内核空间复制到一个页面中, 即替换新页面中的vaddr上的指令
copy_to_page(new_page, vaddr, &opcode, UPROBE_SWBP_INSN_SIZE)
// 7) 交换vma对应的那个old page
__replace_page(vma, vaddr, old_page, new_page)
四、uprobe 生效原理

uprobe注册完毕后,说明所有需要被探测的用户进程中目标函数的入口指令,都已经被替换成BRK指令了。当用户程序执行到uprobe探测函数时,流程如上图所示。
大致流程与之前 kprobe系列文章 中介绍的原理类似,都是通过触发异常陷入内核,由内核的异常处理流程接管,并做相应处理。
需要注意的是,uprobe在一些实现细节上还是与kprobe有所不同:
1)首次触发断点异常后的处理细节
uprobe在第一次触发断点异常进入内核后执行完uprobe探测点上注册的handler并调整PC指向后,会给当前task置上TIF_SINGLESTEP标志,在任务从内核态返回用户态前夕,会将MDSCR_EL1寄存器置位DBG_MDSCR_SS。这么做的目的:为了让任务在用户态执行完一条完整指令后,立即由硬件触发单步异常陷入内核!(MDSCR_EL1设置单步的细节见下面)
2)指令槽的区别
对于kprobe而已,用于存放被探测指令的指令槽,是一张内核全局共享的指令槽页。而对于uprobe而言,其作用的是不同用户进程,我们知道在linux中不同用户进程的地址空间是互相隔离的。所以,uprobe对于每个被探测进程执行到打桩点触发异常时,都会为每个进程分配属于其地址空间的指令槽页。

// 分配一个用于xol的指令槽, 将被探测指令内容拷贝到指令槽中, 返回指令槽地址
xol_vaddr = xol_get_insn_slot(uprobe) {
/* 获取一个进程的xol_area, 若不存在则分配. 该area用于存放指令槽 */
struct xol_area *area = get_xol_area() {
struct mm_struct *mm = current->mm
if (!mm->uprobes_state.xol_area) {
/*【__create_xol_area函数功能】
* 为当前进程分配指令槽, 并为其分配指令槽的页, 并创建对应vma添加到进程mm中
*/
__create_xol_area(0) {
struct xol_area *area = kmalloc(sizeof(*area), GFP_KERNEL)
uprobe_opcode_t insn = UPROBE_SWBP_INSN
area->bitmap = kcalloc(BITS_TO_LONGS(UINSNS_PER_PAGE), sizeof(long), GFP_KERNEL)
/*
* 指令槽对应的物理页, 是提前分配好的,只是没有与vma做映射,当访问到指令槽虚拟地址时,
* 会触发page_fault,在page_fault处理流程中做映射 !!!!
*/
area->xol_mapping.pages = area->pages
area->pages[0] = alloc_page(GFP_HIGHUSER)
area->vaddr = vaddr
arch_uprobe_copy_ixol(area->pages[0], 0, &insn, UPROBE_SWBP_INSN_SIZE) <<<<<< trampoline_vaddr
xol_add_vma(mm, area)
area->vaddr = get_unmapped_area
vma = _install_special_mapping
__install_special_mapping(mm, addr, len, vm_flags, void *priv = (void *)spec, const struct vm_operations_struct *ops = &special_mapping_vmops) {
vma = vm_area_alloc(mm)
vma->vm_start = addr
vma->vm_end = addr + len
vma->vm_flags = vm_flags | mm->def_flags | VM_DONTEXPAND | VM_SOFTDIRTY
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags)
vma->vm_ops = ops
vma->vm_private_data = priv
insert_vm_struct(mm, vma)
}
}
}
area = READ_ONCE(mm->uprobes_state.xol_area)
return area
}
xol_vaddr = xol_take_insn_slot(area) // 从area中搜寻一个空闲的slot
// 将xol page中备份的被探测指令内容复制到进程的地址空间, 以便执行out-of-line breakpoints
arch_uprobe_copy_ixol(area->pages[0], xol_vaddr, &uprobe->arch.ixol, sizeof(uprobe->arch.ixol))
return xol_vaddr
}
需要额外注意的点:“被探测进程uprobe指令槽的虚拟地址与物理地址映射时机”
指令槽的虚拟地址、物理地址映射实际是当第一次发生中断后,返回用户态,PC指向指令槽虚拟地址开始取指令执行时,触发page fault中进行映射的。
在第一次触发中断进入内核处理的流程中,会分配xol_area,其中会分配一个物理页,并保存到area->xol_mapping.pages中。
触发page fault后,内核中进行page fault流程中会调用vma->vm_ops->fault(即: special_mapping_vmops.special_mapping_fault)获取保存在area->xol_mapping中的物理页page。
六、MDSCR_EL1设置单步异常
实际上,在gdb中常用的step单步调试指令,在arm64中就是借助的MDSCR_EL1寄存器提供的单步调试能力!(这里做个小预告^_^:后续会更新gdb的各种底层原理)
MDSCR(Monitor Debug System Control Register)中的第0位,是Software step control bit,用于single step。

在每次进入内核(kernel_entry)的时候、以及从内核返回用户态(ret_to_user)的时候,检查任务的flag是否有TIF_SINGLESTEP,然后调用disable_step_tsk、enable_step_tsk去清除、设置MDSCR_EL1寄存器中的DBG_MDSCR_SS位。 (详见:DDI0487J_a_a-profile_architecture_reference_manual.pdf D2.12 Software Step exceptions)

七、代码实现
1、uprobe相关数据结构
struct uprobe_consumer {
int (*handler)(struct uprobe_consumer *self, struct pt_regs *regs);
int (*ret_handler)(struct uprobe_consumer *self,
unsigned long func,
struct pt_regs *regs);
bool (*filter)(struct uprobe_consumer *self,
enum uprobe_filter_ctx ctx,
struct mm_struct *mm);
struct uprobe_consumer *next;
};
struct uprobe {
struct rb_node rb_node /* node in the rb tree */
refcount_t ref
struct rw_semaphore register_rwsem /* 注册/注销的时候, 需要获取写信号量; 在调用handler,ret_handler的时候需要获取读信号量 */
struct rw_semaphore consumer_rwsem /* 保护对uprobe_consumers结构的读写访问与操作:
* 1)在add/del uprobe_consumer的时候write lock;
* 2)在处理异常filter_chain时read lock;
* 3)在prepare_uprobe的时候write lock, 是同步 "uprobe_mmap" 和 "uprobe_register/uprobe_unregister" 操作时,
* 对uprobe->flags和uprobe->arch.insn这两个变量读写操作的并发; */
struct list_head pending_list
struct uprobe_consumer *consumers
struct inode *inode /* Also hold a ref to inode */
loff_t offset
loff_t ref_ctr_offset
unsigned long flags
/*
* The generic code assumes that it has two members of unknown type
* owned by the arch-specific code:
*
* insn - copy_insn() saves the original instruction here for
* arch_uprobe_analyze_insn().
*
* ixol - potentially modified instruction to execute out of
* line, copied to xol_area by xol_get_insn_slot().
*/
struct arch_uprobe arch;
}
struct arch_uprobe {
union { // <<<< 联合体 !!! 存的是"被探测指令内容". register的时候用insn, 准备指令槽的时候用ixol
u8 insn[MAX_UINSN_BYTES];
u8 ixol[MAX_UINSN_BYTES];
};
struct arch_probe_insn api;
bool simulate;
};
2、注册uprobe
/*【uprobe_register】
* 函数功能: 注册uprobe, 将被探测指令替换为breakpoint
* 1) 分配一个uprobe对象, 若uprobes_tree中已存在同样inode:offset tuple的uprobe对象就直接返回该uprobe对象;
* 2) 根据文件inode->i_mapping->i_mmap, 收集指定偏移量的虚拟地址在哪些进程内存空间中有映射的信息map_info(即: 收集需要install breakpoint的所有进程VMA信息);
* 3) 根据收集到的map_info信息, 在所有用到inode对应文件的进程虚拟地址空间中install breakpoint, 并将被探测指令内容保存到uprobe->arch.insn中.
*/
uprobe_register(struct inode *inode, loff_t offset, struct uprobe_consumer *uc)
{
/* inode : the file in which the probe has to be placed.
* offset: offset from the start of the file.
* uc : information on howto handle the probe. */
__uprobe_register(inode, offset, loff_t ref_ctr_offset = 0, uc) {
if (!uc->handler && !uc->ret_handler) /* Uprobe must have at least one set consumer */
return -EINVAL
retry:
### 分配一个uprobe, 若已存在同样inode:offset的uprobe就直接返回对应uprobe
struct uprobe *uprobe = alloc_uprobe(inode, offset, ref_ctr_offset) {
uprobe = kzalloc(sizeof(struct uprobe), GFP_KERNEL)
uprobe->inode = inode
uprobe->offset = offset
uprobe->ref_ctr_offset = ref_ctr_offset
init_rwsem(&uprobe->register_rwsem)
init_rwsem(&uprobe->consumer_rwsem)
/* add to uprobes_tree, sorted on inode:offset */
cur_uprobe = insert_uprobe(uprobe) {
spin_lock(&uprobes_treelock)
u = __insert_uprobe(uprobe) {
node = rb_find_add(&uprobe->rb_node, &uprobes_tree, __uprobe_cmp) // 若已存在, 则插入失败并返回已存在的uprobe; 若不存在, 则插入成功, 返回NULL.
if (node)
return get_uprobe(__node_2_uprobe(node)) // ***** 若发现已存在同样uprobe, 将其ref加1 *****
/* get access + creation ref */
refcount_set(&uprobe->ref, 2) // 对于新分配的uprobe, 引用计数为2, 分别代表: access refcount, creation refcount
return NULL
}
spin_unlock(&uprobes_treelock)
return u
}
/* a uprobe exists for this inode:offset combination */
if (cur_uprobe) {
if (cur_uprobe->ref_ctr_offset != uprobe->ref_ctr_offset) { // 若已存在uprobe, 但其ref_ctr_offset与欲分配的uprobe's ref_ctr_offset不同, 则返回EINVAL
ref_ctr_mismatch_warn(cur_uprobe, uprobe)
put_uprobe(cur_uprobe)
kfree(uprobe)
return ERR_PTR(-EINVAL);
}
kfree(uprobe)
uprobe = cur_uprobe // 若已存在uprobe, 则释放新分配的uprobe, 将已存在的uprobe返回
}
return uprobe
}
down_write(&uprobe->register_rwsem) // 获取register操作的读写信号量
ret = -EAGAIN
if (likely(uprobe_is_active(uprobe))) {
consumer_add(uprobe, uc) { // 将uprobe_consumer头插到uprobe->consumers链表上
down_write(&uprobe->consumer_rwsem)
uc->next = uprobe->consumers
uprobe->consumers = uc
up_write(&uprobe->consumer_rwsem)
}
/* register_for_each_vma接口, 用于install/remove breakpoint */
ret = register_for_each_vma(uprobe, struct uprobe_consumer *new = uc) {
bool is_register = !!new
struct map_info *info
percpu_down_write(&dup_mmap_sem) <<<<<<<<<<<<<<<<<<<< 与fork->dup_mmap的时候互斥
### 收集map_info信息, 记录相应进程的mm和对应虚拟地址vaddr
info = build_map_info(struct address_space *mapping = uprobe->inode->i_mapping, uprobe->offset, is_register) {
unsigned long pgoff = offset >> PAGE_SHIFT
struct vm_area_struct *vma
struct map_info *curr = NULL
struct map_info *prev = NULL
struct map_info *info
vma_interval_tree_foreach(vma, &mapping->i_mmap, pgoff, pgoff) {
if (!valid_vma(vma, is_register))
continue
if (!prev && !more) {
prev = kmalloc(sizeof(struct map_info), GFP_NOWAIT | __GFP_NOMEMALLOC | __GFP_NOWARN)
if (prev)
prev->next = NULL
}
if (!mmget_not_zero(vma->vm_mm))
continue
info = prev
prev = prev->next
info->next = curr
curr = info
info->mm = vma->vm_mm // 记录mm信息和vaddr信息
info->vaddr = offset_to_vaddr(vma, offset) // 计算文件偏移offset的内容在该vma中对应的虚拟地址 !!!!!
}
return curr // 返回收集好的map_info链表头结点地址
}
### 根据收集到的map_info信息install breakpoint
while (info) {
struct mm_struct *mm = info->mm
mmap_write_lock(mm)
vma = find_vma(mm, info->vaddr)
if (!vma || !valid_vma(vma, is_register) || file_inode(vma->vm_file) != uprobe->inode)
goto unlock
if (vma->vm_start > info->vaddr || vaddr_to_offset(vma, info->vaddr) != uprobe->offset)
goto unlock
if (is_register) {
ret = consumer_filter(uc = new, ctx = UPROBE_FILTER_REGISTER, mm) {
return !uc->filter || uc->filter(uc, ctx, mm)
}
### 若consumer->filter非空, 或者consumer->filter返回非0值代表mm对应的进程满足consumer的过滤条件, 则在mm进程中安装断点指令 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
if (ret) {
###################################### uprobe安装软断点 ######################################
/*【install_breakpoint函数】
* 1) 调用prepare_uprobe, 记录被探测指令内容, 并对指令内容做相应检查;
* 2) 调用set_swbp, 将被探测指令地址上的指令内容替换为软断点
* 3) 若software breakpoint设置成功, 则清除mm->flags中的MMF_RECALC_UPROBES位 */
install_breakpoint(uprobe, mm, vma, unsigned long vaddr = info->vaddr) {
/*【prepare_uprobe函数】
* 1) 保存被探测指令内容到uprobe->arch.insn中;
* 2) 检测被探测指令是否为UPROBE_SWBP_INSN指令, 若是的话则返回-ENOTSUPP;
* 3) 分析ARM64下该被探测指令类型, 若需要simulate, 则根据指令类型提供相应的simulate_handler, 保存在uprobe->arch->api->handler中 */
ret = prepare_uprobe(uprobe, vma->vm_file, mm, vaddr) {
if (test_bit(UPROBE_COPY_INSN, &uprobe->flags)) // 检查UPROBE_COPY_INSN标志, 看探测指令是否已经备份
return ret
down_write(&uprobe->consumer_rwsem)
if (test_bit(UPROBE_COPY_INSN, &uprobe->flags)) // 再次检查UPROBE_COPY_INSN标志, 以防当前任务等待锁期间, 该标志被置位
goto out
ret = copy_insn(uprobe, file) { // file代表包含probed instruction的文件
struct address_space *mapping = uprobe->inode->i_mapping // 文件映射的地址空间
loff_t offs = uprobe->offset // 指令在文件中的偏移
void *insn = &uprobe->arch.insn // 用于备份探测指令内容的空间
int size = sizeof(uprobe->arch.insn)
__copy_insn(mapping, filp, insn, len, offs) { // 将文件映射的地址空间中被探测指令内容拷贝到insn中
### 通过文件映射的地址空间和偏移量, 获取指令所在的页面
struct page *page = read_mapping_page(mapping, offset >> PAGE_SHIFT, filp)
copy_from_page(page, vaddr = offset, dst = insn, len = nbytes) {
void *kaddr = kmap_atomic(page)
memcpy(dst, kaddr + (vaddr & ~PAGE_MASK), len)
kunmap_atomic(kaddr)
}
put_page(page)
}
}
if (ret)
goto out
ret = -ENOTSUPP
if (is_trap_insn((uprobe_opcode_t *)&uprobe->arch.insn)) // 若被探测指令就是UPROBE_SWBP_INSN指令, 则返回-ENOTSUPP
goto out
### 分析被探测指令类型, 若uprobe指令需要simulate执行, 则设置相应的simulate handler
ret = arch_uprobe_analyze_insn(struct arch_uprobe *auprobe = &uprobe->arch, mm, vaddr) {
if (mm->context.flags & MMCF_AARCH32) // 目前不支持 AARCH32 instruction probing
return -EOPNOTSUPP
else if (!IS_ALIGNED(addr, AARCH64_INSN_SIZE)) // 检查被探测指令是否4字节对齐 #define AARCH64_INSN_SIZE 4
return -EINVAL
probe_opcode_t insn = *(probe_opcode_t *)(&auprobe->insn[0])
ret = arm_probe_decode_insn(insn, &auprobe->api) {
if (aarch64_insn_is_steppable(insn))
return INSN_GOOD
if (aarch64_insn_is_bcond(insn))
api->handler = simulate_b_cond // <<<<<< 根据指令类型, 提供相应的 simulate handler
else if (aarch64_insn_is_cbz(insn) || aarch64_insn_is_cbnz(insn))
api->handler = simulate_cbz_cbnz
...
else
return INSN_REJECTED // Instruction cannot be stepped out-of-line and we don't (yet) simulate it.
return INSN_GOOD_NO_SLOT
}
switch (ret) {
case INSN_REJECTED:
return -EINVAL;
case INSN_GOOD_NO_SLOT:
auprobe->simulate = true
}
return 0
}
if (ret)
goto out
set_bit(UPROBE_COPY_INSN, &uprobe->flags) // 设置UPROBE_COPY_INSN标志, 代表探测指令已备份且分析过了
out:
up_write(&uprobe->consumer_rwsem)
return ret
}
### 无需继续替换uprobe点内存页的情况 <<<<<<<<<<<<<<<<<<<<<<<<<<
/* Case1: uprobe->flags已经置上UPROBE_COPY_INSN位, 代表指令已经替换过了 */
if (ret)
return ret
### 若当前进程未添加过uprobe点, 则给进程的mm设置MMF_HAS_UPROBES标志位
bool first_uprobe = !test_bit(MMF_HAS_UPROBES, &mm->flags)
if (first_uprobe)
set_bit(MMF_HAS_UPROBES, &mm->flags)
### Store breakpoint at a given address.
### For mm @mm, store the breakpoint instruction at @vaddr.
### @mm: the probed process address space.
### @vaddr: the virtual address to insert the opcode.
ret = set_swbp(struct arch_uprobe *auprobe = &uprobe->arch, mm, vaddr) {
/*【uprobe_write_opcode函数】
* - write the opcode at a given virtual address.
* @auprobe: arch specific probepoint information.
* @mm : the probed process address space.
* @vaddr : the virtual address to store the opcode.
* @opcode : opcode to be written at @vaddr.
*
* [Note]: 该函数既用于安装软断点, 也用于restore original instruction
*
*【实现逻辑】
* 1) 检测被探测指令内容是否为UPROBE_SWBP_INSN;
* 2) 分配一个新的page;
* 3) 将原始page的内容拷贝到新分配的new_page中, 并将new_page中vaddr处内容替换为UPROBE_SWBP_INSN;
* 4) 将old_page替换成new_page
*
* For arm64, UPROBE_SWBP_INSN is BRK64_OPCODE_UPROBES A.K.A: AARCH64_BREAK_MON | (UPROBES_BRK_IMM << 5) */
return uprobe_write_opcode(auprobe, mm, vaddr, uprobe_opcode_t opcode = UPROBE_SWBP_INSN) {
struct page *old_page, *new_page
is_register = is_swbp_insn(&opcode)
uprobe = container_of(auprobe, struct uprobe, arch)
### 通过给定的vaddr和进程mm_struct, 获取对应page指定数量的用户空间页面,并将其映射到内核空间.
### 若执行成功,则会将获取到的页面指针存储到pages数组中,并将与这些页面相关的虚拟内存区域信息存储到vmas指针中
get_user_pages_remote(mm, vaddr, 1, gup_flags, &old_page, &vma, NULL)
### 检查被探测指令地址上的内容是否为UPROBE_SWBP_INSN, 对于注册的场景下若目标页上的指令已经是brk指令了就直接返回不做页替换了
ret = verify_opcode(struct page *page = old_page, vaddr, uprobe_opcode_t *new_opcode = &opcode) {
uprobe_opcode_t old_opcode
bool is_swbp
copy_from_page(page, vaddr, &old_opcode, UPROBE_SWBP_INSN_SIZE) // 获取old_page vaddr地址上的指令内容
is_swbp = is_swbp_insn(&old_opcode)
if (is_swbp_insn(new_opcode)) // Case1: 针对install breakpoint的场景
if (is_swbp) // 若new_opcode为UPROBE_SWBP_INSN, 且old_page vaddr上内容也为UPROBE_SWBP_INSN
return 0
else // Case2: 针对remove breakpoint的场景
if (!is_swbp) // 若new_opcode不是UPROBE_SWBP_INSN, 且old_page vaddr上内容也不是UPROBE_SWBP_INSN
return 0
return 1
}
if (ret <= 0)
goto put_old
### 为vma对象分配一个匿名VMA对象,并将其与vma对象关联
anon_vma_prepare(vma)
__anon_vma_prepare(vma) {
struct anon_vma *anon_vma
anon_vma = find_mergeable_anon_vma(vma)
if (!anon_vma)
anon_vma = anon_vma_alloc()
vma->anon_vma = anon_vma
}
### 为指定的VMA分配一个页面并返回页面指针
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vaddr)
### 将原始page的内容拷贝到新分配的new_page中, 并将new_page中vaddr处内容替换为UPROBE_SWBP_INSN
__SetPageUptodate(new_page) // 将一个页面的uptodate标志位置为1,表示页面已经被更新
copy_highpage(new_page, old_page) // 将一个页面的数据从旧页面复制到新页面
copy_to_page(new_page, vaddr, &opcode, UPROBE_SWBP_INSN_SIZE) //
### 将old_page替换成new_page
__replace_page(vma, vaddr, old_page, new_page)
put_page(old_page)
}
}
if (!ret)
clear_bit(MMF_RECALC_UPROBES, &mm->flags) // 若软断点设置成功, 则清除MMF_RECALC_UPROBES标志, 表示探针不需要重新计算
else if (first_uprobe)
clear_bit(MMF_HAS_UPROBES, &mm->flags) // 若软断点设置失败, 且这是第一个探针, 则将MMF_HAS_UPROBES标志清0
return ret
}
}
} else if (test_bit(MMF_HAS_UPROBES, &mm->flags)) {
### 只有当该uprobe上的所有consumer都注册了filter, 且所有filter过滤mm时, 都发现mm不满足条件时, 才移除断点
if (!filter_chain(uprobe,UPROBE_FILTER_UNREGISTER, mm))
err |= remove_breakpoint(uprobe, mm, info->vaddr)
}
unlock:
mmap_write_unlock(mm)
free:
mmput(mm)
info = free_map_info(info) {
struct map_info *next = info->next
kfree(info)
return next
}
} // while (info)
out:
percpu_up_write(&dup_mmap_sem);
return err
}
if (ret)
__uprobe_unregister(uprobe, uc)
}
up_write(&uprobe->register_rwsem) // 释放register操作的读写信号量
put_uprobe(uprobe) // ?????????
refcount_dec_and_test(&uprobe->ref)
if (unlikely(ret == -EAGAIN))
goto retry
return ret
}
}
3、第一次uprobe断点异常触发的处理
########################################## 第一次触发软断点时的处理逻辑 ##########################################
/* 1) 判断断点异常类型, 发现是UPROBES_BRK_IMM, 调用对应处理函数uprobe_breakpoint_handler, 将current_thread_info置位TIF_UPROBE
* 2) 调用exit_to_user_mode, 准备从异常处理中返回, 然后检查thread_flags, 发现存在_TIF_UPROBE位, 调用uprobe_notify_resume进行处理
* 3) 清除当前任务flags的TIF_UPROBE位
* 4) 调用handle_swbp函数, 该函数主要做以下处理:
* 4.1) 遍历执行触发异常对应的uprobe上注册的所有consumer->handler
* 4.2) 若注册的consumer中有注册ret_handler, 则在函数返回点打上断点
* 4.3) 若被探测指令需要simulate执行, 则执行simulate该指令相应的handler, 执行完毕后直接返回
* 4.4) 若被探测指令不需要simulate执行, 则准备需要单步执行的被探测指令, 分配指令槽, 指令槽中存入被探测指令内容, 将PC执行指令槽
* 4.5) 将对应uprobe记录到当前任务的uprobe_task->active_uprobe中, 并设置TIF_SINGLESTEP flag, 同时将uprobe_task->state设为UTASK_SSTEP
* 5) 调用ret_to_user并检查当前任务是否需要enable single step(即: 是否置位TIF_SINGLESTEP), 若需要则将 MDSCR_EL1寄存器置上DBG_MDSCR_SS位
* 6) 回到用户态继续执行, 此时若分配了指令槽的话, PC指向指令槽
*/
##################################################################################################################
entry_handler {
el0t_64_sync_handler {
unsigned long esr = read_sysreg(esr_el1)
case ESR_ELx_EC_BRK64:
el0_dbg(regs, esr) {
enter_from_user_mode(regs)
do_debug_exception(far, esr, regs) {
inf->fn(addr_if_watchpoint, esr, regs)
A.K.A
brk_handler
call_break_hook
if ((comment & ~hook->mask) == hook->imm)
fn = hook->fn
fn(regs, esr)
A.K.A
uprobe_breakpoint_handler {
/* uprobe_pre_sstep_notifier gets called from interrupt context as part of
* notifier mechanism. Set TIF_UPROBE flag and indicate breakpoint hit. */
uprobe_pre_sstep_notifier(regs) {
set_thread_flag(flag = TIF_UPROBE) // <<<< 将task_struct flag置上TIF_UPROBE位, 代表uprobe软断点已触发
set_ti_thread_flag(current_thread_info(), flag)
}
}
}
local_daif_restore(DAIF_PROCCTX)
exit_to_user_mode{
prepare_exit_to_user_mode(struct pt_regs *regs) {
local_daif_mask()
flags = READ_ONCE(current_thread_info()->flags)
if (unlikely(flags & _TIF_WORK_MASK)) {
do_notify_resume(struct pt_regs *regs, unsigned long thread_flags = flags) {
local_daif_restore(DAIF_PROCCTX)
if (thread_flags & _TIF_UPROBE) {
uprobe_notify_resume(struct pt_regs *regs) {
clear_thread_flag(TIF_UPROBE)
struct uprobe_task *utask = current->utask
if (utask && utask->active_uprobe)
handle_singlestep(utask, regs) // 第二次单步异常会走这条分支
//...Detail see below
else {
/* Run handler and ask thread to singlestep
* Ensure all non-fatal signals cannot interrupt thread while it singlesteps.
*【handle_swbp函数逻辑】
* 1) 获取触发软断点异常的指令地址
* 2) 根据异常地址找到对应vma, 并获取该vma对应的文件inode, 以及异常指令地址在文件中的偏移offset
* 3) 根据[inode,offset] tuple, 找到对应的uprobe对象
* 4) 为当前任务分配uprobe_task object
* 5) 遍历执行uprobe上挂载的所有consumer->handler, 并检查是否注册了consumer->ret_handler
* 6) 若第4步中发现有注册consumer->ret_handler, 则在被探测函数返回点打上断点
* 7) 若被探测指令需要simulate执行, 则执行simulate该指令相应的handler, 执行完毕后直接返回, 不再执行pre_ssout
* 8) 若被探测指令不需要simulate执行, 则调用pre_ssout, 准备需要单步执行的被探测指令
* 8.1) 分配一个用于xol的指令槽, 将被探测指令内容拷贝到指令槽中, 并返回指令槽地址
* 8.2) 将指令槽地址和触发第一次异常地址保存到当前任务的uprobe_task中的xol_vaddr和vaddr上
* 8.3) 准备uprobe探测点的软单步, 将任务PC指向utask->xol_vaddr, 并设置TIF_SINGLESTEP flag
* 8.4) 设置当前任务uprobe_task的状态信息(active_uprobe、state置为UTASK_SSTEP)
* 9) 从异常处理返回时, 检查当前任务是否需要enable single step, 若需要则将MDSCR_EL1寄存器置上DBG_MDSCR_SS位
*/
handle_swbp(regs) { // 第一次软断点异常会走这条分支
unsigned long bp_vaddr
int is_swbp
### step1) 获取触发软断点的指令地址
bp_vaddr = uprobe_get_swbp_addr(regs) {
return instruction_pointer(regs)
}
### 获取trampoline函数地址
trampoline_vaddr = get_trampoline_vaddr() {
struct xol_area *area = READ_ONCE(current->mm->uprobes_state.xol_area)
if (area)
trampoline_vaddr = area->vaddr
return trampoline_vaddr
}
if (bp_vaddr == trampoline_vaddr) { <<<<<<<<<<<<<<<<<< 处理uretprobe执行到被探测返回点的情况
ret = handle_trampoline(regs) {
/* Detail see below */
}
return ret
}
### step2/step3) 根据触发软断点指令地址, 找出对应uprobe
uprobe = find_active_uprobe(bp_vaddr, &is_swbp) {
struct mm_struct *mm = current->mm
struct vm_area_struct *vma = vma_lookup(mm, bp_vaddr)
if (vma) {
// 判断vma是否是一个executable vma
if (valid_vma(vma, false)) {
struct inode *inode = file_inode(vma->vm_file) // 获取该vma对应的文件inode
loff_t offset = vaddr_to_offset(vma, bp_vaddr) // 获取被探测指令在文件中的偏移
return ((loff_t)vma->vm_pgoff << PAGE_SHIFT) + (vaddr - vma->vm_start)
### 根据inode:offset tuple,在uprobes_tree中查找对应的uprobe
uprobe = find_uprobe(inode, offset) {
spin_lock(&uprobes_treelock)
uprobe = __find_uprobe(inode, offset) {
struct __uprobe_key key = {
.inode = inode,
.offset = offset,
};
struct rb_node *node = rb_find(&key, &uprobes_tree, __uprobe_cmp_key)
if (node)
return get_uprobe(__node_2_uprobe(node))
return NULL
}
spin_unlock(&uprobes_treelock)
return uprobe
}
}
if (!uprobe) {
*is_swbp = is_trap_at_addr(mm, bp_vaddr) {
}
}
} else {
*is_swbp = -EFAULT
}
return uprobe
}
if (!uprobe) {
if (is_swbp > 0)
force_sig(SIGTRAP) /* No matching uprobe; signal SIGTRAP. */
else
instruction_pointer_set(regs, bp_vaddr)
}
instruction_pointer_set(regs, bp_vaddr) // change it in advance for ->handler() and restart
### step4) Allocate a uprobe_task object for the task if necessary
get_utask() {
if (!current->utask)
current->utask = kzalloc(sizeof(struct uprobe_task), GFP_KERNEL)
return current->utask
}
### step5) 遍历执行所有consumer->handler, 并根据是否注册了consumer->ret_handler, 决定是否要在被探测函数返回点打断点
handler_chain(uprobe, regs) {
struct uprobe_consumer *uc
int remove = UPROBE_HANDLER_REMOVE
bool need_prep = false; /* prepare return uprobe, when needed */
down_read(&uprobe->register_rwsem)
### 遍历uprobe上注册的所有consumer, 执行consumer->handler, 并检查consumer是否注册了ret_handler
for (uc = uprobe->consumers; uc; uc = uc->next) {
int rc = 0
if (uc->handler) {
rc = uc->handler(uc, regs) // <<<<<<< 执行用户注册在uprobe探测点上的所有钩子函数 <<<<<<<
}
if (uc->ret_handler)
need_prep = true
remove &= rc
}
### step6) 若consumer注册了ret_handler, 则在被探测函数的返回点加上breakpoint
if (need_prep && !remove)
prepare_uretprobe(uprobe, regs) { /* put bp at return */
/* Detail see content4 */
}
### 若handler返回非0, 移除uprobe
if (remove && uprobe->consumers) {
unapply_uprobe(uprobe, current->mm)
}
up_read(&uprobe->register_rwsem)
}
### step7) 若被探测指令需要simulate执行, 则执行simulate该指令相应的handler, 并返回true, 代表需要跳过单步;
### 否则返回false, 代表不需要跳过单步
ret = arch_uprobe_skip_sstep(struct arch_uprobe *auprobe = &uprobe->arch, regs) {
if (!auprobe->simulate)
return false
insn = *(probe_opcode_t *)(&auprobe->insn[0])
addr = instruction_pointer(regs)
### 模拟执行被探测指令
if (auprobe->api.handler)
auprobe->api.handler(insn, addr, regs)
return true
}
if (ret) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 模拟单步执行探测指令 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
goto out
### step8) 若被探测指令不需要simulate运行, 则准备需要单步执行的被探测指令
pre_ssout(uprobe, regs, bp_vaddr) {
struct uprobe_task *utask = get_utask() {
if (!current->utask)
current->utask = kzalloc(sizeof(struct uprobe_task), GFP_KERNEL)
return current->utask
}
### step8.1) 分配一个用于xol的指令槽, 将被探测指令内容拷贝到指令槽中, 返回指令槽地址
xol_vaddr = xol_get_insn_slot(uprobe) {
/* 获取一个进程的xol_area, 若不存在则分配. 该area用于存放指令槽 */
struct xol_area *area = get_xol_area() {
struct mm_struct *mm = current->mm
if (!mm->uprobes_state.xol_area) {
__create_xol_area(0) { <<<<<<<<<<<<<<<<<<< 指令槽内存分配 Detail see below!!!
struct xol_area *area = kmalloc(sizeof(*area), GFP_KERNEL)
uprobe_opcode_t insn = UPROBE_SWBP_INSN
area->bitmap = kcalloc(BITS_TO_LONGS(UINSNS_PER_PAGE), sizeof(long), GFP_KERNEL)
area->xol_mapping.pages = area->pages
area->pages[0] = alloc_page(GFP_HIGHUSER)
area->vaddr = vaddr
arch_uprobe_copy_ixol(area->pages[0], 0, &insn, UPROBE_SWBP_INSN_SIZE) <<<<<< trampoline_vaddr
xol_add_vma(mm, area)
area->vaddr = get_unmapped_area
vma = _install_special_mapping
}
}
area = READ_ONCE(mm->uprobes_state.xol_area)
return area
}
xol_vaddr = xol_take_insn_slot(area) // 从area中搜寻一个空闲的slot
// 将xol page中备份的被探测指令内容复制到进程的地址空间, 以便执行out-of-line breakpoints
arch_uprobe_copy_ixol(area->pages[0], xol_vaddr, &uprobe->arch.ixol, sizeof(uprobe->arch.ixol))
return xol_vaddr
}
### step8.2) 将指令槽地址和触发第一次异常地址保存到当前任务的uprobe_task中
utask->xol_vaddr = xol_vaddr
utask->vaddr = bp_vaddr
### step8.3) 准备uprobe探测点的软单步, 将任务PC指向utask->xol_vaddr, 并设置TIF_SINGLESTEP flag, 在返回用户态时会MDSCR_EL1寄存器中DBG_MDSCR_SS位置1
arch_uprobe_pre_xol(struct arch_uprobe *auprobe = &uprobe->arch, regs) {
struct uprobe_task *utask = current->utask
current->thread.fault_code = UPROBE_INV_FAULT_CODE
/* Instruction points to execute xol */
instruction_pointer_set(regs, utask->xol_vaddr) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< PC指向指令槽 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
user_enable_single_step(current) {
struct thread_info *ti = task_thread_info(task)
### If task has TIF_SINGLESTEP, then set DBG_MDSCR_SS to MDSCR_EL1 during ret_to_user
test_and_set_ti_thread_flag(ti, TIF_SINGLESTEP)
set_regs_spsr_ss(task_pt_regs(task))
set_user_regs_spsr_ss(&(r)->user_regs)
regs->pstate |= DBG_SPSR_SS
}
}
### step8.4) 设置当前任务uprobe_task的状态信息
utask->active_uprobe = uprobe
utask->state = UTASK_SSTEP
}
out:
put_uprobe(uprobe)
}
}
}
}
local_daif_mask()
}
}
}
}
}
}
b ret_to_user
A.K.A
SYM_CODE_START_LOCAL(ret_to_user) {
ldr x19, [tsk, #TSK_TI_FLAGS] // re-check for single-step
### step9) 从异常处理返回时, 检查当前任务是否需要enable single step, 若需要则将MDSCR_EL1寄存器置上DBG_MDSCR_SS位
enable_step_tsk x19, x2
A.K.A
{
.macro enable_step_tsk, flgs, tmp
tbz \flgs, #TIF_SINGLESTEP, 9990f // ### If task has TIF_SINGLESTEP, then set DBG_MDSCR_SS to MDSCR_EL1
mrs \tmp, mdscr_el1
orr \tmp, \tmp, #DBG_MDSCR_SS // <<<<<<<<<<< 设置单步异常控制硬件寄存器
msr mdscr_el1, \tmp
}
kernel_exit 0 // It will restore general-purpose register
}
}
4、第二次uprobe单步异常触发的处理
######################################### 第二次触发单步异常时的处理逻辑 #########################################
/* 1) 判断断点异常类型, 发现是单步异常ESR_ELx_EC_SOFTSTP_LOW, 调用对应处理函数uprobe_single_step_handler
* 2) 将uprobe_task->state置为UTASK_SSTEP_ACK, 并将current_thread_info置位TIF_UPROBE
* 3) 调用exit_to_user_mode, 准备从异常处理中返回, 然后检查thread_flags, 发现存在_TIF_UPROBE位, 调用uprobe_notify_resume进行处理
* 4) 发现当前任务的uprobe_task->active_uprobe非空, 说明当前任务触发了软单步异常, 则调用handle_singlestep进行处理
* 5) 若state为UTASK_SSTEP_ACK, 则说明触发了uprobe探测点的第二次单步异常, 则将PC指向被探测指令地址+4的位置, 并清除thread_flag中的TIF_SINGLESTEP位(即: disable single step)
* 6) uprobe_task->active_uprobe置NULL, 并将uprobe_task->state置为UTASK_RUNNING
* 7) 回到用户态继续执行, 此时PC指向被探测指令地址的+4位置
*/
##################################################################################################################
entry_handler {
el0t_64_sync_handler {
unsigned long esr = read_sysreg(esr_el1)
case ESR_ELx_EC_SOFTSTP_LOW:
el0_dbg(regs, esr) {
do_debug_exception(far, esr, regs) {
const struct fault_info *inf = esr_to_debug_fault_info(esr)
inf->fn
A.K.A
single_step_handler(unsigned long unused = addr_if_watchpoint, unsigned int esr = esr, struct pt_regs *regs = regs) {
call_step_hook(regs, esr) {
list = user_mode(regs) ? &user_step_hook : &kernel_step_hook
/* single-step exception disables interrupt */
list_for_each_entry_rcu(hook, list, node) {
hook->fn(regs, esr)
A.K.A
uprobe_single_step_handler(regs, esr) { // ############# Call uprobe single step handler #############
struct uprobe_task *utask = current->utask
WARN_ON(utask && (instruction_pointer(regs) != utask->xol_vaddr + 4)) // check whether PC is correct
ret = uprobe_post_sstep_notifier(regs) {
struct uprobe_task *utask = current->utask
if (!current->mm || !utask || !utask->active_uprobe) /* task is currently not uprobed */
return 0
utask->state = UTASK_SSTEP_ACK
set_thread_flag(TIF_UPROBE)
return 1
}
if (ret)
return DBG_HOOK_HANDLED
}
}
}
}
}
exit_to_user_mode{
prepare_exit_to_user_mode(struct pt_regs *regs) {
local_daif_mask()
flags = READ_ONCE(current_thread_info()->flags)
if (unlikely(flags & _TIF_WORK_MASK)) {
do_notify_resume(struct pt_regs *regs, unsigned long thread_flags = flags) {
local_daif_restore(DAIF_PROCCTX)
if (thread_flags & _TIF_UPROBE) {
uprobe_notify_resume(struct pt_regs *regs) { // #################### 处理uprobe ####################
clear_thread_flag(TIF_UPROBE)
struct uprobe_task *utask = current->utask
if (utask && utask->active_uprobe) {
/*【handle_singlestep函数逻辑】
* 1) 读取当前任务uprobe_task的state成员
* 2) 若state为UTASK_SSTEP_ACK, 则说明触发了uprobe探测点的第二次单步异常, 则将PC指向被探测指令地址+4的位置, 并清除thread_flag中的TIF_SINGLESTEP位(即: disable single step)
* 3) 若state为UTASK_SSTEP_TRAPPED, 则将PC置为被探测指令地址, 并disable single step
* 4) 将uprobe_task->active_uprobe置NULL, 并将uprobe_task->state置为UTASK_RUNNING
* 5) 释放当前任务uprobe占用的指令槽
*/
handle_singlestep(utask, regs) { // 第二次单步异常会走这条分支
uprobe = utask->active_uprobe
if (utask->state == UTASK_SSTEP_ACK) {
arch_uprobe_post_xol(&uprobe->arch, regs) {
struct uprobe_task *utask = current->utask
WARN_ON_ONCE(current->thread.fault_code != UPROBE_INV_FAULT_CODE) // error check
### Instruction points to execute next to breakpoint address
instruction_pointer_set(regs, utask->vaddr + 4) <<<<<<<<<<<<<<<<<<<<<<<<<<<<< 恢复PC到被探测指令+4位置处
user_disable_single_step(current) {
clear_ti_thread_flag(task_thread_info(task), TIF_SINGLESTEP)
}
}
}
else if (utask->state == UTASK_SSTEP_TRAPPED) {
arch_uprobe_abort_xol(&uprobe->arch, regs) {
struct uprobe_task *utask = current->utask
### Task has received a fatal signal, so reset back to probbed address
instruction_pointer_set(regs, utask->vaddr)
user_disable_single_step(current)
}
}
put_uprobe(uprobe)
utask->active_uprobe = NULL
utask->state = UTASK_RUNNING
xol_free_insn_slot(current)
spin_lock_irq(¤t->sighand->siglock)
recalc_sigpending() { /* see uprobe_deny_signal() */
if (!recalc_sigpending_tsk(current) && !freezing(current))
clear_thread_flag(TIF_SIGPENDING)
}
spin_unlock_irq(¤t->sighand->siglock)
if (unlikely(err))
force_sig(SIGILL)
}
}
}
}
}
}
}
}
}
}
}
以上就是uprobe的所有实现原理,感谢观看!~
uprobe实现原理&spm=1001.2101.3001.5002&articleId=145214904&d=1&t=3&u=226546f6c89c43b49879a421c54383bd)
6783

被折叠的 条评论
为什么被折叠?



