0 引言

该篇为了探究 uprobe 的执行流程和结构.

1 测试代码

trace_uprobe.ko

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <linux/module.h>
#include <linux/ptrace.h>
#include <linux/uprobes.h>
#include <linux/namei.h>
#include <linux/moduleparam.h>

MODULE_AUTHOR("john doe");
MODULE_LICENSE("GPL v2");

static char *filename;
module_param(filename, charp, S_IRUGO);

static long offset;
module_param(offset, long, S_IRUGO);

static int handler_pre(struct uprobe_consumer *self, struct pt_regs *regs){
pr_info("handler: arg0 = %d arg1 =%d \n", (int)regs->di, (int)regs->si);
return 0;
}

static int handler_ret(struct uprobe_consumer *self,
unsigned long func,
struct pt_regs *regs){
pr_info("ret_handler ret = %d \n", (int)regs->ax);
return 0;
}

static struct uprobe_consumer uc = {
.handler = handler_pre,
.ret_handler = handler_ret,
};


static struct inode *inode;

static int __init uprobe_init(void) {
struct path path;
int ret;

ret = kern_path(filename, LOOKUP_FOLLOW, &path);
if (ret < 0) {
pr_err("kern_path failed, returned %d\n", ret);
return ret;
}

inode = igrab(path.dentry->d_inode);
path_put(&path);

ret = uprobe_register(inode, offset, &uc);
if (ret < 0) {
pr_err("register_uprobe failed, returned %d\n", ret);
return ret;
}

return 0;
}

static void __exit uprobe_exit(void) {
uprobe_unregister(inode, offset, &uc);
}

module_init(uprobe_init);
module_exit(uprobe_exit);

add_main.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int main(void) {
add(1, 2);
}

编译完成之后我们开始进行调试(需要对 add_main 加上 -static 选项).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ readelf -s main /mnt/rootfs/add_main | grep add
...
1261: 0000000000401865 24 FUNC GLOBAL DEFAULT 7 add
$ readelf -l /mnt/rootfs/add_main | sed -n '1,120p'

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

程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000004f8 0x00000000000004f8 R 0x1000

可以看到 base_vaddr 是 0x400000, 被加载的地址是 0x401865, 因此符号在文件中的偏移是 0x1865.

2 实现原理

2.1 注册 uprobe 的流程

我们调试的时候可以给几个关键节点打上断点.

1
b uprobe_register

然后我们开始加载模块.

1
insmod trace_uprobe.ko filename=./add_main offset=0x1865

这里我们卡在了 uprobe 的注册阶段.

1
2
3
4
5
int uprobe_register(struct inode *inode, loff_t offset,
struct uprobe_consumer *uc)
{
return __uprobe_register(inode, offset, 0, uc);
}

这里我们可以查看一下 add_main 的 inode 号, 发现是对应现在的 inode 的, offset 也是对应我们传入的参数, 当然这里的 uc 也对应我们 ko 文件使用的 uc.

1
2
$ ls -i /mnt/rootfs/add_main 
2443 /mnt/rootfs/add_main

alt text

alt text

通过函数我们知道, 通过 alloc_uprobe 来创建一个新的 uprobe, 然后通过 consumer_adduprobe 加入到 consumer 链表中(uprobe->consumer = uc), 之后注册探针到每个 VMA.

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/*
* __uprobe_register - register a probe
* @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..
*
* Apart from the access refcount, __uprobe_register() takes a creation
* refcount (thro alloc_uprobe) if and only if this @uprobe is getting
* inserted into the rbtree (i.e first consumer for a @inode:@offset
* tuple). Creation refcount stops uprobe_unregister from freeing the
* @uprobe even before the register operation is complete. Creation
* refcount is released when the last @uc for the @uprobe
* unregisters. Caller of __uprobe_register() is required to keep @inode
* (and the containing mount) referenced.
*
* Return errno if it cannot successully install probes
* else return 0 (success)
*/
static int __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 -EINVAL;

