使用ftrace和kprobes捕获用户空间程序集(通过使用虚拟地址转换)?

时间:2014-02-07 13:30:32

标签: linux debugging linux-kernel ftrace

对于这篇冗长的帖子道歉,我在以较短的方式制定它时遇到了麻烦。此外,这可能更适合Unix& Linux Stack Exchange,但我首先在SO尝试,因为有一个ftrace标记。

无论如何 - 我想观察使用function_graph在完整ftrace捕获的上下文中执行用户程序的机器指令。一个问题是我需要这个旧内核:

$ uname -a
Linux mypc 2.6.38-16-generic #67-Ubuntu SMP Thu Sep 6 18:00:43 UTC 2012 i686 i686 i386 GNU/Linux

...在这个版本中,没有UPROBES - 正如Uprobes in 3.5 [LWN.net]所述,它应该能够做到这一点。 (只要我不必修补原始内核,我就会愿意尝试用树构建的内核模块,正如User-Space Probes (Uprobes) [chunghwan.com]似乎证明的那样;但是我可以从0: Inode based uprobes [LWN.net]看,2.6可能需要一个完整的补丁

但是,在此版本中,有/sys/kernel/debug/kprobes/sys/kernel/debug/tracing/kprobe_events;和Documentation/trace/kprobetrace.txt意味着可以直接在地址上设置kprobe;即使我无法在任何地方找到如何使用它的例子。

在任何情况下,我仍然不确定要使用哪些地址 - 作为一个小例子,让我们说我想跟踪main wtest.c函数的开头计划(包括在下面)。我可以这样做来编译并获得一个机器指令汇编列表:

$ gcc -g -O0 wtest.c -o wtest
$ objdump -S wtest | less
...
08048474 <main>:
int main(void) {
 8048474:       55                      push   %ebp
 8048475:       89 e5                   mov    %esp,%ebp
 8048477:       83 e4 f0                and    $0xfffffff0,%esp
 804847a:       83 ec 30                sub    $0x30,%esp
 804847d:       65 a1 14 00 00 00       mov    %gs:0x14,%eax
 8048483:       89 44 24 2c             mov    %eax,0x2c(%esp)
 8048487:       31 c0                   xor    %eax,%eax
  char filename[] = "/tmp/wtest.txt";
...
  return 0;
 804850a:       b8 00 00 00 00          mov    $0x0,%eax
}
...

我会通过这个脚本设置ftrace日志记录:

sudo bash -c '
KDBGPATH="/sys/kernel/debug/tracing"
echo function_graph > $KDBGPATH/current_tracer
echo funcgraph-abstime > $KDBGPATH/trace_options
echo funcgraph-proc > $KDBGPATH/trace_options
echo 0 > $KDBGPATH/tracing_on
echo > $KDBGPATH/trace
echo 1 > $KDBGPATH/tracing_on ; ./wtest ; echo 0 > $KDBGPATH/tracing_on
cat $KDBGPATH/trace > wtest.ftrace
'

你可以看到debugging - Observing a hard-disk write in kernel space (with drivers/modules) - Unix & Linux Stack Exchange {(1}}中ftrace的一部分(其他复杂的)登录(我从中得到了这个例子)。

基本上,当ftrace的第一条指令 - 比如0x8048474,0x8048475,0x8048477,0x804847a,0x804847d,0x8048483的指令时,我希望在main日志中打印输出。 0x8048487 - 由(任何)CPU执行。问题是,据Anatomy of a Program in Memory : Gustavo Duarte我可以理解,这些地址是虚拟地址,从流程本身的角度来看(我收集的是,相同的视角显示/proc/PID/maps)...显然,krpobe_event我需要一个实际地址?

所以,我的想法是:如果我能找到与程序反汇编的虚拟地址相对应的物理地址(比如编写一个内核模块,它会接受pid和地址,并通过procfs返回物理地址),我可以将地址设置为一种&#34;跟踪点&#34;在上面的脚本中通过/sys/kernel/debug/tracing/kprobe_events - 并希望将它们放在ftrace日志中。原则上这可行吗?

有一个问题,我在Linux(ubuntu), C language: Virtual to Physical Address Translation - Stack Overflow上找到了:

  

在用户代码中,您无法知道与虚拟地址对应的物理地址。这是信息根本不在内核之外导出。它甚至可以随时改变,特别是如果内核决定更换你的部分进程的内存   ...
  使用systemcall / procfs将虚拟地址传递给内核并使用vmalloc_to_pfn。通过procfs / registers返回物理地址。

然而,vmalloc_to_pfn似乎并不重要:

x86 64 - vmalloc_to_pfn returns 32 bit address on Linux 32 system. Why does it chop off higher bits of PAE physical address? - Stack Overflow

  

VA:0xf8ab87fc PA使用vmalloc_to_pfn:0x36f7f7fc。但我实际上期待:0x136f7f7fc   ...
  物理地址介于4到5 GB之间。但我无法得到确切的物理地址,我只能得到切断的32位地址。有没有其他方法可以获得真实的物理地址?

所以,我不确定如何可靠地提取物理地址,以便kprobes跟踪它们 - 特别是因为&#34;它甚至可以随时改变#34;。但是在这里,我希望由于程序规模小而且微不足道,程序在跟踪时不会交换,从而可以获得适当的捕获。 (所以即使我必须多次运行上面的调试脚本,只要我希望获得&#34;正确的&#34;捕获10次(甚至100次),我和# 39;好吧。)。

请注意,我希望通过ftrace输出,以便时间戳在同一个域中表示(有关时间戳问题的说明,请参阅Reliable Linux kernel timestamps (or adjustment thereof) with both usbmon and ftrace? - Stack Overflow)。因此,即使我能提出一个gdb脚本,从用户空间运行和跟踪程序(同时获得ftrace捕获) - 我想避免因为gdb本身的开销将显示在ftrace日志中。

所以,总结一下:

  • 是否从虚拟(从可执行文件的反汇编)地址获取(可能通过单独的内核模块)物理地址的方法 - 因此它们用于触发由ftrace记录的kprobe_event - 值得追求?如果是这样,是否有任何可用于此地址转换目的的内核模块示例?
  • 我是否可以使用内核模块来注册&#34;正在执行特定内存地址时的回调/处理函数?然后我可以简单地在该函数中使用trace_printk来生成ftrace日志(或者即使没有这个,处理函数名称本身也应该显示在ftrace日志中),并且它不会&# 39;似乎会有太多的开销......

实际上,在2007年的帖子Jim Keniston - utrace-based uprobes: systemtap mailing list中,有一个11. Uprobes Example(添加到Documentation/uprobes.txt),这似乎就是一个内核模块注册处理函数。不幸的是,它使用linux/uprobes.h;我的kprobes.h只有/usr/src/linux-headers-2.6.38-16/include/linux/。此外,在我的系统上,即使systemtap抱怨CONFIG_UTRACE未启用(请参阅this comment)...所以,如果有其他任何方法我可以用来获取调试像我想要的那样跟踪,无需重新编译内核来获取探测器,知道它会很棒......


wtest.c

#include <stdio.h>
#include <fcntl.h>  // O_CREAT, O_WRONLY, S_IRUSR

int main(void) {
  char filename[] = "/tmp/wtest.txt";
  char buffer[] = "abcd";
  int fd;
  mode_t perms = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH;

  fd = open(filename, O_RDWR|O_CREAT, perms);
  write(fd,buffer,4);
  close(fd);

  return 0;
}

1 个答案:

答案 0 :(得分:1)

显然,使用3.5+内核的内置增强功能会更容易;但鉴于我的内核2.6.38的uprobes是一个非常深入的补丁(我无法在一个单独的内核模块中真正隔离,以避免修补内核),这是我可以注意到的2.6.38上的独立模块。 (由于我仍然不确定很多事情,我仍然希望看到一个可以纠正这篇文章中任何误解的答案。)

我想我到了某个地方,但不是kprobes。我不确定,但似乎我设法让物理地址正确;但是,kprobes文档是特定的,当使用&#34; @ADDR:在ADDR获取内存(ADDR应该在内核中)&#34 ;;我得到的物理地址低于0xc0000000的内核边界(但是,0xc0000000通常与虚拟内存布局一起?)。

所以我使用了硬件断点 - 模块在下面,但是需要注意的是 - 它随机行为,偶尔会导致内核哎呀!通过编译模块,并在bash中运行:

$ sudo bash -c 'KDBGPATH="/sys/kernel/debug/tracing" ;
echo function_graph > $KDBGPATH/current_tracer ; echo funcgraph-abstime > $KDBGPATH/trace_options
echo funcgraph-proc > $KDBGPATH/trace_options ; echo 8192 > $KDBGPATH/buffer_size_kb ;
echo 0 > $KDBGPATH/tracing_on ; echo > $KDBGPATH/trace'
$ sudo insmod ./callmodule.ko && sleep 0.1 && sudo rmmod callmodule && \
tail -n25 /var/log/syslog | tee log.txt && \
sudo cat /sys/kernel/debug/tracing/trace >> log.txt

...我得到一份日志。我想跟踪main()的{​​{1}}的前两条说明,对我来说是:

wtest

...在虚拟地址0x08048474和0x08048475。在$ objdump -S wtest/wtest | grep -A3 'int main' int main(void) { 8048474: 55 push %ebp 8048475: 89 e5 mov %esp,%ebp 8048477: 83 e4 f0 and $0xfffffff0,%esp 输出中,我可以得到,说:

syslog

...意味着它将虚拟地址0x08048474映射到物理地址0x639ec474。但是,物理不用于硬件断点 - 我们可以直接向... [ 1106.383011] callmodule: parent task a: f40a9940 c: kworker/u:1 p: [14] s: stopped [ 1106.383017] callmodule: - wtest [9404] [ 1106.383023] callmodule: Trying to walk page table; addr task 0xEAE90CA0 ->mm ->start_code: 0x08048000 ->end_code: 0x080485F4 [ 1106.383029] callmodule: walk_ 0x8048000 callmodule: Valid pgd : Valid pud: Valid pmd: page frame struct is @ f63e5d80; *virtual (page_address) @ (null) (is_vmalloc_addr 0 virt_addr_valid 0 virt_to_phys 0x40000000) page_to_pfn 639ec page_to_phys 0x639ec000 [ 1106.383049] callmodule: walk_ 0x80483c0 callmodule: Valid pgd : Valid pud: Valid pmd: page frame struct is @ f63e5d80; *virtual (page_address) @ (null) (is_vmalloc_addr 0 virt_addr_valid 0 virt_to_phys 0x40000000) page_to_pfn 639ec page_to_phys 0x639ec000 [ 1106.383067] callmodule: walk_ 0x8048474 callmodule: Valid pgd : Valid pud: Valid pmd: page frame struct is @ f63e5d80; *virtual (page_address) @ (null) (is_vmalloc_addr 0 virt_addr_valid 0 virt_to_phys 0x40000000) page_to_pfn 639ec page_to_phys 0x639ec000 [ 1106.383083] callmodule: physaddr : (0x080483c0 ->) 0x639ec3c0 : (0x08048474 ->) 0x639ec474 [ 1106.383106] callmodule: 0x08048474 id [3] [ 1106.383113] callmodule: 0x08048475 id [4] [ 1106.383118] callmodule: (( 0x08048000 is_vmalloc_addr 0 virt_addr_valid 0 )) [ 1106.383130] callmodule: cont pid task a: eae90ca0 c: wtest p: [9404] s: runnable [ 1106.383147] initcall callmodule_init+0x0/0x1000 [callmodule] returned with preemption imbalance [ 1106.518074] callmodule: < exit 提供虚拟地址;但是,我们也需要提供该过程的register_user_hw_breakpoint。有了这个,我可以在task_struct输出中得到类似的内容:

ftrace

...其中与程序集对应的跟踪由断点ID标记。值得庆幸的是,正如预期的那样,它们是正确的;但是,... 597.907256 | 1) wtest-5339 | | handle_mm_fault() { ... 597.907310 | 1) wtest-5339 | + 35.627 us | } 597.907311 | 1) wtest-5339 | + 46.245 us | } 597.907312 | 1) wtest-5339 | + 56.143 us | } 597.907313 | 1) wtest-5339 | 1.039 us | up_read(); 597.907317 | 1) wtest-5339 | 1.285 us | native_get_debugreg(); 597.907319 | 1) wtest-5339 | 1.075 us | native_set_debugreg(); 597.907322 | 1) wtest-5339 | 1.129 us | native_get_debugreg(); 597.907324 | 1) wtest-5339 | 1.189 us | native_set_debugreg(); 597.907329 | 1) wtest-5339 | | () { 597.907333 | 1) wtest-5339 | | /* callmodule: hwbp hit: id [3] */ 597.907334 | 1) wtest-5339 | 5.567 us | } 597.907336 | 1) wtest-5339 | 1.123 us | native_set_debugreg(); 597.907339 | 1) wtest-5339 | 1.130 us | native_get_debugreg(); 597.907341 | 1) wtest-5339 | 1.075 us | native_set_debugreg(); 597.907343 | 1) wtest-5339 | 1.075 us | native_get_debugreg(); 597.907345 | 1) wtest-5339 | 1.081 us | native_set_debugreg(); 597.907348 | 1) wtest-5339 | | () { 597.907350 | 1) wtest-5339 | | /* callmodule: hwbp hit: id [4] */ 597.907351 | 1) wtest-5339 | 3.033 us | } 597.907352 | 1) wtest-5339 | 1.105 us | native_set_debugreg(); 597.907358 | 1) wtest-5339 | 1.315 us | down_read_trylock(); 597.907360 | 1) wtest-5339 | 1.123 us | _cond_resched(); 597.907362 | 1) wtest-5339 | 1.027 us | find_vma(); 597.907364 | 1) wtest-5339 | | handle_mm_fault() { ... 还在其间捕获了一些调试命令。无论如何,这是我想看到的。

以下是关于该模块的一些注意事项:

  • 大部分模块来自Execute/invoke user-space program, and get its pid, from a kernel module;启动用户进程并获取pid的位置
    • 因为我们必须到task_struct去到pid;在这里我保存了两个(这是多余的)
  • 不导出功能符号的地方;如果符号在ftrace中,那么我使用一个指向地址的函数指针;否则从源
  • 复制其他所需的功能
  • 我不知道如何开始停止用户空间进程,所以在产生后我发出kallsyms(就其本身而言,似乎有点不可靠),并将状态设置为SIGSTOP)。
    • 我可能仍然可以获得状态&#34; runnable&#34;有时候我不期待它 - 但是,如果init早期退出并出现错误,我已经注意到__TASK_STOPPED挂在进程列表中很久就会自然终止,所以我猜这有效
  • 要获取绝对/物理地址,我使用Walking page tables of a process in Linux到达与虚拟地址对应的页面,然后挖掘内核源代码,我发现wtest到达地址(内部通过页面)帧号); LDD3 ch.15有助于理解pfn和物理地址之间的关系。
    • 由于此处我希望有实际地址,我不会使用PAGE_SHIFT,而是直接从page_to_phys()的程序集输出计算偏移量 - 我不是100%确定这是正确的,尽管
    • 注意,(另请参阅How to get a struct page from any address in the Linux kernel),模块输出显示虚拟地址objdump既不是0x08048000也不是is_vmalloc_addr;我想,这应该告诉我,一个人既没有使用virt_addr_valid也没有vmalloc_to_pfn()来到达其实际地址!?
  • 从内核空间为virt_to_page()设置kprobes有点棘手(需要复制功能)
    • 尝试在我获得的物理地址上设置ftrace(例如0x639ec474),总是以&#34; 结果无法插入探测器(-22)&#34; < / LI>
    • 只是为了查看格式是否已解析,我尝试使用下面kprobe函数的kallsyms地址(0xc10bcf60);这似乎有效 - 因为它引发致命的&#34; BUG:调度而原子&#34; (显然,我们并不打算在module_init中设置断点?)。 Bug是致命的,因为它使tracing_on()目录从kprobes调试目录消失了
    • 仅创建ftrace不会使其显示在kprobe日志中 - 它也需要启用;启用的必要代码就在那里 - 但由于之前的错误,我从未尝试过它
  • 最后,断点设置来自Watch a variable (memory address) change in Linux kernel, and print stack trace when it changes?
    • 我从未见过设置可执行硬件断点的示例;它一直没能通过我,直到通过内核源代码搜索,我发现对于ftraceHW_BREAKPOINT_X需要设置为attr.bp_len
    • 如果我尝试sizeof(long) printk变量 - 来自_init或来自处理程序 - 某些东西变得严重搞砸了,无论我接下来要打印什么变量,我都得到值0x5(或0x48)为它(?!)
    • 由于我试图为两个断点使用单个处理程序函数,唯一可靠的信息从_init存储到处理程序,能够区分两者,似乎是attr < / LI>
    • 这些ID是自动分配的,如果取消注册断点,它们似乎不会被重新声明(我不会取消注册它们以避免额外的ftrace打印输出)。

