可观测性 uprobe


介绍

uprobe是User-Level Dynamic Tracing的简称,相对于内核态的kprobe而言,它是用于跟踪用户态程序的,使用上和kprobe类似。

使用前先检测一下内核的配置,确保CONFIG_UPROBE_EVENTS使能。

# cat /boot/config-`uname -r` |grep CONFIG_UPROBE_EVENTS
CONFIG_UPROBE_EVENTS=y

通过echo xxx > /sys/kernel/debug/tracing/uprobe_events 添加探测点。 使能添加的探测点echo 1 > /sys/kernel/debug/tracing/events/uprobes/<EVENT>/enable

查看事件查看依然是 # cat /sys/kernel/debug/tracing/trace

工作原理

uprobe是和kprobe类似的,我们回归一下kprobe的工作原理,理解了kprobe处理,再看uprobe就简单了。

uprobe注册

可以通过ebpf的方式或者ftrace的方式注册uprobe,这里讲ftrace的情况,对于ebpf方式在后续的ebpf专题系列中讲解。

init_uprobe_trace函数注册debugfs文件trace file接口 /sys/kernel/debug/tracing/uprobe_events

通过写入特定的格式命令到uprobe_events文件的方式注册一个uprobe,写入的内容包括程序文件索引节点、指令偏移、相关操作列表和替换指令。

命令格式为p|r[:[GRP/][EVENT]] PATH:OFFSET[%return][(REF)] [FETCHARGS]

命令执行时对应的调用关系如下:

probes_write或者
        -->create_or_delete_trace_uprobe
                -->trace_uprobe_create 
                        --> __trace_uprobe_create                       

__trace_uprobe_create 创建一个 trace_uprobe 并初始化consumer.handler;

/*
 * uprobe event core functions
 */
struct trace_uprobe {
	struct dyn_event		devent;
	struct uprobe_consumer		consumer;
	struct path			path;
	struct inode			*inode;
	char				*filename;
	unsigned long			offset;
	unsigned long			ref_ctr_offset;
	unsigned long			nhit;
	struct trace_probe		tp;
};
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;
};

static struct trace_uprobe *
alloc_trace_uprobe(const char *group, const char *event, int nargs, bool is_ret)
{
...
	ret = trace_probe_init(&tu->tp, event, group, true);
	if (ret < 0)
		goto error;

	dyn_event_init(&tu->devent, &trace_uprobe_ops);
	tu->consumer.handler = uprobe_dispatcher;
	if (is_ret)
		tu->consumer.ret_handler = uretprobe_dispatcher;
	init_trace_uprobe_filter(tu->tp.event->filter);
...
}

注册uprobe event初始化trace_event_call,这里的reg在使能的时候会用到,用于注册uprobe。

static inline void init_trace_event_call(struct trace_uprobe *tu)
{
	struct trace_event_call *call = trace_probe_event_call(&tu->tp);
	call->event.funcs = &uprobe_funcs;
	call->class->fields_array = uprobe_fields_array;

	call->flags = TRACE_EVENT_FL_UPROBE | TRACE_EVENT_FL_CAP_ANY;
	call->class->reg = trace_uprobe_register;
}

注册 trace_event 到全局event_hash,同时注册trace_event_call到全局的ftrace_events。以及注册对应的event file。

注册之后便在/sys/kernel/debug/tracing/events/uprobes/目录下出现对应的event目录。之后便可操作该目录下的文件做相应的功能配置。

uprobe激活