/* copy_insn() uses read_mapping_page() or shmem_read_mapping_page() */
if (!inode->i_mapping->a_ops->read_folio &&
!shmem_mapping(inode->i_mapping))
return -EIO;
/* Racy, just to catch the obvious mistakes */
if (offset > i_size_read(inode))
return -EINVAL;

/*
* This ensures that copy_from_page(), copy_to_page() and
* __update_ref_ctr() can't cross page boundary.
*/
if (!IS_ALIGNED(offset, UPROBE_SWBP_INSN_SIZE))
return -EINVAL;
if (!IS_ALIGNED(ref_ctr_offset, sizeof(short)))
return -EINVAL;

retry:
uprobe = alloc_uprobe(inode, offset, ref_ctr_offset);
if (!uprobe)
return -ENOMEM;
if (IS_ERR(uprobe))
return PTR_ERR(uprobe);

/*
* We can race with uprobe_unregister()->delete_uprobe().
* Check uprobe_is_active() and retry if it is false.
*/
down_write(&uprobe->register_rwsem);
ret = -EAGAIN;
if (likely(uprobe_is_active(uprobe))) {
consumer_add(uprobe, uc);
ret = register_for_each_vma(uprobe, uc);
if (ret)
__uprobe_unregister(uprobe, uc);
}
up_write(&uprobe->register_rwsem);
put_uprobe(uprobe);

if (unlikely(ret == -EAGAIN))
goto retry;
return ret;
}

这里我们观察 register_for_each_vma 函数, 他有结构体 mm_struct 是与进程内存相关的结构体, 而 vm_area_struct 是与内存区域相关的结构体.

其中他会拿到我们待注册程序指定 offset 对应那一页的 VMA 信息, 遍历整个的 VMA 空间, 观察如果是 register 的状态, 那么就 install_breakpoint, 否则是 unregister 的状态, 那么就 remove_breakpoint.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static int
register_for_each_vma(struct uprobe *uprobe, struct uprobe_consumer *new)
{
bool is_register = !!new;
struct map_info *info;
int err = 0;

percpu_down_write(&dup_mmap_sem);
info = build_map_info(uprobe->inode->i_mapping,
uprobe->offset, is_register);
if (IS_ERR(info)) {
err = PTR_ERR(info);
goto out;
}

while (info) {
struct mm_struct *mm = info->mm;
struct vm_area_struct *vma;

if (err && is_register)
goto free;

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) {
/* consult only the "caller", new consumer. */
if (consumer_filter(new,
UPROBE_FILTER_REGISTER, mm))
err = install_breakpoint(uprobe, mm, vma, info->vaddr);
} else if (test_bit(MMF_HAS_UPROBES, &mm->flags)) {
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);
}
out:
percpu_up_write(&dup_mmap_sem);
return err;
}

这里我们关注一下注册 uprobe 的过程, 中断查看 prepare_uprobe, 还有通过 set_swbp 来设置软件断点(真正设置断点指令到探点的代码).

arch insn machine code
x86 INT3 0xCC
ARM BRK / BKPT 0xD4200000
RISC-V EBREAK 0x00100073
RISC-V C.EBREAK 0x9002
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int
install_breakpoint(struct uprobe *uprobe, struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long vaddr)
{
bool first_uprobe;
int ret;

ret = prepare_uprobe(uprobe, vma->vm_file, mm, vaddr);
if (ret)
return ret;

/*
* set MMF_HAS_UPROBES in advance for uprobe_pre_sstep_notifier(),
* the task can hit this breakpoint right after __replace_page().
*/
first_uprobe = !test_bit(MMF_HAS_UPROBES, &mm->flags);
if (first_uprobe)
set_bit(MMF_HAS_UPROBES, &mm->flags);

ret = set_swbp(&uprobe->arch, mm, vaddr);
if (!ret)
clear_bit(MMF_RECALC_UPROBES, &mm->flags);
else if (first_uprobe)
clear_bit(MMF_HAS_UPROBES, &mm->flags);

return ret;
}

先观察 prepare_uprobe, 我们将探针处的指令复制到 uprobe 当中, 然后检查该探点处指令的类型是否为 trap, 随后分析指令的类型是否需要内核模拟执行, 或者可以探测, 从而设置 simulate 参数.

