Skip to content
Stack & Ink
Go back

【Linux】 uprobe 源码分析

uprobe

之前介绍了 kprobe,它能够在内核的大部分函数或指令位置挂载回调函数,用于观测内核执行过程。uprobe 可以看作其对应的用户态版本,用于观测用户态进程。其核心机制与 kprobe 类似,都是通过将目标指令替换为断点指令,在异常处理路径中执行用户注册的回调。不同的是,uprobe 的探测点使用 inode + offset 来标识,而实际断点需要安装到该文件在各个进程中的用户态映射上。因此,相比 kprobe,uprobe 还需要额外处理 inode、VMA、进程地址空间、COW 以及 mmap 等问题。

Table of contents

Open Table of contents

1. 环境搭建

编写用户态程序:

#include <stdio.h>

__attribute__((noinline))
long demo_add(long a, long b)
{
	return a + b;
}

int main(void)
{
	long ret = demo_add(10, 20);

	printf("result = %ld\n", ret);
	return 0;
}

编译参数:

-g -Og -Wall -Wextra -Werror -fno-inline \
	-fno-omit-frame-pointer -fno-pie -no-pie

查看代码段偏移,段偏移是0x000000,段虚拟地址是0x400000

readelf -lW rootfs/usr/bin/uprobe_demo

Elf 文件类型为 EXEC (可执行文件)
Entry point 0x4006c0
There are 9 program headers, starting at offset 64