分为两种情况,一种是通过命令行,另外一种是程序启动时加载。

  1. 通过命令行执行echo 1 >/sys/kernel/debug/tracing/events/uprobes/<EVENT>/enable便可以使能该uprobe。

    是如何做到的呢?

    在系统启动的时候会初始化trace,会注册一些解析event_command命令的方法。

    trace_init
         -->trace_event_init
                 -->event_trace_enable
                         -->register_trigger_cmds
                                 -->register_trigger_enable_disable_cmds
    
    static __init int register_trigger_enable_disable_cmds(void)
    {
        int ret;
    
        ret = register_event_command(&trigger_enable_cmd);
        if (WARN_ON(ret < 0))
            return ret;
        ret = register_event_command(&trigger_disable_cmd);
        if (WARN_ON(ret < 0))
            unregister_trigger_enable_disable_cmds();
    
        return ret;
    }    
    

    当使能uprobe的时候执行便会执行如下调用

    event_enable_trigger_parse  即注册的trigger_enable_cmd的解析函数
         -->trace_event_enable_disable
                 -->__ftrace_event_enable_disable
                         -->call->class->reg(call, TRACE_REG_REGISTER, file)
    

    call->class->reg即函数trace_uprobe_register

    trace_uprobe_register
         -->probe_event_enable
                 -->trace_uprobe_enable
                         -->uprobe_register(tu->inode, tu->offset, &tu->consumer)
                                 -->__uprobe_register
    

    在__uprobe_register中创建一个uprobe 加入到全局的uprobes_tree;找到需要hook的地址替换为brk指令。

     static int __uprobe_register(struct inode *inode, loff_t offset, loff_t ref_ctr_offset, struct uprobe_consumer *uc)
     {
     ...
             uprobe = alloc_uprobe(inode, offset, ref_ctr_offset);
     ...
             ret = register_for_each_vma(uprobe, uc);
     ...
     }
    

    register_for_each_vma调用find_vma从进程内存描述符mm_struct中找到需要hook的地址对应的线性内存描述符vm_area_struct *vma,调用install_breakpoint将目标hook地址的指令先保存,然后向对应的线性地址写入断点指令。

    #define UPROBE_SWBP_INSN		0xcc
    
     int __weak set_swbp(struct arch_uprobe *auprobe, struct mm_struct *mm, unsigned long vaddr)
     {
     return uprobe_write_opcode(auprobe, mm, vaddr, UPROBE_SWBP_INSN);
     }
    
     static int
     register_for_each_vma(struct uprobe *uprobe, struct uprobe_consumer *new)
     {
     ...
     struct mm_struct *mm = info->mm;
     struct vm_area_struct *vma;
     ...
     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);
     }
     ...
    
     }   
    
  2. 当程序启动的时候会调用mmap系统调用来映射内存区域。在内核中mmap会调用到uprobe_mmap。uprobe_mmap会查找全局的uprobes_tree来构建符合程序地址区间的uprobe list,替换目标hook地址指令为brk指令。

uprobe执行

uprobes_init初始化的时候注册了die_chain通知链uprobe_exception_nb的notifier_call为 arch_uprobe_exception_notify, 注意这个是和体系结构相关的,不同的体系结构处理上稍微有点差异,下面以x86为例。

当brk指令执行时,会产生一个int3异常中断,处理die_chain通知链的回调函数arch_uprobe_exception_notify。

arch_uprobe_exception_notify
        -->uprobe_pre_sstep_notifier

uprobe_pre_sstep_notifier在中断上下文中,设置线程标志TIF_UPROBE 表明断点被命中。

异常执行之后,在返回用户态之前检查线程是否设置了TIF_UPROBE。如果设置了调用uprobe_notify_resume。注意这是第一次调用。

__visible noinstr void do_syscall_64(struct pt_regs *regs, int nr)
{
	add_random_kstack_offset();
	nr = syscall_enter_from_user_mode(regs, nr);

	instrumentation_begin();

	if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
		/* Invalid system call, but still a system call. */
		regs->ax = __x64_sys_ni_syscall(regs);
	}

	instrumentation_end();
	syscall_exit_to_user_mode(regs);
}
syscall_exit_to_user_mode
        -->__syscall_exit_to_user_mode_work
                -->exit_to_user_mode_prepare
                        -->exit_to_user_mode_loop
                               if (ti_work & _TIF_UPROBE) {
                                uprobe_notify_resume(regs);
                               }

当第一次进入uprobe_notify_resume时,utask->active_uprobe为空,执行handle_swbp

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找到uprobe执行注册的处理程序,即注册uprobe时注册的uprobe_consumer的处理函数。

/*
 * 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);
...
	/* change it in advance for ->handler() and restart */
	instruction_pointer_set(regs, bp_vaddr);
...
	handler_chain(uprobe, regs);
...
	if (!pre_ssout(uprobe, regs, bp_vaddr))
		return;
}

pre_ssout 这里涉及到一个 xol_area (Execute out of line area)这个放在后续分析,原始指令被brk指令替换掉,无法在存储器中执行,在一片新的区域执行。

pre_ssout 把中断异常返回地址设置为uprobes(创建的xol_area区域)中保存原始指令的地址,设置单步执行,并设置 utask->active_uprobe。