就随机性而言,我认为这是因为该过程不是以停止状态开始的;当它停止时,它最终处于不同的状态(或者很可能,我在某处失去了一些锁定)。无论如何,你也可以期待bp->id

syslog

...也就是说,即使使用适当的任务指针(通过start_code判断),也只获得0x0作为物理地址。有时你会得到相同的结果,但[ 1661.815114] callmodule: Trying to walk page table; addr task 0xEAF68CA0 ->mm ->start_code: 0x08048000 ->end_code: 0x080485F4 [ 1661.815319] callmodule: walk_ 0x8048000 callmodule: Valid pgd : Valid pud: Valid pmd: page frame struct is @ f5772000; *virtual (page_address) @ c0000000 (is_vmalloc_addr 0 virt_addr_valid 1 virt_to_phys 0x0) page_to_pfn 0 page_to_phys 0x0 [ 1661.815837] callmodule: walk_ 0x80483c0 callmodule: Valid pgd : Valid pud: Valid pmd: page frame struct is @ f5772000; *virtual (page_address) @ c0000000 (is_vmalloc_addr 0 virt_addr_valid 1 virt_to_phys 0x0) page_to_pfn 0 page_to_phys 0x0 [ 1661.816846] callmodule: walk_ 0x8048474 callmodule: Valid pgd : Valid pud: Valid pmd: page frame struct is @ f5772000; *virtual (page_address) @ c0000000 (is_vmalloc_addr 0 virt_addr_valid 1 virt_to_phys 0x0) page_to_pfn 0 page_to_phys 0x0 。有时,即使pid可以,也无法获得start_code: 0x00000000 ->end_code: 0x00000000

task_struct

嗯,希望有人会评论并澄清这个模块[ 833.380417] callmodule:c: pid 7663 [ 833.380424] callmodule: everything all right; pid 7663 (7663) [ 833.380430] callmodule: p is NULL - exiting [ 833.516160] callmodule: < exit 的一些行为 希望这有助于某人,
干杯!

:)

Makefile

EXTRA_CFLAGS=-g -O0 obj-m += callmodule.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

callmodule.c