程序头:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R   0x8
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x0009bc 0x0009bc R E 0x10000
  INTERP         0x000238 0x0000000000400238 0x0000000000400238 0x00001b 0x00001b R   0x1
      [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
  GNU_EH_FRAME   0x000860 0x0000000000400860 0x0000000000400860 0x00004c 0x00004c R   0x4
  NOTE           0x00099c 0x000000000040099c 0x000000000040099c 0x000020 0x000020 R   0x4
  LOAD           0x00fd88 0x000000000041fd88 0x000000000041fd88 0x000288 0x000290 RW  0x10000
  GNU_RELRO      0x00fd88 0x000000000041fd88 0x000000000041fd88 0x000278 0x000278 R   0x1
  DYNAMIC        0x00fd98 0x000000000041fd98 0x000000000041fd98 0x000210 0x000210 RW  0x8

再看一下要挂载的这个demo_add的符号地址:

readelf -sW rootfs/usr/bin/uprobe_demo | grep demo_add
    96: 00000000004007e4    24 FUNC    GLOBAL DEFAULT   12 demo_add

最后计算出符号偏移:

>>> 0x000000 + (0x4007e4 - 0x400000)
2020
>>> hex(_)
'0x7e4'

使用tracefs进行挂载,输入以下两条命令后进入到uprobe_register注册uprobe:

echo 'p:demo_add /usr/bin/uprobe_demo:0x7e4 a=%x0 b=%x1' \
     > /sys/kernel/tracing/uprobe_events
    
echo 1 > /sys/kernel/tracing/events/uprobes/demo_add/enable

2. 注册uprobe

注册入口:

struct uprobe *uprobe_register(struct inode *inode,
				loff_t offset, loff_t ref_ctr_offset,
				struct uprobe_consumer *uc)
{
	struct uprobe *uprobe;
	int ret;

	/* Uprobe must have at least one set consumer */
	if (!uc->handler && !uc->ret_handler)
		return ERR_PTR(-EINVAL);

	/* copy_insn() uses read_mapping_page() or shmem_read_mapping_page() */
    // 判断当前文件可以读到page cache中,或者在shmem中
	if (!inode->i_mapping->a_ops->read_folio &&
	    !shmem_mapping(inode->i_mapping))
		return ERR_PTR(-EIO);
	// 判断offset是否合法,不能比elf文件还大
	if (offset > i_size_read(inode))
		return ERR_PTR(-EINVAL);

	/*
	 * This ensures that uprobe_copy_from_page(), copy_to_page() and
	 * __update_ref_ctr() can't cross page boundary.
	 */
    // offset按指令大小对齐,一条不可能跨页
	if (!IS_ALIGNED(offset, UPROBE_SWBP_INSN_SIZE))
		return ERR_PTR(-EINVAL);
    // 计数器的offset按照2字节对齐,同样不会跨页
	if (!IS_ALIGNED(ref_ctr_offset, sizeof(short)))
		return ERR_PTR(-EINVAL);
	// 分配一个uprobe,然后尝试将其添加到一棵全局的红黑树uprobe_tree中
    // 该树使用inode+offset作为key,如果已经存在节点,则会返回原来的节点,不会重复添加
	uprobe = alloc_uprobe(inode, offset, ref_ctr_offset);
	if (IS_ERR(uprobe))
		return uprobe;
	
    // 拿锁
	down_write(&uprobe->register_rwsem);
	// 给当前挂载点添加一个consumer,也就是从tracefs中传递过来的结构体
    consumer_add(uprobe, uc);
    // 1. 从当前inode->i_mmap这棵红黑树上找到所有包含该offset的VMA
    // 2. 检查VMA是否真的满足patch条件
    // 3. 对每个满足条件的VMA调用install_breakpoint进行patch
	ret = register_for_each_vma(uprobe, uc);
	up_write(&uprobe->register_rwsem);

	if (ret) {
		uprobe_unregister_nosync(uprobe, uc);
		/*
		 * Registration might have partially succeeded, so we can have
		 * this consumer being called right at this time. We need to
		 * sync here. It's ok, it's unlikely slow path.
		 */
		uprobe_unregister_sync();
		return ERR_PTR(ret);
	}

	return uprobe;
}

开始在VMA上修改指令:

static int install_breakpoint(struct uprobe *uprobe, struct vm_area_struct *vma,
		unsigned long vaddr)
{
	struct mm_struct *mm = vma->vm_mm;
	bool first_uprobe;
	int ret;
	// 将原始指令保存到uprobe.arch.insn
	ret = prepare_uprobe(uprobe, vma->vm_file, mm, vaddr);
	if (ret)
		return ret;
	// 如果是本VMA第一个uprobe,需要标记
	first_uprobe = !mm_flags_test(MMF_HAS_UPROBES, mm);
	if (first_uprobe)
		mm_flags_set(MMF_HAS_UPROBES, mm);
	// 替换指令,最终调用到uprobe_write
	ret = set_swbp(&uprobe->arch, vma, vaddr);
	if (!ret)
		mm_flags_clear(MMF_RECALC_UPROBES, mm);
	else if (first_uprobe)
		mm_flags_clear(MMF_HAS_UPROBES, mm);

	return ret;
}

准备阶段会根据指令的类型判断使用XOL / Simulate:

int arch_uprobe_analyze_insn(struct arch_uprobe *auprobe, struct mm_struct *mm,
		unsigned long addr)
{
	u32 insn;

	/* TODO: Currently we do not support AARCH32 instruction probing */
	if (mm->context.flags & MMCF_AARCH32)
		return -EOPNOTSUPP;
	else if (!IS_ALIGNED(addr, AARCH64_INSN_SIZE))
		return -EINVAL;

	insn = le32_to_cpu(auprobe->insn);

	switch (arm_probe_decode_insn(insn, &auprobe->api)) {
	case INSN_REJECTED:
		return -EINVAL;

	case INSN_GOOD_NO_SLOT:
		auprobe->simulate = true;
		break;

	default:
		break;
	}

	return 0;
}

真正写入指令:

int uprobe_write(struct arch_uprobe *auprobe, struct vm_area_struct *vma,
		 const unsigned long insn_vaddr, uprobe_opcode_t *insn, int nbytes,
		 uprobe_write_verify_t verify, bool is_register, bool do_update_ref_ctr,
		 void *data)
{
	const unsigned long vaddr = insn_vaddr & PAGE_MASK;
	struct mm_struct *mm = vma->vm_mm;
	struct uprobe *uprobe;
	int ret, ref_ctr_updated = 0;
	unsigned int gup_flags = FOLL_FORCE;
	struct mmu_notifier_range range;
	struct folio_walk fw;
	struct folio *folio;
	struct page *page;

	uprobe = container_of(auprobe, struct uprobe, arch);

	if (WARN_ON_ONCE(!is_cow_mapping(vma->vm_flags)))
		return -EINVAL;

	if (is_register)
		gup_flags |= FOLL_WRITE | FOLL_SPLIT_PMD;

retry:
	// 因为gup_flags中有WRITE,这里会出发COW,返回的page是新分配的一个匿名页
	ret = get_user_pages_remote(mm, vaddr, 1, gup_flags, &page, NULL);
	if (ret <= 0)
		goto out;
	folio = page_folio(page);

	ret = verify(page, insn_vaddr, insn, nbytes, data);
	if (ret <= 0) {
		folio_put(folio);
		goto out;
	}

	/* We are going to replace instruction, update ref_ctr. */
	// 更新ref counter
	if (do_update_ref_ctr && !ref_ctr_updated && uprobe->ref_ctr_offset) {
		ret = update_ref_ctr(uprobe, mm, is_register ? 1 : -1);
		if (ret) {
			folio_put(folio);
			goto out;
		}

		ref_ctr_updated = 1;
	}

	ret = 0;
	// 测试当前是否是匿名页
	if (unlikely(!folio_test_anon(folio) || folio_is_zone_device(folio))) {
		VM_WARN_ON_ONCE(is_register);
		folio_put(folio);
		goto out;
	}

	if (!is_register) {
		/*
		 * In the common case, we'll be able to zap the page when
		 * unregistering. So trigger MMU notifiers now, as we won't
		 * be able to do it under PTL.
		 */
		mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
					vaddr, vaddr + PAGE_SIZE);
		mmu_notifier_invalidate_range_start(&range);
	}

	ret = -EAGAIN;
	/* Walk the page tables again, to perform the actual update. */
	// 向folio中写入insn,底层调用copy_to_page最终用memcpy直接写入
	if (folio_walk_start(&fw, vma, vaddr, 0)) {
		if (fw.page == page)
			ret = __uprobe_write(vma, &fw, folio, insn_vaddr, insn, nbytes, is_register);
		folio_walk_end(&fw, vma);
	}

	if (!is_register)
		mmu_notifier_invalidate_range_end(&range);

	folio_put(folio);
	switch (ret) {
	case -EFAULT:
		gup_flags |= FOLL_WRITE | FOLL_SPLIT_PMD;
		fallthrough;
	case -EAGAIN:
		goto retry;
	default:
		break;
	}

out:
	/* Revert back reference counter if instruction update failed. */
	if (do_update_ref_ctr && ret < 0 && ref_ctr_updated)
		update_ref_ctr(uprobe, mm, is_register ? -1 : 1);

	/* try collapse pmd for compound page */
	if (ret > 0)
		collapse_pte_mapped_thp(mm, vaddr, false);

	return ret < 0 ? ret : 0;
}