/* Prepare to single-step probed instruction out of line. */
static int
pre_ssout(struct uprobe *uprobe, struct pt_regs *regs, unsigned long bp_vaddr)
{
	struct uprobe_task *utask;
	unsigned long xol_vaddr;
	int err;

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

	xol_vaddr = xol_get_insn_slot(uprobe);
	if (!xol_vaddr)
		return -ENOMEM;

	utask->xol_vaddr = xol_vaddr;
	utask->vaddr = bp_vaddr;

	err = arch_uprobe_pre_xol(&uprobe->arch, regs);
	if (unlikely(err)) {
		xol_free_insn_slot(current);
		return err;
	}

	utask->active_uprobe = uprobe;
	utask->state = UTASK_SSTEP;
	return 0;
}

当原始的指令执行完之后返回内核陷入单步异常,执行uprobe_post_sstep_notifier设置线程标志TIF_UPROBE

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;
}

这样当返回用户态之前,检查线程设置了TIF_UPROBE标志,因为在第一次返回用户态之前utask->active_uprobe 被赋值了,在uprobe_notify_resume函数中就会调用到handle_singlestep。注意这是第二次调用。

在handle_singlestep中做一些清理性的工作,包括将单步异常的返回地址设置为hook地址的下一条指令对应的地址,清理资源,将active_uprobe设置为NULL。

最后返回到用户态的时候执行hook点之后的指令。

uprobe event语法

event语法格式规则与kprobetrace是类似的。

p[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS] : Set a uprobe
r[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS] : Set a return uprobe (uretprobe)
p[:[GRP/][EVENT]] PATH:OFFSET%return [FETCHARGS] : Set a return uprobe (uretprobe)
-:[GRP/][EVENT]                           : Clear uprobe or uretprobe event

GRP           : Group name. If omitted, "uprobes" is the default value.
EVENT         : Event name. If omitted, the event name is generated based
                on PATH+OFFSET.
PATH          : Path to an executable or a library.
OFFSET        : Offset where the probe is inserted.
OFFSET%return : Offset where the return probe is inserted.