对于 riscv64 来讲, 这里的 trap 指令指的是 ebreak 或 c.ebreak.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static int prepare_uprobe(struct uprobe *uprobe, struct file *file,
struct mm_struct *mm, unsigned long vaddr)
{
int ret = 0;

if (test_bit(UPROBE_COPY_INSN, &uprobe->flags))
return ret;

/* TODO: move this into _register, until then we abuse this sem. */
down_write(&uprobe->consumer_rwsem);
if (test_bit(UPROBE_COPY_INSN, &uprobe->flags))
goto out;

ret = copy_insn(uprobe, file);
if (ret)
goto out;

ret = -ENOTSUPP;
if (is_trap_insn((uprobe_opcode_t *)&uprobe->arch.insn))
goto out;

// 这里根据断点处 insn 的情况进行判断.
// INSN_REJECTED 则意味着这条指令不能够被 uprobes 支持.
//
// INSN_GOOD_NO_SLOT 表示这条指令不能够在 slot 当中执行,
// 内核将在 trap 中模拟它.
//
// INSN_GOOD 表示这条指令是安全的, 可以被 uprobe 替换为断点.
// 另外该函数还会根据情况填充 auprobe->insn 区域的指令, 以及
// 确定 auprobe->api 的 handler
// 具体的 handler 根据架构进行不同的指定.
// handler 就是具体模拟执行 auprobe->insn 指令的 C 代码.
// riscv: https://elixir.bootlin.com/linux/v6.6.114/source/arch/riscv/kernel/probes/simulate-insn.h#L23
ret = arch_uprobe_analyze_insn(&uprobe->arch, mm, vaddr);
if (ret)
goto out;

smp_wmb(); /* pairs with the smp_rmb() in handle_swbp() */
set_bit(UPROBE_COPY_INSN, &uprobe->flags);

out:
up_write(&uprobe->consumer_rwsem);

return ret;
}

关于上面 slot 和 xol 的关系, 这里笔者不太了解, 然后笔者咨询了一下 GPT5, 它给出了答案, 当然答案不一定是对的.

1
2
3
4
5
6
7
XOL 区(例如 0x7ffff7ff0000 - 0x7ffff7ff0fff)

┌──────────────────────────────┐
│ slot[0]: 指令副本 for probe A │
│ slot[1]: 指令副本 for probe B │
│ slot[2]: 指令副本 for probe C │
└──────────────────────────────┘

2.2 handler 执行的流程

uprobe_notify_resume 是执行 uprobe 探点代码的关键处, 因此我们打上断点.

我们先来看一下是如何到达 uprobe_notify_resume 的.

当执行到目标指令的时候, 由于探点处是一个 ebreak 指令, 于是进入 do_trap_break.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
asmlinkage __visible __trap_section void do_trap_break(struct pt_regs *regs)
{
if (user_mode(regs)) {
irqentry_enter_from_user_mode(regs);

handle_break(regs);

irqentry_exit_to_user_mode(regs);
} else {
irqentry_state_t state = irqentry_nmi_enter(regs);

handle_break(regs);

irqentry_nmi_exit(regs, state);
}
}

这里我们直接进入 handle_break.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void handle_break(struct pt_regs *regs)
{
// 1
if (probe_single_step_handler(regs))
return;

// 2
if (probe_breakpoint_handler(regs))
return;

current->thread.bad_cause = regs->cause;

if (user_mode(regs))
force_sig_fault(SIGTRAP, TRAP_BRKPT, (void __user *)regs->epc);
#ifdef CONFIG_KGDB
else if (notify_die(DIE_TRAP, "EBREAK", regs, 0, regs->cause, SIGTRAP)
== NOTIFY_STOP)
return;
#endif
else if (report_bug(regs->epc, regs) == BUG_TRAP_TYPE_WARN ||
handle_cfi_failure(regs) == BUG_TRAP_TYPE_WARN)
regs->epc += get_break_insn_length(regs->epc);
else
die(regs, "Kernel BUG");
}

