二十分钟Linux ftrace原理抛砖引玉

我们可以通过objdump -D看到内核模块或者用户态程序里面的函数开头的指令,以便知道如果想hook它的话,要预先备份多少指令。

但是如何看到内核函数的开头几个指令呢?

我试图去objdump系统boot目录下的vmlinux,但是什么也看不到。这里说一句,如果你的/boot目录下只有vmlinuz,那么首先你必须将其解压成vmlinux,这个比较容易,内核源码或者内核头文件开发包中都自带了这个脚本:

/usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux vmlinuz-$(uname -r) > vmlinux
1
然后去objdump这个生成的vmlinux的话,很遗憾,没有函数的名字。此时我能想到的办法就是自己写一个模块,然后从/proc/kallsyms文件中根据函数名字找到函数的起始地址,将此地址作为参数传递给内核模块,然后内核模块从该地址出开始打印即可,类似:

[root@localhost ~]# addr=`cat /proc/kallsyms |grep ip_rcv$|cut -d " " -f 1`
[root@localhost ~]# echo $addr
ffffffff8ae36bc0
[root@localhost ~]# insmod ./getinst.ko addr=$addr
[root@localhost ~]# dmesg
...
66 66 66 66 90 55 48 89 e5 41 55 ...

嗯,这严格遵循了UNIX的哲学,用小玩意儿组合的方式完成一件场面相对大的事。

但这并不好。我希望vmlinux作为一个二进制程序被objdump,因此我需要对应当前uname -r版本的debuginfo中的vmlinux,debuginfo中携带大量的字符符号信息。

于是就从centos官网上下载了一个,安装之,最后其vmlinux的位置在:

/usr/lib/debug/usr/lib/modules/`uname -r`/vmlinux
1
用上面的方法将其dump:

[root@localhost ~]# objdump -D /usr/lib/debug/usr/lib/modules/`uname -r`/vmlinux >./dump
1
很久的时间,最终dump的大小是:

[root@localhost ]# ll
总用量 4263968
...
-rw-r--r--. 1 root root 3513205250 11月 23 23:59 dump

5
使用vim打开是比较费劲的,我是将其切割打开的,来看看同样的ip_rcv函数:

...
1815334 ffffffff81636bc0 <ip_rcv>:
1815335 ffffffff81636bc0:   e8 2b 25 0f 00          callq  ffffffff817290f0 <__fentry__>
1815336 ffffffff81636bc5:   55                      push   %rbp
1815337 ffffffff81636bc6:   48 89 e5                mov    %rsp,%rbp
1815338 ffffffff81636bc9:   41 55                   push   %r13
1815339 ffffffff81636bcb:   41 54                   push   %r12

有点问题, 同样都是0xffffffff81636bc0这个地址,前5个字节怎么和我用模块dump下来的不一样??

这个时候,我们看看objdump结果的 callq ffffffff817290f0 <fentry> ,我们看看 fentry 到底是什么:

...
2085623 ffffffff817290f0 <__fentry__>:
2085624 ffffffff817290f0:   c3                      retq
2085625 ffffffff817290f1:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
2085626 ffffffff817290f6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
2085627 ffffffff817290fd:   00 00 00

非常简单,你可以理解为就是一个ret(后面的各种尺寸的nop都是为了为了指令替换做支撑的)。

然而我们知道,这么一个在应用程序看来看似没用的call-and-ret序列,在CPU看来场面确实及其宏大的,所以在Linux内核启动的过程中,这个call __fentry__被替换成了标准的 5字节nop ,即 66 66 66 66 90,也就是我的模块里打印的结果。

也就是说Linux内核启动的过程中对每一个函数进行了一次 hot hook。所以说,静态的vmlinux中函数开头的5字节指令被动态替换成了运行时的nop!

那么,何必多此一举呢?

这就要引入ftrace了。仔细看dump文件里的下面这些信息:

...
2085650 ffffffff8172915c <ftrace_call>:
2085651 ffffffff8172915c:   e8 2f 00 00 00          callq  ffffffff81729190 <ftrace_stub>
2085652 ffffffff81729161:   4c 8b 4c 24 40          mov    0x40(%rsp),%r9
2085653 ffffffff81729166:   4c 8b 44 24 48          mov    0x48(%rsp),%r8
2085654 ffffffff8172916b:   48 8b 7c 24 70          mov    0x70(%rsp),%rdi
2085655 ffffffff81729170:   48 8b 74 24 68          mov    0x68(%rsp),%rsi
2085656 ffffffff81729175:   48 8b 54 24 60          mov    0x60(%rsp),%rdx
2085657 ffffffff8172917a:   48 8b 4c 24 58          mov    0x58(%rsp),%rcx
2085658 ffffffff8172917f:   48 8b 44 24 50          mov    0x50(%rsp),%rax
2085659 ffffffff81729184:   48 81 c4 a8 00 00 00    add    $0xa8,%rsp
2085660
2085661 ffffffff8172918b <ftrace_graph_call>:
2085662 ffffffff8172918b:   e9 00 00 00 00          jmpq   ffffffff81729190 <ftrace_stub>
2085663
2085664 ffffffff81729190 <ftrace_stub>:
2085665 ffffffff81729190:   c3                      retq
2085666 ffffffff81729191:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
2085667 ffffffff81729196:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
2085668 ffffffff8172919d:   00 00 00

其实这些辅助函数连同__fentry__就是用来支撑ftrace机制的,使得你一旦开启ftrace,便可以动态地对内核函数进行跟踪调试,非常方便,当然,如果你不开启ftrace,那么额外的支撑指令就像nop一样虚无。

这里只是花了20分钟时间抛个砖,顺势看下去,就会理解ftrace的原理和应用了。

顺便说一下,内核在使能ftrace的时候,其实也是需要将5字节nop换回call指令的,这又是一次hot hook的过程,为了防止 CPU0在取指第3个字节的时候,CPU1替换了第4个字节,导致CPU0误解了完整的5字节指令,造成不可预知的后果。 内核采用的机制和我前段时间hot hook时使用的机制是一致的,即先原子替换第一个字节为 0xcc, 即一个断点指令,然后再统一替换后面的。只不过我是借用了kprobe的int3,而ftrace则是直接替换,具体参见下面的函数:

void ftrace_replace_code(int enable);
 

猜你喜欢

转载自blog.csdn.net/qq_42763389/article/details/88354164