1, 背景
之前发过一个介绍objtrace的帖子(https://blog.csdn.net/chensong_2000/article/details/125277794?spm=1001.2014.3001.5501),主要介绍了objtrace的功能和实现。
在作者jeff老师提交到内核的过程中,maintainer提出要使用kprobe的格式和方法来解析,存储和打印参数。
我觉得这个功能比较有趣,便尝试这实现一下。
2, kprobe的实现 (echo 'p vfs_open arg1=+0x38(+0x8($arg1)):string' > ./kprobe_events)
首先分析一下kprobe的实现,以“echo 'p vfs_open arg1=+0x38(+0x8($arg1)):string' > ./kprobe_events”为例,该命令在函数vfs_open中设置了一个kprobe,vfs_open一旦被调用,kprobe就在trace中打印第一个参数path的第二个元素dentry里面的d_iname,path和dentry的定义如下:
include/linux/path.h
struct path {
struct vfsmount *mnt;
struct dentry *dentry; //+0x08
} __randomize_layout;
include/linux/dcache.h
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_spinlock_t d_seq; /* per dentry seqlock */
...
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ //+0x38
运行效果如下:
/ # cd /sys/kernel/debug/tracing/
/sys/kernel/debug/tracing # echo 'p vfs_open arg1=+0x38(+0x8($arg1)):string' > ./kprobe_events
/sys/kernel/debug/tracing # echo 1 > ./events/kprobes/p_vfs_open_0/enable
/sys/kernel/debug/tracing # cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 3/3 #P:2
#
# _-----=> irqs-off/BH-disabled
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / _-=> migrate-disable
# |||| / delay
# TASK-PID CPU# ||||| TIMESTAMP FUNCTION
# | | | ||||| | |
sh-132 [000] ..... 41.541319: p_vfs_open_0: (vfs_open+0x0/0x40) arg1=".ash_history"
cat-134 [001] ..... 41.549021: p_vfs_open_0: (vfs_open+0x0/0x40) arg1="busybox"
cat-134 [001] ..... 41.555422: p_vfs_open_0: (vfs_open+0x0/0x40) arg1="trace"
2.1创建:__trace_kprobe_create
当输入命令“echo 'p vfs_open arg1=+0x38(+0x8($arg1)):string' > ./kprobe_events”之后,代码运行到__trace_kprobe_create,准备对“arg1=+0x38(+0x8($arg1)):string”进行解析。
static int __trace_kprobe_create(int argc, const char *argv[])
{
tk = alloc_trace_kprobe(group, event, addr, symbol, offset, maxactive,
argc - 2, is_return); --- (1)
for (i = 0; i < argc && i < MAX_TRACE_ARGS; i++) {
trace_probe_log_set_index(i + 2);
ret = traceprobe_parse_probe_arg(&tk->tp, i, argv[i], flags); --- (2)
if (ret)
goto error; /* This can be -ENOMEM */
}
}
static struct trace_kprobe *alloc_trace_kprobe(const char *group,...)
{
tk = kzalloc(struct_size(tk, tp.args, nargs), GFP_KERNEL); --- (3)
}
static int
parse_probe_arg(char *arg, const struct fetch_type *type,
switch (arg[0]) { --- (4)
case '$':
...
case '+': /* deref memory */
case '-':
...
ret = parse_probe_arg(arg, t2, &code, end, flags, offs); (5)
}
(1)首先分配一个tk(struct trace_kprobe)
(2)接下来调用traceprobe_parse_probe_arg对每一个参数进行分析,本例中只有一个,就是“arg1=+0x38(+0x8($arg1)):string”。
(3)分配tk的时候,会根据参数的数量创建若干个probe_arg。
(4)traceprobe_parse_probe_arg --> traceprobe_parse_probe_arg_body -->parse_probe_arg来分析参数。
(5)parse_probe_arg是递归调用的,第一次的参数是“+0x38(+0x8($arg1)):string”,得到“0x38”这个偏移量,保存在tp->args->code[0]中,code为“struct fetch_insn”。
第二次分析参数“+0x8($arg1)):string”,得到0x8这个偏移量,保存在tp->args->code[1]中。
除此之外,分析过程还获得了参数的index, arg1位vfs_open的第一个参数,即path;还获得了所跟踪参数的类型,string。
2.2触发:__kprobe_trace_func
当输入“echo 1 > events/kprobes/p_vfs_open_0/enable”之后,每次调用到vfs_open, kprobe就开始工作,调用栈为:
[ 595.802063] kprobe_trace_func+0x1ed/0x270
[ 595.802090] ? avc_has_perm_noaudit+0xd4/0x150
[ 595.802123] ? vfs_open+0x5/0x30
[ 595.802213] kprobe_dispatcher+0x3d/0x60
[ 595.802237] ? vfs_open+0x1/0x30
[ 595.802264] kprobe_ftrace_handler+0x156/0x1e0
[ 595.802302] 0xffffffffc024f0c8
[ 595.802336] ? vfs_open+0x1/0x30
[ 595.802363] vfs_open+0x5/0x30
[ 595.802386] path_openat+0xb8b/0x1000
[ 595.802421] do_filp_open+0xb2/0x120
[ 595.802466] do_sys_openat2+0x24a/0x320
[ 595.802494] do_sys_open+0x44/0x80
[ 595.802520] do_syscall_64+0x38/0x90
[ 595.802548] entry_SYSCALL_64_after_hwframe+0x63/0xcd
__kprobe_trace_func的函数分析如下:
static nokprobe_inline void
__kprobe_trace_func(struct trace_kprobe *tk, struct pt_regs *regs,
struct trace_event_file *trace_file)
{
entry = trace_event_buffer_reserve(&fbuffer, trace_file,
sizeof(*entry) + tk->tp.size + dsize); --- (1)
fbuffer.regs = regs;
entry = fbuffer.entry = ring_buffer_event_data(fbuffer.event);
entry->ip = (unsigned long)tk->rp.kp.addr;
store_trace_args(&entry[1], &tk->tp, regs, sizeof(*entry), dsize); --- (2)
trace_event_buffer_commit(&fbuffer); --- (3)
}
(1) ringbuffer中申请一个entry,用来保存获取的参数值,本例中就是path->dentry->d_iname。
(2) regs为vfs_open的参数,store_trace_args根据tp里解析的规则,从regs获取参数地址,进而获取参数值,保存在entry里。
(3) 将entry设置为commit状态,后续打印到trace文件中使用。
那么,store_trace_args是如何获取参数地址和值的呢,继续看如下代码:
process_fetch_insn(struct fetch_insn *code, void *rec, void *dest,
case FETCH_OP_ARG:
val = regs_get_kernel_argument(regs, code->param); ---(1)
return process_fetch_insn_bottom(code, val, dest, base); --- (2)
process_fetch_insn_bottom(struct fetch_insn *code, unsigned long val,
switch (code->op) {
...
case FETCH_OP_ST_STRING:
loc = *(u32 *)dest;
ret = fetch_store_string(val + code->offset, dest, base); ---(3)
break;
(1) store_trace_args调用process_fetch_insn,根据参数的所以,从regs获取参数的地址,本例中val就是path的地址。
(2) 继续调用process_fetch_insn_bottom,根据保存在tp->args->code中的信息获取path->dentry->d_iname的地址。
(3) 根据path->dentry->d_iname的地址将其内容拷贝到entry的地址中。
到此为止,已经获取到了vfs_open的第一个参数path的+8 -- +38的信息,并保存在ringbuffer的一个entry里,接下来要打印出来。
2.3打印:print_kprobe_event
print_kprobe_event的调用栈为:
[ 61.130926] RIP: 0010:print_kprobe_event+0x1d0/0x1e0
[ 61.131306] Code: 89 ff e8 d3 da fc ff e9 d5 fe ff ff 0f 0b e9 ce fe ff ff 48 c7 c7 7c 89 5d 82 48 80
[ 61.132236] RSP: 0018:ffffc90000403c80 EFLAGS: 00000282
[ 61.132564] RAX: 0000000000000000 RBX: 0000000000000001 RCX: 0000000000000000
[ 61.132910] RDX: 0000000000000001 RSI: ffffffff811245ea RDI: ffffffff811245ea
[ 61.133380] RBP: ffffc90000403cb8 R08: 0000000000000000 R09: ffffc90000403ae8
[ 61.134076] R10: 0000000000000001 R11: 0000000000000001 R12: ffff8880066d8000
[ 61.135544] R13: 0000000000000000 R14: ffff8880066d90b0 R15: ffff8880066d90b0
[ 61.136376] FS: 0000000000a078c0(0000) GS:ffff88803d900000(0000) knlGS:0000000000000000
[ 61.136965] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 61.137506] CR2: 0000000000530294 CR3: 00000000043f8000 CR4: 00000000000006e0
[ 61.138579] Call Trace:
[ 61.140448]
[ 61.141013] ? x86_event_sysfs_show+0x153/0x180
[ 61.141772] print_trace_line+0x262/0x570
[ 61.142530] ? ring_buffer_iter_advance+0x32/0x40
[ 61.143680] s_show+0x50/0x170
[ 61.144024] seq_read_iter+0x2a7/0x450
[ 61.144354] seq_read+0xad/0xe0
[ 61.144644] vfs_read+0xb2/0x300
[ 61.144964] ksys_read+0x67/0xf0
[ 61.145239] __x64_sys_read+0x1a/0x20
[ 61.145469] do_syscall_64+0x3b/0x90
[ 61.145682] entry_SYSCALL_64_after_hwframe+0x63/0xcd
[ 61.145913] RIP: 0033:0x4bdff2
代码为:
print_kprobe_event(struct trace_iterator *iter, int flags,
if (print_probe_args(s, tp->args, tp->nr_args, ---(1)
(u8 *)&field[1], field) < 0)
goto out;
(1) print_kprobe_event调用print_probe_args根据参数的类型,将保存在entry里的内容打印到trace文件中。
3,objtrace的修改
objtrace的命令需要修改为如下格式
echo 'p vfs_open $arg1:u64' > ./kprobe_events
echo 'objtrace:+0x38(+0x8($arg1)):string if comm == "cat"' > ./events/kprobes/p_vfs_open_0/trigger
cat trace
一开始准备在kprobe中也使用string类型,即“echo 'p vfs_open arg1=+0x38(+0x8($arg1)):string' > ./kprobe_events”,后来在调试中发现了问题,必须使用u8,u32,或u64,原因在后面分析代码的时候会说明。
3.1 traceprobe创建和parse (event_object_trigger_parse)
event_object_trigger_parse所做的工作跟前面章节所讲的创建kprobe的函数__trace_kprobe_create所做的工作基本相同,就是创建一个tp(struct trace_probe),并解析参数“+0x38(+0x8($arg1)):string”,将结果保存在tp->args->code[]里面,代码如下:
event_object_trigger_parse(struct event_command *cmd_ops,...
{
/* parse params into a trace_probe*/
obj_trig_data = kzalloc(struct_size(obj_trig_data, objprobe.tp.args, 1), GFP_KERNEL);
if (!obj_trig_data)
return -ENOMEM;
//TODO: argv_slipt argc argv
ret = trace_probe_init(&obj_trig_data->objprobe.tp, file->event_call->name, cmd, false);
if (ret)
goto obj_free;
ret = traceprobe_parse_probe_arg(&obj_trig_data->objprobe.tp, 0, param, TPARG_FL_KERNEL | TPARG_FL_FENTRY);
if (ret)
goto obj_free;
}
这里我还有一个TODO没有做,即在多参数的情况下(+0x38(+0x8($arg1)):string +0x16($arg2):u64),调用argv_split分解参数,每个参数创建对应的tp并解析。
3.2获取obj的地址 (trace_object_trigger)
创建了kprobe和trigger之后,当系统运行到“vfs_open函数”,kprobe回调函数__kprobe_trace_func会被触发,进而触发trigger的函数trace_object_trigger,调用栈如下:
[ 248.070455] RIP: 0010:trace_object_trigger+0x184/0x1a0
[ 248.070462] Code: 7a ae 48 c7 c6 2d 69 3d ad e8 28 82 fe ff eb bc 48 c7 c6 60 05 43 ae 48 c7 c7 80 ce 7a ae c6 05 2c 11 95 01 01 e8 db 84 9d 00 <0f> 0b eb 9e 48 85 db 0f 84 16 ff ff ff 48 c7 c0 80 96 bc ae e9 d1
[ 248.070467] RSP: 0018:ffffb122007d36a8 EFLAGS: 00010086
[ 248.070475] RAX: 0000000000000000 RBX: ffffa0c980055000 RCX: 0000000000000000
[ 248.070480] RDX: 0000000000000005 RSI: ffffffffae7a5fb1 RDI: 0000000000000001
[ 248.070484] RBP: ffffb122007d36d8 R08: 0000000000000000 R09: ffffb122007d3498
[ 248.070489] R10: 0000000000000001 R11: 0000000000000001 R12: ffffffffaebc58e0
[ 248.070495] R13: ffffa0c886cc8700 R14: 0000000000000286 R15: ffffa0c980055148
[ 248.070500] FS: 00007f6c3540e580(0000) GS:ffffa0c99bb80000(0000) knlGS:0000000000000000
[ 248.070505] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 248.070510] CR2: 00007f6c3531d600 CR3: 00000000016e4006 CR4: 00000000001706e0
[ 248.070515] Call Trace:
[ 248.070520]
[ 248.070533] trace_object_count_trigger+0x1e/0x30
[ 248.070541] event_triggers_call+0x5d/0xe0
[ 248.070556] trace_event_buffer_commit+0x1a2/0x260
[ 248.070573] kprobe_trace_func+0x1ad/0x2a0
[ 248.070602] kprobe_dispatcher+0x42/0x70
代码分析:
__kprobe_trace_func(struct trace_kprobe *tk, struct pt_regs *regs, ...
store_trace_args(&entry[1], &tk->tp, regs, sizeof(*entry), dsize); --- (1)
trace_event_buffer_commit(&fbuffer); --- (2)
...
void trace_event_buffer_commit(struct trace_event_buffer *fbuffer)
if (__event_trigger_test_discard(file, fbuffer->buffer, fbuffer->event,
fbuffer->entry, &tt))
__event_trigger_test_discard(struct trace_event_file *file, ...
*tt = event_triggers_call(file, buffer, entry, event);
event_triggers_call(struct trace_event_file *file,...
data->ops->trigger(data, buffer, rec, event); --- (3)
(1) store_trace_args之前分析过,主要是从寄存器获取参数地址,并按照tp中保存的参数的索引,偏移量,类型等信息获取参数地址和值,保存在entry中。
(2)调用trace_event_buffer_commit将entry提交到ringbuffer中。
(3)最后调用trigger注册的回调函数,由trigger进行处理,其中entry作为参数,会传递给trigger的回调函数,本例的回调函数为trace_object_trigger。
static void
trace_object_trigger(struct event_trigger_data *data,
struct trace_buffer *buffer, void *rec,
struct ring_buffer_event *event)
{
field = obj_trig_data->field;
memcpy(&obj, rec + field->offset, sizeof(obj)); ---(1)
obj_trig_data->objprobe.obj_addr = obj;
/* set the offset from the special object and the type size of the value*/
set_trace_object(data);
}
(1)在本函数中,最主要的工作就是将保存在entry中的参数地址,本例中为vfs_open的第一个参数path,从entry中读取出来,并保存在obj_ins[]里面,这样,后面的函数如果用到了这个参数,就可以被捕获到,并被追踪,这就是objtrace的目的。
在处理这个函数的时候,遇到了一个很棘手的问题,当创建kprobe的命令为“echo 'p vfs_open name=+0x38(+0x8($arg1)):string' > ./kprobe_events”的时候,在entry中只能得到文件名的内容,而我们希望在这个地方得到path的地址,我们使用qemu+gdb,可以看到entry里的内容:
其中高亮的地方是命令“cat trace”的文件名trace的ascii码,即“0x74,0x72,0x61,0x63,0x65”,而并不是我们所期待的path地址,类似“0xffffac07c05bfd90”。
后来使用gdb单步跟踪process_fetch_insn_bottom时发现了问题:
当类型为string是,会进入fetch_store_sting将内容,而不是地址,保存在entry里。
所以需要将创建kprobe的命令改为“echo 'p vfs_open $arg1:u64' > ./kprobe_events”
这样,就会进入fetch_store_raw,将地址保存在entry中,在trace_object_trigger就会得到path的地址,然后保存在obj_ins[]中进行跟踪。
3.3 打印到trace文件
1)提交ringbuffer (trace_object_events_call, store_trace_args)
这部分跟kprobe基本相似,但写代码的时候,由于遗漏了一个小细节,后面导致功能不能实现,调试了很久才发现,后面会讲到。
由于objtrace命令为“echo 'objtrace:+0x38(+0x8($arg1)):string if comm == "cat"' > ./events/kprobes/p_vfs_open_0/trigger”,所指定的类型为string,所以会走到fetch_store_sting将内容保存在entry里。
2)打印(trace_object_print,print_probe_args)
最后到了按格式打印的函数。
一开始,不能按需求打印出文件名,如果命令是“cat trace”的话,就期望打印出“trace”,但结果并非如此,gdb看一下:
这里面只能看到entry里另外一个参数“0xffff8880066816f8”,并没有期望看到的d_iname的字符串。
原来在申请entry的时候调用了函数
event = trace_buffer_lock_reserve(buffer, TRACE_OBJECT,
sizeof(*entry) , trace_ctx);
后来,经过了很长时间的调试,才发现,原来是size指定错了,应该修改为:
event = trace_buffer_lock_reserve(buffer, TRACE_OBJECT,
sizeof(*entry) + tp->size + dsize , trace_ctx);
最后看一下效果:
此时,field里既有tp的地址0xffff88800669b6f8,也有trace的ascii,即“0x74,0x72,0x61,0x63,0x65”。
arg1打印出vfs_open的第一个参数path偏移0x8地址的dentry,再偏移0x38地址的d_iname的内容,也就是文件名。
4,总结
objtrace很有创意,值得深入研究。
qemu+gdb很适合这个场景, 具体方法见(https://blog.csdn.net/chensong_2000/article/details/127447378?spm=1001.2014.3001.5501)。
能够在不赶工期,没人催促的情况下调试代码,感觉很好。
走了弯路也没关系,可以促使你阅读更多的代码,在阅读代码的时候,还给kprobe贡献了两个patch。