这里通过 probe_single_step_handler 处理单步执行异常, 或者 probe_breakpoint_handler 处理断点命中事件(这里笔者也不知道前者的意图, 后者则是遇到 ebreak, 这些指令主动触发异常后执行的). 这里我们看后者(两者逻辑貌似差不多).

1
2
3
4
5
6
static bool probe_breakpoint_handler(struct pt_regs *regs)
{
bool user = user_mode(regs);

return user ? uprobe_breakpoint_handler(regs) : kprobe_breakpoint_handler(regs);
}

这里根据系统的状态决定是使用 uprobe 还是 kprobe. 这里当然是 uprobe.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这里可以看到两种方式的区别, 主要是执行了不同的函数链
bool uprobe_breakpoint_handler(struct pt_regs *regs)
{
if (uprobe_pre_sstep_notifier(regs))
return true;

return false;
}

bool uprobe_single_step_handler(struct pt_regs *regs)
{
if (uprobe_post_sstep_notifier(regs))
return true;

return false;
}

然后执行 uprobe_pre_sstep_notifier. 这里注释也说明了 uprobe_pre_sstep_notifieruprobe_post_sstep_notifier 都是在中断的期间被调用的, 用来作为一种通知的机制, 当他们设置 TIF_UPROBE 之后, uprobe_notify_resume 开始被调用.

可以看到后者的区别是修改了当前任务的状态为 UTASK_SSTEP_ACK, 这在 handle_singlestep 会被使用到.

在一开始 utask->active_uprobe 是不存在的, 因此这里只有在执行过 handler_swbp 之后才会有可能进入 uprobe_post_sstep_notifier 的判断.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* uprobe_pre_sstep_notifier gets called from interrupt context as part of
* notifier mechanism. Set TIF_UPROBE flag and indicate breakpoint hit.
*/
int uprobe_pre_sstep_notifier(struct pt_regs *regs)
{
if (!current->mm)
return 0;

if (!test_bit(MMF_HAS_UPROBES, &current->mm->flags) &&
(!current->utask || !current->utask->return_instances))
return 0;

set_thread_flag(TIF_UPROBE);
return 1;
}

/*
* uprobe_post_sstep_notifier gets called in interrupt context as part of notifier
* mechanism. Set TIF_UPROBE flag and indicate completion of singlestep.
*/
int uprobe_post_sstep_notifier(struct pt_regs *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;
}

因此这里我们主要关注 uprobe_notify_resume, 它才是 probe 的主体.

1
b uprobe_notify_resume

这里我们开始关注核心代码.

uprobe_notify_resumeTIF_UPROBE 标志设置后被触发, 它的任务是在 handle_swbp 之后设置 utask->active_uprobe. 因此这里的调用顺序肯定是先执行 handle_swbp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* On breakpoint hit, breakpoint notifier sets the TIF_UPROBE flag and
* allows the thread to return from interrupt. After that handle_swbp()
* sets utask->active_uprobe.
*
* On singlestep exception, singlestep notifier sets the TIF_UPROBE flag
* and allows the thread to return from interrupt.
*
* While returning to userspace, thread notices the TIF_UPROBE flag and calls
* uprobe_notify_resume().
*/
void uprobe_notify_resume(struct pt_regs *regs)
{
struct uprobe_task *utask;

clear_thread_flag(TIF_UPROBE);

utask = current->utask;
if (utask && utask->active_uprobe)
handle_singlestep(utask, regs);
else
handle_swbp(regs);
}

这里我们重点关注 handle_swbp 的代码部分. 此时我们查看寄存器 IP 的数值是 0x401866, 我们设置的探测点是 0x401865(这里为什么会多出1字节, 笔者也比较好奇, 应该是自动指向中断后的下一条指令), 同时我们可以看到对应的 0x401865 处的指令是 INT3, 然后我们对比没有加 uprobe 原本的汇编代码如下.

alt text

加入 uprobe 探点之后.

alt text

alt text

加入 uprobe 探点之前.

alt text

很明显我们只是替换了开头的第一个字节为 INT3 的指令.

