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中。