FETCHARGS     : Arguments. Each probe can have up to 128 args.
 %REG         : Fetch register REG
 @ADDR        : Fetch memory at ADDR (ADDR should be in userspace)
 @+OFFSET     : Fetch memory at OFFSET (OFFSET from same file as PATH)
 $stackN      : Fetch Nth entry of stack (N >= 0)
 $stack       : Fetch stack address.
 $retval      : Fetch return value.(\*1)
 $comm        : Fetch current task comm.
 +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*2)(\*3)
 \IMM         : Store an immediate value to the argument.
 NAME=FETCHARG     : Set NAME as the argument name of FETCHARG.
 FETCHARG:TYPE     : Set TYPE as the type of FETCHARG. Currently, basic types
                     (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types
                     (x8/x16/x32/x64), "string" and bitfield are supported.

(\*1) only for return probe.
(\*2) this is useful for fetching a field of data structures.
(\*3) Unlike kprobe event, "u" prefix will just be ignored, because uprobe
      events can access only user-space memory.

FETCHARGS 指定 kprobe event 数据显示的格式

  1. s、u: 分别表示signed、unsigned;
  2. x:表示十六进制;
  3. 数字:s、u十进制,x十六进制,如果没有类型转换,则使用“x32”或“x64”取决于体系结构(例如,x86-32 使用 x32,x86-64 使用 x64);
  4. 字符串:在内存中读取一个“null-terminated”的字符串;
  5. 对于$comm,默认类型是“string”;任何其他类型均无效。

实验

实例demo代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int func_1_num = 0;
static int func_2_num = 0;

static void func_1(void)
{
        printf("func 1\n");
        func_1_num++;
}

static void func_2(void)
{
        printf("func 2\n");
        func_2_num++;
}

int main(int argc, char **argv)
{
        int num = 0;

        while(1) {
                sleep(1);
                num++;
                if (num == 1) {
                        func_1();
                }else if (num == 2) {
                        func_2();
                }else {
                        break;
                }
        }

        return 0;
}

编译

# gcc uprobe.c -o test

查看汇编代码, 汇编代码占用篇幅比较多,这里只列出了实验需要的部分。 # objdump -d test

0000000000001169 <func_1>:
    1169:       f3 0f 1e fa             endbr64
    116d:       55                      push   %rbp
    116e:       48 89 e5                mov    %rsp,%rbp
    1171:       48 8d 05 8c 0e 00 00    lea    0xe8c(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    1178:       48 89 c7                mov    %rax,%rdi
    117b:       e8 e0 fe ff ff          call   1060 <puts@plt>
    1180:       8b 05 8e 2e 00 00       mov    0x2e8e(%rip),%eax        # 4014 <func_1_num>
    1186:       83 c0 01                add    $0x1,%eax
    1189:       89 05 85 2e 00 00       mov    %eax,0x2e85(%rip)        # 4014 <func_1_num>
    118f:       90                      nop
    1190:       5d                      pop    %rbp
    1191:       c3                      ret

0000000000001192 <func_2>:
    1192:       f3 0f 1e fa             endbr64
    1196:       55                      push   %rbp
    1197:       48 89 e5                mov    %rsp,%rbp
    119a:       48 8d 05 6a 0e 00 00    lea    0xe6a(%rip),%rax        # 200b <_IO_stdin_used+0xb>
    11a1:       48 89 c7                mov    %rax,%rdi
    11a4:       e8 b7 fe ff ff          call   1060 <puts@plt>
    11a9:       8b 05 69 2e 00 00       mov    0x2e69(%rip),%eax        # 4018 <func_2_num>
    11af:       83 c0 01                add    $0x1,%eax
    11b2:       89 05 60 2e 00 00       mov    %eax,0x2e60(%rip)        # 4018 <func_2_num>
    11b8:       90                      nop
    11b9:       5d                      pop    %rbp
    11ba:       c3                      ret

探测函数入口func_1的0x1169地址处,func_2的0x1192地址处。 注意这里需要使用程序所在位置路径,绝对路径或者相对路径。

# echo 'p:func1 /code/tracing/test:0x1169' > /sys/kernel/debug/tracing/uprobe_events
# echo 'p:func2 /code/tracing/test:0x1192' >> /sys/kernel/debug/tracing/uprobe_events
# cat /sys/kernel/debug/tracing/uprobe_events
p:uprobes/func1 /code/tracing/test:0x0000000000001169
p:uprobes/func2 /code/tracing/test:0x0000000000001192

使能探测追踪

# ls -l /sys/kernel/debug/tracing/events/uprobes/
total 0
-rw-r----- 1 root root 0 11月  9 12:50 enable
-rw-r----- 1 root root 0 11月  9 12:47 filter
drwxr-x--- 2 root root 0 11月  9 12:47 func1
drwxr-x--- 2 root root 0 11月  9 12:47 func2

# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable

这里同时探测两个点,所有就不用到func1 func2目录下分别使能了。

观测探测日志追踪记录

root@kube-virtual-machine:/code/tracing# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 2/2   #P:2
#
#                                _-----=> irqs-off/BH-disabled
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| / _-=> migrate-disable
#                              |||| /     delay
#           TASK-PID     CPU#  |||||  TIMESTAMP  FUNCTION
#              | |         |   |||||     |         |
            test-100587  [000] DNZff 106874.691156: func1: (0x55a837234169)
            test-100587  [000] DNZff 106875.693495: func2: (0x55a837234192)

ebpf系列预告

追踪系列已接近尾声,时时候考虑开始下个系列了。

下一个系列依然是云原生相关,而且是比较火热的ebpf。

敬请期待。。。。

ebpf系列部分专题

参考

https://opensource.com/article/17/7/dynamic-tracing-linux-user-and-kernel-space https://switch-router.gitee.io/blog/uprobe-kprobe/ https://blog.csdn.net/u012489236/article/details/127954817 https://blog.seeflower.dev/archives/90/ https://github.com/brendangregg/perf-tools/blob/master/user/uprobe https://www.expoli.tech/articles/2023/01/06/bpf-example-program-uprobes https://blog.quarkslab.com/defeating-ebpf-uprobe-monitoring.html https://www.cnblogs.com/revercc/p/17803876.html https://docs.kernel.org/trace/uprobetracer.html?highlight=uprobe

欢迎大家转发分享。未经授权,严禁任何复制、转载、摘编或以其它方式进行使用,转载须注明来自eBPFLAB并附上本文链接。如果有侵犯到您权益的地方,请及时联系我删除。