这里我们整理一下 handle_swbp 的步骤, 首先 bp_vaddr, 这里指的应该是断点处的地址(需要寄存器里面的IP值加以纠正), 因此在代码中的 2 处之前, regs->ip 相关的数值存在指向不为断点处地址的情况(这个因架构而异, 貌似一般 uprobe_get_swbp_addr 函数的做法是使用当前的 regs->ip 减去一个 INT, ebreak … 等中断指令的长度).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
* Run handler and ask thread to singlestep.
* Ensure all non-fatal signals cannot interrupt thread while it singlesteps.
*/
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 == get_trampoline_vaddr())
return handle_trampoline(regs);

uprobe = find_active_uprobe(bp_vaddr, &is_swbp);
if (!uprobe) {
if (is_swbp > 0) {
/* No matching uprobe; signal SIGTRAP. */
force_sig(SIGTRAP);
} else {
/*
* Either we raced with uprobe_unregister() or we can't
* access this memory. The latter is only possible if
* another thread plays with our ->mm. In both cases
* we can simply restart. If this vma was unmapped we
* can pretend this insn was not executed yet and get
* the (correct) SIGSEGV after restart.
*/
instruction_pointer_set(regs, bp_vaddr);
}
return;
}

// ============ 2 ============
// 这里修正 regs->ip 的数值指向正确的断点触发时的地址
/* change it in advance for ->handler() and restart */
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();

/* Tracing handlers use ->utask to communicate with fetch methods */
if (!get_utask())
goto out;

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

handler_chain(uprobe, regs);

// 这里很重要, 会根据 simulate 的数值决定是否模拟执行
// auprobe->insn 的代码
// 如果 simulate 为 false 那么就进入 pre_ssout 函数在 xol_area 区域执行指令
if (arch_uprobe_skip_sstep(&uprobe->arch, regs))
goto out;

// 1. 在这里我们将前面的 active_uprobe 设置为当前的 uprobe
// 2. 并且这里会开始填充 ixol 字段, 不再使用 insn 字段
// 3. 这里会填充 xol_area 区域, 后期调用 ret_handler 需要这个区域
// 4. 这里会将 utask->state 设置为 UTASK_SSTEP.
// 5. 修改 PC 值为 xol_area 中 slot 的地址
if (!pre_ssout(uprobe, regs, bp_vaddr))
return;

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

这里我们先忽略掉 handle_trampoline 这行代码, 直接看第一次执行到这个函数的时候触发的重点 handler_chain.

其中我们会在初期将 uprobe->arch 的地址留给 current->utask->auprobe, 这里关注一下里面的内容是什么, 是架构相关的, 但是每个架构都存在 insn/ixol 字段, 保存着被断点指令替换的指令.

1
2
3
4
5
6
7
8
9
10
11
// 对于 insn 和 ixol 的区别
/*
* 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().
*/

alt text

这里我们开始执行 uprobe 前期的第一个 hook 了.

alt text

执行完成之后, 如果该 uprobe 存在一个 ret_handler, 我们标记一下, 之后这里 prepare_uretprobe 执行的条件是 rc 返回值为 0 且 ret_handler 存在, 否则执行 unapply_uprobe 函数.

这是两条不同的路, 一条是走 uretprobe 的路径, 另一个是走普通断点的恢复路径. 于是我们可以利用这里 rc 的值进行一些不同的用法, 一般 rc 为 0 表示继续执行, 为其它数值表示特殊处理, 针对这里, 笔者想两边都探索一下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static void handler_chain(struct uprobe *uprobe, struct pt_regs *regs)
{
struct uprobe_consumer *uc;
int remove = UPROBE_HANDLER_REMOVE;
bool need_prep = false; /* prepare return uprobe, when needed */

down_read(&uprobe->register_rwsem);
current->utask->auprobe = &uprobe->arch;
for (uc = uprobe->consumers; uc; uc = uc->next) {
int rc = 0;

if (uc->handler) {
rc = uc->handler(uc, regs);
WARN(rc & ~UPROBE_HANDLER_MASK,
"bad rc=0x%x from %ps()\n", rc, uc->handler);
}

if (uc->ret_handler)
need_prep = true;

remove &= rc;
}
current->utask->auprobe = NULL;

if (need_prep && !remove)
prepare_uretprobe(uprobe, regs); /* put bp at return */

if (remove && uprobe->consumers) {
WARN_ON(!uprobe_is_active(uprobe));
unapply_uprobe(uprobe, current->mm);
}
up_read(&uprobe->register_rwsem);
}