如果是在此之后使用该ELF创建的进程,那么会在进程映射程序段的时候进行安装:

int uprobe_mmap(struct vm_area_struct *vma)
{
	struct list_head tmp_list;
	struct uprobe *uprobe, *u;
	struct inode *inode;

	if (no_uprobe_events())
		return 0;

	if (vma->vm_file &&
	    (vma->vm_flags & (VM_WRITE|VM_SHARED)) == VM_WRITE &&
	    mm_flags_test(MMF_HAS_UPROBES, vma->vm_mm))
		delayed_ref_ctr_inc(vma);

	if (!valid_vma(vma, true))
		return 0;

	inode = file_inode(vma->vm_file);
	if (!inode)
		return 0;

	mutex_lock(uprobes_mmap_hash(inode));
    // 根据inode从uprobe_tree中获取该VMA中的所有uprobe
	build_probe_list(inode, vma, vma->vm_start, vma->vm_end, &tmp_list);
	/*
	 * We can race with uprobe_unregister(), this uprobe can be already
	 * removed. But in this case filter_chain() must return false, all
	 * consumers have gone away.
	 */
	list_for_each_entry_safe(uprobe, u, &tmp_list, pending_list) {
		if (!fatal_signal_pending(current) &&
		    filter_chain(uprobe, vma->vm_mm)) {
			unsigned long vaddr = offset_to_vaddr(vma, uprobe->offset);
			install_breakpoint(uprobe, vma, vaddr);
		}
		put_uprobe(uprobe);
	}
	mutex_unlock(uprobes_mmap_hash(inode));

	return 0;
}

3. 处理流程

经过同步中断和BRK分发后来到:

static void handle_swbp(struct pt_regs *regs)
{
	struct uprobe *uprobe;
	unsigned long bp_vaddr;
	int is_swbp;

	bp_vaddr = uprobe_get_swbp_addr(regs);
	if (bp_vaddr == uprobe_get_trampoline_vaddr())
		return uprobe_handle_trampoline(regs);

	rcu_read_lock_trace();
	
    // 根据虚拟地址和inode找到对应的uprobe
	uprobe = find_active_uprobe_rcu(bp_vaddr, &is_swbp);
	if (!uprobe) {
		if (is_swbp > 0) {
			/* No matching uprobe; signal SIGTRAP. */
			force_sig(SIGTRAP);
		} else {
			instruction_pointer_set(regs, bp_vaddr);
		}
		goto out;
	}

	// 还原PC
	instruction_pointer_set(regs, bp_vaddr);

	/*
	 * TODO: move copy_insn/etc into _register and remove this hack.
	 * After we hit the bp, _unregister + _register can install the
	 * new and not-yet-analyzed uprobe at the same address, restart.
	 */
	if (unlikely(!test_bit(UPROBE_COPY_INSN, &uprobe->flags)))
		goto out;

	/*
	 * Pairs with the smp_wmb() in prepare_uprobe().
	 *
	 * Guarantees that if we see the UPROBE_COPY_INSN bit set, then
	 * we must also see the stores to &uprobe->arch performed by the
	 * prepare_uprobe() call.
	 */
	smp_rmb();

	// 给当前进程分配一个utask,用于uprobe处理
	if (!get_utask())
		goto out;

	if (arch_uprobe_ignore(&uprobe->arch, regs))
		goto out;

	// 调用该uprobe上所有consumer的handler,这里调用的就是tracefs挂载的回调
	handler_chain(uprobe, regs);

	arch_uprobe_optimize(&uprobe->arch, bp_vaddr);

	// consumer的handler修改了PC,无需再接管控制流了
	if (instruction_pointer(regs) != bp_vaddr)
		goto out;

	// Simulate
	if (arch_uprobe_skip_sstep(&uprobe->arch, regs))
		goto out;

	// XOL
	if (pre_ssout(uprobe, regs, bp_vaddr))
		goto out;

out:
	/* arch_uprobe_skip_sstep() succeeded, or restart if can't singlestep */
	rcu_read_unlock_trace();
}

