objtrace代码调试

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里的内容:

objtrace代码调试_第1张图片

其中高亮的地方是命令“cat trace”的文件名trace的ascii码,即“0x74,0x72,0x61,0x63,0x65”,而并不是我们所期待的path地址,类似“0xffffac07c05bfd90”。

后来使用gdb单步跟踪process_fetch_insn_bottom时发现了问题:

objtrace代码调试_第2张图片

当类型为string是,会进入fetch_store_sting将内容,而不是地址,保存在entry里。

所以需要将创建kprobe的命令改为“echo 'p vfs_open $arg1:u64' > ./kprobe_events”

objtrace代码调试_第3张图片

这样,就会进入fetch_store_raw,将地址保存在entry中,在trace_object_trigger就会得到path的地址,然后保存在obj_ins[]中进行跟踪。

objtrace代码调试_第4张图片

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看一下:

 objtrace代码调试_第5张图片

这里面只能看到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);

 最后看一下效果:

objtrace代码调试_第6张图片

此时,field里既有tp的地址0xffff88800669b6f8,也有trace的ascii,即“0x74,0x72,0x61,0x63,0x65”。

objtrace代码调试_第7张图片

 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。

你可能感兴趣的:(linux,运维,服务器,ftrace,tracer)