首先是 prepare_uretprobe 的路径.

什么是 XOL(execute-out-of-line area)? 它是一个存放执行副本的区域, 在 uprobe 这里, 该区域存储着执行时被断点覆盖的那条指令.

这里就是做一个 ret_handler 的前期准备, 主要填充了 ri 变量. 这里将 xol 区域的地址放到了 regs->ra 当中, 方便后续进行执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static void prepare_uretprobe(struct uprobe *uprobe, struct pt_regs *regs)
{
struct return_instance *ri;
struct uprobe_task *utask;
unsigned long orig_ret_vaddr, trampoline_vaddr;
bool chained;

if (!get_xol_area())
return;

utask = get_utask();
if (!utask)
return;

if (utask->depth >= MAX_URETPROBE_DEPTH) {
printk_ratelimited(KERN_INFO "uprobe: omit uretprobe due to"
" nestedness limit pid/tgid=%d/%d\n",
current->pid, current->tgid);
return;
}

ri = kmalloc(sizeof(struct return_instance), GFP_KERNEL);
if (!ri)
return;

// 这里同时会将 trampoline_vaddr 处的指令设置为 ebreak (riscv)
trampoline_vaddr = get_trampoline_vaddr();
// =========== 1 ===========
// 这里将 regs->ra 替换为 trampoline_vaddr
// orig_ret_vaddr 暂存 ra 的数值
orig_ret_vaddr = arch_uretprobe_hijack_return_addr(trampoline_vaddr, regs);
if (orig_ret_vaddr == -1)
goto fail;

/* drop the entries invalidated by longjmp() */
chained = (orig_ret_vaddr == trampoline_vaddr);
cleanup_return_instances(utask, chained, regs);

/*
* We don't want to keep trampoline address in stack, rather keep the
* original return address of first caller thru all the consequent
* instances. This also makes breakpoint unwrapping easier.
*/
if (chained) {
if (!utask->return_instances) {
/*
* This situation is not possible. Likely we have an
* attack from user-space.
*/
uprobe_warn(current, "handle tail call");
goto fail;
}
orig_ret_vaddr = utask->return_instances->orig_ret_vaddr;
}

ri->uprobe = get_uprobe(uprobe);
ri->func = instruction_pointer(regs);
ri->stack = user_stack_pointer(regs);
ri->orig_ret_vaddr = orig_ret_vaddr;
ri->chained = chained;

utask->depth++;
ri->next = utask->return_instances;
utask->return_instances = ri;

return;
fail:
kfree(ri);
}

下面是 unapply_uprobe 函数, 这里主要是遍历待补丁符号所处的虚拟地址空间, 将之前 install_breakpoint 的探测点都删除.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int unapply_uprobe(struct uprobe *uprobe, struct mm_struct *mm)
{
VMA_ITERATOR(vmi, mm, 0);
struct vm_area_struct *vma;
int err = 0;

mmap_read_lock(mm);
for_each_vma(vmi, vma) {
unsigned long vaddr;
loff_t offset;

if (!valid_vma(vma, false) ||
file_inode(vma->vm_file) != uprobe->inode)
continue;

offset = (loff_t)vma->vm_pgoff << PAGE_SHIFT;
if (uprobe->offset < offset ||
uprobe->offset >= offset + vma->vm_end - vma->vm_start)
continue;

vaddr = offset_to_vaddr(vma, uprobe->offset);
err |= remove_breakpoint(uprobe, mm, vaddr);
}
mmap_read_unlock(mm);

return err;
}