Simulate的处理和kprobe一样:

bool arch_uprobe_skip_sstep(struct arch_uprobe *auprobe, struct pt_regs *regs)
{
	u32 insn;
	unsigned long addr;

	if (!auprobe->simulate)
		return false;

	insn = le32_to_cpu(auprobe->insn);
	addr = instruction_pointer(regs);

	if (auprobe->api.handler)
		auprobe->api.handler(insn, addr, regs);

	return true;
}

XOL的处理与kprobe略有不同,因为EL0下可以直接使用Hardware single-step,不需要在slot中再添加一条BRK_SS:

static int
pre_ssout(struct uprobe *uprobe, struct pt_regs *regs, unsigned long bp_vaddr)
{
	struct uprobe_task *utask = current->utask;
	int err;

	if (!try_get_uprobe(uprobe))
		return -EINVAL;
	// 分配一个用户态的slot(4B),把原指令复制进去
    // slot是通过
	if (!xol_get_insn_slot(uprobe, utask)) {
		err = -ENOMEM;
		goto err_out;
	}
	// 保存原指令地址
	utask->vaddr = bp_vaddr;
    // 修改PC地址为slot,开启单步(给thread_info设置一个flag:TIF_SINGLESTEP)
	err = arch_uprobe_pre_xol(&uprobe->arch, regs);
	if (unlikely(err)) {
		xol_free_insn_slot(utask);
		goto err_out;
	}
	// 保存下状态
	utask->active_uprobe = uprobe;
	utask->state = UTASK_SSTEP;
	return 0;
err_out:
	put_uprobe(uprobe);
	return err;
}

eret到EL0,执行完一条原始指令后再次来到内核态,经过同步异常分发最终来到:

int arch_uprobe_post_xol(struct arch_uprobe *auprobe, struct pt_regs *regs)
{
	struct uprobe_task *utask = current->utask;

	WARN_ON_ONCE(current->thread.fault_code != UPROBE_INV_FAULT_CODE);

	// 修改PC为原始指令的下一条
	instruction_pointer_set(regs, utask->vaddr + 4);
	// 禁用单步
	user_disable_single_step(current);

	return 0;
}

eret 再次返回用户态,至此一次uprobe执行完毕;

4. 注销uprobe

入口:

void uprobe_unregister_nosync(struct uprobe *uprobe, struct uprobe_consumer *uc)
{
	int err;

	down_write(&uprobe->register_rwsem);
    // 删除当前consumer
	consumer_del(uprobe, uc);
    // 和注册时一样,从uprobe_tree上拿到所有VMA,对于每个一VMA判断当前consumer时候是最后一个
    // 如果是则将该VMA的probe移除,还原成原始指令
	err = register_for_each_vma(uprobe, NULL);
	up_write(&uprobe->register_rwsem);

	/* TODO : cant unregister? schedule a worker thread */
	if (unlikely(err)) {
		uprobe_warn(current, "unregister, leaking uprobe");
		return;
	}

	put_uprobe(uprobe);
}

然后RCU等待GP结束后回收资源:

void uprobe_unregister_sync(void)
{
	/*
	 * Now that handler_chain() and handle_uretprobe_chain() iterate over
	 * uprobe->consumers list under RCU protection without holding
	 * uprobe->register_rwsem, we need to wait for RCU grace period to
	 * make sure that we can't call into just unregistered
	 * uprobe_consumer's callbacks anymore. If we don't do that, fast and
	 * unlucky enough caller can free consumer's memory and cause
	 * handler_chain() or handle_uretprobe_chain() to do an use-after-free.
	 */
	synchronize_rcu_tasks_trace();
	synchronize_srcu(&uretprobes_srcu);
}

5. uretprobe

与kretprobe的处理过程一样,不过trampoline放在用户态,其实现依旧是一个简单的BRK,进入内核态执行完回调后修改pc返回到caller中。


Share this post:

Previous Post
【Linux】 ftrace 机制分析
Next Post
【Linux】 kprobe 源码分析