可观测性 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激活
分为两种情况,一种是通过命令行,另外一种是程序启动时加载。
通过命令行执行
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); } ... }
当程序启动的时候会调用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 数据显示的格式
- s、u: 分别表示signed、unsigned;
- x:表示十六进制;
- 数字:s、u十进制,x十六进制,如果没有类型转换,则使用“x32”或“x64”取决于体系结构(例如,x86-32 使用 x32,x86-64 使用 x64);
- 字符串:在内存中读取一个“null-terminated”的字符串;
- 对于$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并附上本文链接。如果有侵犯到您权益的地方,请及时联系我删除。