在第一次执行完毕 uprobe_notify_resume 函数之后, 我们这时由于在前期设置了 utask->active_uprobe, 这里再次进入到函数中,运行一个不同的执行流.

handle_singlestep 是执行另一个执行流的第一个函数. 主要是根据前期在 handler_swbp 的执行结果进行一些后续的恢复处理. 比如这里的 arch_uprobe_post_xol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* Perform required fix-ups and disable singlestep.
* Allow pending signals to take effect.
*/
static void handle_singlestep(struct uprobe_task *utask, struct pt_regs *regs)
{
struct uprobe *uprobe;
int err = 0;

uprobe = utask->active_uprobe;
if (utask->state == UTASK_SSTEP_ACK) // 表示单步执行已经完成
err = arch_uprobe_post_xol(&uprobe->arch, regs);
else if (utask->state == UTASK_SSTEP_TRAPPED) // 表示在单步执行中发生异常
arch_uprobe_abort_xol(&uprobe->arch, regs);
else
WARN_ON_ONCE(1);

put_uprobe(uprobe);
utask->active_uprobe = NULL;
utask->state = UTASK_RUNNING;
xol_free_insn_slot(current);

spin_lock_irq(&current->sighand->siglock);
recalc_sigpending(); /* see uprobe_deny_signal() */
spin_unlock_irq(&current->sighand->siglock);

if (unlikely(err)) {
uprobe_warn(current, "execute the probed insn, sending SIGILL.");
force_sig(SIGILL);
}
}

比如这里设置寄存器 PC 为探测地址的下一条指令.

1
2
3
4
5
6
7
8
9
10
11
12
// riscv
int arch_uprobe_post_xol(struct arch_uprobe *auprobe, struct pt_regs *regs)
{
struct uprobe_task *utask = current->utask;

WARN_ON_ONCE(current->thread.bad_cause != UPROBE_TRAP_NR);
current->thread.bad_cause = utask->autask.saved_cause;

instruction_pointer_set(regs, utask->vaddr + auprobe->insn_size);

return 0;
}

2.3 ret_handler 的流程

这里执行两次 uprobe_notify_resume 之后, 我们再次进入到了 handle_swbp. 这次我们会执行到一个 handle_trampoline 函数. 这个函数负责进行 ret_handler 的处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void handle_trampoline(struct pt_regs *regs)
{
struct uprobe_task *utask;
struct return_instance *ri, *next;
bool valid;

utask = current->utask;
if (!utask)
goto sigill;

ri = utask->return_instances;
if (!ri)
goto sigill;

do {
/*
* We should throw out the frames invalidated by longjmp().
* If this chain is valid, then the next one should be alive
* or NULL; the latter case means that nobody but ri->func
* could hit this trampoline on return. TODO: sigaltstack().
*/
next = find_next_ret_chain(ri);
valid = !next || arch_uretprobe_is_alive(next, RP_CHECK_RET, regs);

instruction_pointer_set(regs, ri->orig_ret_vaddr);
do {
if (valid)
// 同 handler_chain 的逻辑
handle_uretprobe_chain(ri, regs);
ri = free_ret_instance(ri);
utask->depth--;
} while (ri != next);
} while (!valid);

utask->return_instances = ri;
return;

sigill:
uprobe_warn(current, "handle uretprobe, sending SIGILL.");
force_sig(SIGILL);

}

3 References

3.1 TIF

IF 指的是 Thread Information Flags, 这些标志位记录在 task_struct, 用来表示当前线程是否有某些待处理的内核任务或特殊状态. 根据架构相关定义在不同的头文件当中(https://elixir.bootlin.com/linux/v6.6.109/source/arch/riscv/include/asm/thread_info.h#L101).

3.2 handler 和 ret_handler 原理

在浏览完整个流程之后可以去看知乎这两个回答, 里面都有整个流程的图, 很通透. 只不过图中有个地方有误, 是先判断是否有 ret_handler 再判断 simulate 的.

  1. handler: https://zhuanlan.zhihu.com/p/19085021883

  2. ret_handler: https://zhuanlan.zhihu.com/p/20315466139