标签 追踪 下的文章

trace-cmd 是一个易于使用,且特性众多、可用来追踪内核函数的命令。

 title=

之前的文章 里,我介绍了如何利用 ftrace 来追踪内核函数。通过写入和读出文件来使用 ftrace 会变得很枯燥,所以我对它做了一个封装来运行带有选项的命令,以启用和禁用追踪、设置过滤器、查看输出、清除输出等等。

trace-cmd 命令是一个可以帮助你做到这一点的工具。在这篇文章中,我使用 trace-cmd 来执行我在 ftrace 文章中所做的相同任务。由于会经常参考那篇文章,建议在阅读这篇文章之前先阅读它。

安装 trace-cmd

本文中所有的命令都运行在 root 用户下。

因为 ftrace 机制被内置于内核中,因此你可以使用下面的命令进行验证它是否启用:

# mount | grep tracefs
none on /sys/kernel/tracing type tracefs (rw,relatime,seclabel)

不过,你需要手动尝试安装 trace-cmd 命令:

# dnf install trace-cmd -y

列出可用的追踪器

当使用 ftrace 时,你必须查看文件的内容以了解有哪些追踪器可用。但使用 trace-cmd,你可以通过以下方式获得这些信息:

# trace-cmd list -t
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

启用函数追踪器

在我 之前的文章 中,我使用了两个追踪器,在这里我也会这么做。用 function 启用你的第一个追踪器:

$ trace-cmd start -p function
  plugin 'function'

查看追踪输出

一旦追踪器被启用,你可以通过使用 show 参数来查看输出。这只显示了前 20 行以保持例子的简短(见我之前的文章对输出的解释):

# trace-cmd show | head -20
## tracer: function
#
# entries-in-buffer/entries-written: 410142/3380032   #P:8
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
           gdbus-2606    [004] ..s. 10520.538759: __msecs_to_jiffies <-rebalance_domains
           gdbus-2606    [004] ..s. 10520.538760: load_balance <-rebalance_domains
           gdbus-2606    [004] ..s. 10520.538761: idle_cpu <-load_balance
           gdbus-2606    [004] ..s. 10520.538762: group_balance_cpu <-load_balance
           gdbus-2606    [004] ..s. 10520.538762: find_busiest_group <-load_balance
           gdbus-2606    [004] ..s. 10520.538763: update_group_capacity <-update_sd_lb_stats.constprop.0
           gdbus-2606    [004] ..s. 10520.538763: __msecs_to_jiffies <-update_group_capacity
           gdbus-2606    [004] ..s. 10520.538765: idle_cpu <-update_sd_lb_stats.constprop.0
           gdbus-2606    [004] ..s. 10520.538766: __msecs_to_jiffies <-rebalance_domains

停止追踪并清除缓冲区

追踪将会在后台继续运行,你可以继续用 show 查看输出。

要停止追踪,请运行带有 stop 参数的 trace-cmd 命令:

# trace-cmd stop

要清除缓冲区,用 clear 参数运行它:

# trace-cmd clear

启用函数调用图追踪器

运行第二个追踪器,通过 function_graph 参数来启用它。

# trace-cmd start -p function_graph
  Plugin 'function_graph'

再次使用 show 参数查看输出。正如预期的那样,输出与第一次追踪输出略有不同。这一次,它包括一个函数调用链:

# trace-cmd show | head -20
## tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 4)   0.079 us    |        } /* rcu_all_qs */
 4)   0.327 us    |      } /* __cond_resched */
 4)   0.081 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {
 4)   0.078 us    |        rcu_all_qs();
 4)   0.243 us    |      }
 4)   0.080 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {
 4)   0.078 us    |        rcu_all_qs();
 4)   0.241 us    |      }
 4)   0.080 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {
 4)   0.079 us    |        rcu_all_qs();
 4)   0.235 us    |      }
 4)   0.095 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {

使用 stopclear 命令来停止追踪和清除缓存区:

# trace-cmd stop
# trace-cmd clear

调整追踪以增加深度

如果你想在函数调用中看到更多的深度,你可以对追踪器进行调整:

# trace-cmd start -p function_graph --max-graph-depth 5
  plugin 'function_graph'

现在,当你将这个输出与你之前看到的进行比较时,你应该看到更多的嵌套函数调用:

# trace-cmd show | head -20
## tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 6)               |        __fget_light() {
 6)   0.804 us    |          __fget_files();
 6)   2.708 us    |        }
 6)   3.650 us    |      } /* __fdget */
 6)   0.547 us    |      eventfd_poll();
 6)   0.535 us    |      fput();
 6)               |      __fdget() {
 6)               |        __fget_light() {
 6)   0.946 us    |          __fget_files();
 6)   1.895 us    |        }
 6)   2.849 us    |      }
 6)               |      sock_poll() {
 6)   0.651 us    |        unix_poll();
 6)   1.905 us    |      }
 6)   0.475 us    |      fput();
 6)               |      __fdget() {

了解可被追踪的函数

如果你想只追踪某些函数而忽略其他的,你需要知道确切的函数名称。你可以用 list -f 参数来得到它们。例如搜索常见的内核函数 kmalloc,它被用来在内核中分配内存:

# trace-cmd list -f | grep kmalloc
bpf_map_kmalloc_node
mempool_kmalloc
__traceiter_kmalloc
__traceiter_kmalloc_node
kmalloc_slab
kmalloc_order
kmalloc_order_trace
kmalloc_large_node
__kmalloc
__kmalloc_track_caller
__kmalloc_node
__kmalloc_node_track_caller
[...]

下面是我的测试系统中可被追踪的函数总数:

# trace-cmd list -f | wc -l
63165

追踪内核模块相关的函数

你也可以追踪与特定内核模块相关的函数。假设你想追踪 kvm 内核模块相关的功能,你可以通过以下方式来实现。请确保该模块已经加载:

# lsmod | grep kvm_intel
kvm_intel 335872 0
kvm 987136 1 kvm_intel

再次运行 trace-cmd,使用 list 参数,并从输出结果中,grep 查找以 ] 结尾的行。这将过滤掉内核模块。然后 grep 内核模块 kvm_intel ,你应该看到所有与该内核模块有关的函数。

# trace-cmd list -f | grep ]$  | grep kvm_intel
vmx_can_emulate_instruction [kvm_intel]
vmx_update_emulated_instruction [kvm_intel]
vmx_setup_uret_msr [kvm_intel]
vmx_set_identity_map_addr [kvm_intel]
handle_machine_check [kvm_intel]
handle_triple_fault [kvm_intel]
vmx_patch_hypercall [kvm_intel]

[...]

vmx_dump_dtsel [kvm_intel]
vmx_dump_sel [kvm_intel]

追踪特定函数

现在你知道了如何找到感兴趣的函数,请用一个例子把这些内容用于时间。就像前面的文章一样,试着追踪与文件系统相关的函数。我的测试系统上的文件系统是 ext4

这个过程略有不同;你在运行命令时,不使用 start 参数,而是在 record 参数后面加上你想追踪的函数的“模式”。你还需要指定你想要的追踪器;在这种情况下,就是 function_graph。该命令会继续记录追踪,直到你用 Ctrl+C 停止它。所以几秒钟后,按 Ctrl+C 停止追踪:

# trace-cmd list -f | grep ^ext4_

# trace-cmd record -l ext4_* -p function_graph
  plugin 'function_graph'
Hit Ctrl^C to stop recording
^C
CPU0 data recorded at offset=0x856000
    8192 bytes in size
[...]

查看追踪记录

要查看你之前的追踪记录,运行带有 report 参数的命令。从输出结果来看,很明显过滤器起作用了,你只看到 ext4 相关的函数追踪:

# trace-cmd report | head -20
[...]
cpus=8
       trace-cmd-12697 [000] 11303.928103: funcgraph_entry:                   |  ext4_show_options() {
       trace-cmd-12697 [000] 11303.928104: funcgraph_entry:        0.187 us   |    ext4_get_dummy_policy();
       trace-cmd-12697 [000] 11303.928105: funcgraph_exit:         1.583 us   |  }
       trace-cmd-12697 [000] 11303.928122: funcgraph_entry:                   |  ext4_create() {
       trace-cmd-12697 [000] 11303.928122: funcgraph_entry:                   |    ext4_alloc_inode() {
       trace-cmd-12697 [000] 11303.928123: funcgraph_entry:        0.101 us   |      ext4_es_init_tree();
       trace-cmd-12697 [000] 11303.928123: funcgraph_entry:        0.083 us   |      ext4_init_pending_tree();
       trace-cmd-12697 [000] 11303.928123: funcgraph_entry:        0.141 us   |      ext4_fc_init_inode();
       trace-cmd-12697 [000] 11303.928123: funcgraph_exit:         0.931 us   |    }
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.081 us   |    ext4_get_dummy_policy();
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.133 us   |    ext4_get_group_desc();
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.115 us   |    ext4_free_inodes_count();
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.114 us   |    ext4_get_group_desc();

追踪一个特定的 PID

假设你想追踪与一个进程(PID)有关的函数。打开另一个终端,注意运行中的 shell 的PID:

# echo $$
10885

再次运行 record 命令,用 -P 选项传递PID。这一次,让终端运行(也就是说,先不要按 Ctrl+C ):

# trace-cmd record -P 10885 -p function_graph
  Plugin 'function_graph'
Hit Ctrl^C to stop recording

在 shell 上运行一些命令

移动到另一个终端,在那里你有一个以特定 PID 运行的 shell,并运行任何命令,例如,ls 命令用来列出文件:

# ls
Temp-9b61f280-fdc1-4512-9211-5c60f764d702
tracker-extract-3-files.1000
v8-compile-cache-1000
[...]

移动到你启用追踪的终端,按 Ctrl+C 停止追踪:

# trace-cmd record -P 10885 -p function_graph
  plugin 'function_graph'
Hit Ctrl^C to stop recording
^C
CPU1 data recorded at offset=0x856000
    618496 bytes in size
[...]

在追踪的输出中,你可以看到左边是 PID 和 Bash shell,右边是与之相关的函数调用。这对于缩小你的追踪范围是非常方便的:

# trace-cmd report  | head -20

cpus=8
          <idle>-0     [001] 11555.380581: funcgraph_entry:                   |  switch_mm_irqs_off() {
          <idle>-0     [001] 11555.380583: funcgraph_entry:        1.703 us   |    load_new_mm_cr3();
          <idle>-0     [001] 11555.380586: funcgraph_entry:        0.493 us   |    switch_ldt();
          <idle>-0     [001] 11555.380587: funcgraph_exit:         7.235 us   |  }
            bash-10885 [001] 11555.380589: funcgraph_entry:        1.046 us   |  finish_task_switch.isra.0();
            bash-10885 [001] 11555.380591: funcgraph_entry:                   |  __fdget() {
            bash-10885 [001] 11555.380592: funcgraph_entry:        2.036 us   |    __fget_light();
            bash-10885 [001] 11555.380594: funcgraph_exit:         3.256 us   |  }
            bash-10885 [001] 11555.380595: funcgraph_entry:                   |  tty_poll() {
            bash-10885 [001] 11555.380597: funcgraph_entry:                   |    tty_ldisc_ref_wait() {
            bash-10885 [001] 11555.380598: funcgraph_entry:                   |      ldsem_down_read() {
            bash-10885 [001] 11555.380598: funcgraph_entry:                   |        __cond_resched() {

试一试

这些简短的例子显示了使用 trace-cmd 命令而不是底层的 ftrace 机制,是如何实现既容易使用又拥有丰富的功能,许多内容本文并没有涉及。要想了解更多信息并更好地使用它,请查阅它的手册,并尝试使用其他有用的命令。


via: https://opensource.com/article/21/7/linux-kernel-trace-cmd

作者:Gaurav Kamathe 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

通过 ftrace 来了解 Linux 内核内部工作方式是一个好方法。

 title=

操作系统的内核是最难以理解的软件之一。自从你的系统启动后,它会一直在后台运行。尽管每个用户都不与内核直接交互,但他们在内核的帮助下完成自己的计算任务。与内核的交互发生在调用系统调用或者用户日常使用的各种库或应用间接调用了系统调用。

在之前的文章里我介绍了如何使用 strace 来追踪系统调用。然而,使用 strace 时你的视野是有限的。它允许你查看特定参数的系统调用。并在工作完成后,看到其返回值或状态,以表明是成功还是失败。但是你无法知道内核在这段时间内发生了什么。除了系统调用外,还有很多其他活动内核中发生,而你却视而不见。

ftrace 介绍

本文的旨在通过使用一个名为 ftrace 的机制来阐明追踪内核函数的一些情况。它使得任何 Linux 用户可以轻松地追踪内核,并且了解更多关于 Linux 内核内部如何工作。

ftrace 默认产生的输出往往是巨大的,因为内核总是忙碌的。为了节省空间,很多情况下我会通过截断来给出尽量小的输出。

我使用 Fedora 来演示下面的例子,但是它们应该在其他最新的 Linux 发行版上同样可以运行。

启用 ftrace

ftrace 现在已经是内核中的一部分了,你不再需要事先安装它了。也就是说,如果你在使用最近的 Linux 系统,那么 ftrace 是已经启用了的。为了验证 ftrace 是否可用,运行 mount 命令并查找 tracefs。如果你看到类似下面的输出,表示 ftrace 已经启用,你可以轻松地尝试本文中下面的例子。下面有些命令需要在 root 用户下使用(用 sudo 执行是不够的)。

# mount | grep tracefs
none on /sys/kernel/tracing type tracefs (rw,relatime,seclabel)

要想使用 ftrace,你首先需要进入上面 mount 命令中找到的特定目录中,在那个目录下运行文章中的其他命令。

# cd /sys/kernel/tracing

一般的工作流程

首先,你需要理解捕捉踪迹和获取输出的一般流程。如果你直接运行 ftrace,不会运行任何特定的 ftrace 命令。相反的,基本操作是通过标准 Linux 命令来写入或读取一些文件。

一般的步骤如下:

  1. 通过写入一些特定文件来启用/结束追踪
  2. 通过写入一些特定文件来设置/取消追踪时的过滤规则
  3. 从文件中读取基于第 1 和 2 步的追踪输出
  4. 从文件中清除早期输出或缓冲区
  5. 缩小到你的特定用例(你要追踪的内核函数),重复 1、2、3、4 步

可用的追踪器类型

有多种不同的追踪器可供使用。之前提到,在运行任何命令前,你需要进入一个特定的目录下,因为需要的文件在这些目录下。我在我的例子中使用了相对路径(而不是绝对路径)。

你可以查看 available_tracers 文件内容来查看所有可用的追踪器类型。你可以看下面列出了几个。不需要担心这些:

$ pwd
/sys/kernel/tracing

$ sudo cat available_tracers
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

在所有输出的追踪器中,我会聚焦于下面三个特殊的:启用追踪的 functionfunction_graph,以及停止追踪的 nop

确认当前的追踪器

通常情况默认的追踪器设定为 nop。即在特殊文件中 current_tracer 中的 “无操作”,这意味着追踪目前是关闭的:

$ pwd
/sys/kernel/tracing

$ sudo cat current_tracer
nop

查看追踪输出

在启用任何追踪功能之前,请你看一下保存追踪输出的文件。你可以用 cat 命令查看名为 trace 的文件的内容:

# cat trace

# tracer: nop
#
# entries-in-buffer/entries-written: 0/0   #P:8
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |

启用 function 追踪器

你可以通过向 current_tracer 文件写入 function 来启用第一个追踪器 function(文件原本内容为 nop,意味着追踪是关闭的)。把这个操作看成是启用追踪的一种方式:

$ pwd
/sys/kernel/tracing

$ sudo cat current_tracer
nop
$ echo function > current_tracer
$
$ cat current_tracer
function

查看 function 追踪器的更新追踪输出

现在你已启用追踪,是时候查看输出了。如果你查看 trace 文件内容,你将会看到许多被连续写入的内容。我通过管道只展示了文件内容的前 20 行。根据左边输出的标题,你可以看到在某个 CPU 上运行的任务和进程 ID。根据右边输出的内容,你可以看到具体的内核函数和其父函数。中间显示了时间戳信息:

# sudo cat trace | head -20

# tracer: function
#
# entries-in-buffer/entries-written: 409936/4276216   #P:8
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
          <idle>-0       [000] d...  2088.841739: tsc_verify_tsc_adjust <-arch_cpu_idle_enter
          <idle>-0       [000] d...  2088.841739: local_touch_nmi <-do_idle
          <idle>-0       [000] d...  2088.841740: rcu_nocb_flush_deferred_wakeup <-do_idle
          <idle>-0       [000] d...  2088.841740: tick_check_broadcast_expired <-do_idle
          <idle>-0       [000] d...  2088.841740: cpuidle_get_cpu_driver <-do_idle
          <idle>-0       [000] d...  2088.841740: cpuidle_not_available <-do_idle
          <idle>-0       [000] d...  2088.841741: cpuidle_select <-do_idle
          <idle>-0       [000] d...  2088.841741: menu_select <-do_idle
          <idle>-0       [000] d...  2088.841741: cpuidle_governor_latency_req <-menu_select

请记住当追踪打开后,这意味着追踪结果会被一直连续写入直至你关闭追踪。

关闭追踪

关闭追踪是简单的。你只需要在 current_tracer 文件中用 nop 替换 function 追踪器即可:

$ sudo cat current_tracer
function

$ sudo echo nop > current_tracer

$ sudo cat current_tracer
nop

启用 function\_graph 追踪器

现在尝试第二个名为 function_graph 的追踪器。你可以使用和上面相同的步骤:在 current_tracer 文件中写入 function_graph

$ sudo echo function_graph > current_tracer

$ sudo cat current_tracer
function_graph

function\_tracer 追踪器的追踪输出

注意到目前 trace 文件的输出格式已经发生变化。现在,你可以看到 CPU ID 和内核函数的执行时长。接下来,一个花括号表示一个函数的开始,以及它内部调用了哪些其他函数:

# cat trace | head -20

# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 6)               |              n_tty_write() {
 6)               |                down_read() {
 6)               |                  __cond_resched() {
 6)   0.341 us    |                    rcu_all_qs();
 6)   1.057 us    |                  }
 6)   1.807 us    |                }
 6)   0.402 us    |                process_echoes();
 6)               |                add_wait_queue() {
 6)   0.391 us    |                  _raw_spin_lock_irqsave();
 6)   0.359 us    |                  _raw_spin_unlock_irqrestore();
 6)   1.757 us    |                }
 6)   0.350 us    |                tty_hung_up_p();
 6)               |                mutex_lock() {
 6)               |                  __cond_resched() {
 6)   0.404 us    |                    rcu_all_qs();
 6)   1.067 us    |                  }

启用追踪的设置来增加追踪的深度

你可以使用下面的步骤来调整追踪器以看到更深层次的函数调用。完成之后,你可以查看 trace 文件的内容并发现输出变得更加详细了。为了文章的可读性,这个例子的输出被省略了:

# cat max_graph_depth
0

# echo 1 > max_graph_depth ## or:
# echo 2 > max_graph_depth

# sudo cat trace

查找要追踪的函数

上面的步骤足以让你开始追踪。但是它产生的输出内容是巨大的,当你想试图找到自己感兴趣的内容时,往往会很困难。通常你更希望能够只追踪特定的函数,而忽略其他函数。但如果你不知道它们确切的名称,你怎么知道要追踪哪些进程?有一个文件可以帮助你解决这个问题 —— available_filter_functions 文件提供了一个可供追踪的函数列表:

$ sudo wc -l available_filter_functions  
63165 available_filter_functions

查找一般的内核函数

现在试着搜索一个你所知道的简单内核函数。用户空间由 malloc 函数用来分配内存,而内核由 kmalloc 函数,它提供类似的功能。下面是所有与 kmalloc 相关的函数:

$ sudo grep kmalloc available_filter_functions
debug_kmalloc
mempool_kmalloc
kmalloc_slab
kmalloc_order
kmalloc_order_trace
kmalloc_fix_flags
kmalloc_large_node
__kmalloc
__kmalloc_track_caller
__kmalloc_node
__kmalloc_node_track_caller
[...]

查找内核模块或者驱动相关函数

available_filter_functions 文件的输出中,你可以看到一些以括号内文字结尾的行,例如下面的例子中的 [kvm_intel]。这些函数与当前加载的内核模块 kvm_intel 有关。你可以运行 lsmod 命令来验证:

$ sudo grep kvm available_filter_functions | tail
__pi_post_block [kvm_intel]
vmx_vcpu_pi_load [kvm_intel]
vmx_vcpu_pi_put [kvm_intel]
pi_pre_block [kvm_intel]
pi_post_block [kvm_intel]
pi_wakeup_handler [kvm_intel]
pi_has_pending_interrupt [kvm_intel]
pi_update_irte [kvm_intel]
vmx_dump_dtsel [kvm_intel]
vmx_dump_sel [kvm_intel]

$ lsmod  | grep -i kvm
kvm_intel             335872  0
kvm                   987136  1 kvm_intel
irqbypass              16384  1 kvm

仅追踪特定的函数

为了实现对特定函数或模式的追踪,你可以利用 set_ftrace_filter 文件来指定你要追踪上述输出中的哪些函数。这个文件也接受 * 模式,它可以扩展到包括具有给定模式的其他函数。作为一个例子,我在我的机器上使用 ext4 文件系统。我可以用下面的命令指定 ext4 的特定内核函数来追踪:

# mount | grep home
/dev/mapper/fedora-home on /home type ext4 (rw,relatime,seclabel)

# pwd
/sys/kernel/tracing

# cat set_ftrace_filter

#### all functions enabled ####
$
$ echo ext4_* > set_ftrace_filter
$
$ cat set_ftrace_filter
ext4_has_free_clusters
ext4_validate_block_bitmap
ext4_get_group_number
ext4_get_group_no_and_offset
ext4_get_group_desc
[...]

现在当你可以看到追踪输出时,你只能看到与内核函数有关的 ext4 函数,而你之前已经为其设置了一个过滤器。所有其他的输出都被忽略了:

# cat trace |head -20

## tracer: function
#
# entries-in-buffer/entries-written: 3871/3871   #P:8
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
           cupsd-1066    [004] ....  3308.989545: ext4_file_getattr <-vfs_fstat
           cupsd-1066    [004] ....  3308.989547: ext4_getattr <-ext4_file_getattr
           cupsd-1066    [004] ....  3308.989552: ext4_file_getattr <-vfs_fstat
           cupsd-1066    [004] ....  3308.989553: ext4_getattr <-ext4_file_getattr
           cupsd-1066    [004] ....  3308.990097: ext4_file_open <-do_dentry_open
           cupsd-1066    [004] ....  3308.990111: ext4_file_getattr <-vfs_fstat
           cupsd-1066    [004] ....  3308.990111: ext4_getattr <-ext4_file_getattr
           cupsd-1066    [004] ....  3308.990122: ext4_llseek <-ksys_lseek
           cupsd-1066    [004] ....  3308.990130: ext4_file_read_iter <-new_sync_read

排除要被追踪的函数

你并不总是知道你想追踪什么,但是,你肯定知道你不想追踪什么。因此,有一个 set_ftrace_notrace —— 请注意其中的 “no”。你可以在这个文件中写下你想要的模式,并启用追踪。这样除了所提到的模式外,任何其他东西都会被追踪到。这通常有助于删除那些使我们的输出变得混乱的普通功能:

$ sudo cat set_ftrace_notrace
#### no functions disabled ####

具有目标性的追踪

到目前为止,你一直在追踪内核中发生的一切。但是,它无法帮助你追踪与某个特定命令有关的事件。为了达到这个目的,你可以按需打开和关闭跟踪,并且在它们之间,运行我们选择的命令,这样你就不会在跟踪输出中得到额外的输出。你可以通过向 tracing_on 写入 1 来启用跟踪,写 0 来关闭跟踪。

# cat tracing_on
0

# echo 1 > tracing_on

# cat tracing_on
1

### Run some specific command that we wish to trace here ###

# echo 0 > tracing_on

# cat tracing_on
0

追踪特定的 PID

如果你想追踪与正在运行的特定进程有关的活动,你可以将该 PID 写入一个名为 set_ftrace_pid 的文件,然后启用追踪。这样一来,追踪就只限于这个 PID,这在某些情况下是非常有帮助的。

$ sudo echo $PID > set_ftrace_pid

总结

ftrace 是一个了解 Linux 内核内部工作的很好方式。通过一些练习,你可以学会对 ftrace 进行调整以缩小搜索范围。要想更详细地了解 ftrace 和它的高级用法,请看 ftrace 的核心作者 Steven Rostedt 写的这些优秀文章。


via: https://opensource.com/article/21/7/linux-kernel-ftrace

作者:Gaurav Kamathe 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这是调试器工作原理系列文章的第二部分,阅读本文前,请确保你已经读过第一部分

关于本文

我将会演示如何在调试器中实现断点。断点是调试的两大利器之一,另一个是可以在被调试进程的内存中检查变量值。我们在系列的第一部分已经了解过值检查,但是断点对我们来说依然神秘。不过本文过后,它们就不再如此了。

软件中断

为了在 x86 架构机器上实现断点,软件中断(也被称作“陷阱”)被会派上用场。在我们深入细节之前,我想先大致解释一下中断和陷阱的概念。

CPU 有一条单独的执行流,一条指令接一条的执行(在更高的层面看是这样的,但是在底层的细节上来说,现在的许多 CPU 都会并行执行多个指令,这其中的一些指令就不是按照原本的顺序执行的)。为了能够处理异步的事件,如 IO 和 硬件定时器,CPU 使用了中断。硬件中断通常是一个特定的电子信号,并附加了一个特别的”响应电路”。该电路通知中断激活,并让 CPU 停止当前执行,保存状态,然后跳转到一个预定义的地址,也就是中断处理程序的位置。当处理程序完成其工作后,CPU 又从之前停止的地方重新恢复运行。

软件中断在规则上与硬件相似,但实际操作中有些不同。CPU 支持一些特殊的指令,来允许软件模拟出一个中断。当这样的一个指令被执行时,CPU 像对待一个硬件中断那样 —— 停止正常的执行流,保存状态,然后跳转到一个处理程序。这种“中断”使得许多现代 OS 的惊叹设计得以高效地实现(如任务调度,虚拟内存,内存保护,调试)。

许多编程错误(如被 0 除)也被 CPU 当做中断对待,常常也叫做“异常”, 这时候硬件和软件中断之间的界限就模糊了,很难说这种异常到底是硬件中断还是软件中断。但我已经偏离今天主题太远了,所以现在让我们回到断点上来。

int 3 理论

前面说了很多,现在简单来说断点就是一个部署在 CPU 上的特殊中断,叫 int 3int 是一个 “中断指令”的 x86 术语,该指令是对一个预定义中断处理的调用。x86 支持 8 位的 int 指令操作数,这决定了中断的数量,所以理论上可以支持 256 个中断。前 32 个中断为 CPU 自己保留,而 int 3 就是本文关注的 —— 它被叫做 “调试器专用中断”。

避免更深的解释,我将引用“圣经”里一段话(这里说的“圣经”,当然指的是英特尔的体系结构软件开发者手册, 卷 2A)。

INT 3 指令生成一个以字节操作码(CC),用于调用该调试异常处理程序。(这个一字节格式是非常有用的,因为它可以用于使用断点来替换任意指令的第一个字节 ,包括哪些一字节指令,而不会覆写其它代码)

上述引用非常重要,但是目前去解释它还是为时过早。本文后面我们会回过头再看。

int 3 实践

没错,知道事物背后的理论非常不错,不过,这些理论到底意思是啥?我们怎样使用 int 3 部署断点?或者怎么翻译成通用的编程术语 —— 请给我看代码!

实际上,实现非常简单。一旦你的程序执行了 int 3 指令, OS 就会停止程序( OS 是怎么做到像这样停止进程的? OS 注册其 int 3 的控制程序到 CPU 即可,就这么简单)。在 Linux(这也是本文比较关心的地方) 上, OS 会发送给进程一个信号 —— SIGTRAP

就是这样,真的。现在回想一下本系列的第一部分, 追踪进程(调试程序) 会得到其子进程(或它所连接的被调试进程)所得到的所有信号的通知,接下来你就知道了。

就这样, 没有更多的电脑架构基础术语了。该是例子和代码的时候了。

手动设置断点

现在我要演示在程序里设置断点的代码。我要使用的程序如下:

section    .text
    ; The _start symbol must be declared for the linker (ld)
    global _start

_start:

    ; Prepare arguments for the sys_write system call:
    ;   - eax: system call number (sys_write)
    ;   - ebx: file descriptor (stdout)
    ;   - ecx: pointer to string
    ;   - edx: string length
    mov     edx, len1
    mov     ecx, msg1
    mov     ebx, 1
    mov     eax, 4

    ; Execute the sys_write system call
    int     0x80

    ; Now print the other message
    mov     edx, len2
    mov     ecx, msg2
    mov     ebx, 1
    mov     eax, 4
    int     0x80

    ; Execute sys_exit
    mov     eax, 1
    int     0x80

section    .data

msg1    db      'Hello,', 0xa
len1    equ     $ - msg1
msg2    db      'world!', 0xa
len2    equ     $ - msg2

我现在在使用汇编语言,是为了当我们面对 C 代码的时候,能清楚一些编译细节。上面代码做的事情非常简单,就是在一行打印出 “hello,”,然后在下一行打印出 “world!”。这与之前文章中的程序非常类似。

现在我想在第一次打印和第二次打印之间设置一个断点。我们看到在第一条 int 0x80 ,其后指令是 mov edx, len2。(等等,再次 int?是的,Linux 使用 int 0x80 来实现用户进程到系统内核的系统调用。用户将系统调用的号码及其参数放到寄存器,并执行 int 0x80。然后 CPU 会跳到相应的中断处理程序,其中, OS 注册了一个过程,该过程查看寄存器并决定要执行的系统调用。)首先,我们需要知道该指令所映射的地址。运行 objdump -d:

traced_printer2:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000033  08048080  08048080  00000080  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         0000000e  080490b4  080490b4  000000b4  2**2
                  CONTENTS, ALLOC, LOAD, DATA

Disassembly of section .text:

08048080 <.text>:
 8048080:     ba 07 00 00 00          mov    $0x7,%edx
 8048085:     b9 b4 90 04 08          mov    $0x80490b4,%ecx
 804808a:     bb 01 00 00 00          mov    $0x1,%ebx
 804808f:     b8 04 00 00 00          mov    $0x4,%eax
 8048094:     cd 80                   int    $0x80
 8048096:     ba 07 00 00 00          mov    $0x7,%edx
 804809b:     b9 bb 90 04 08          mov    $0x80490bb,%ecx
 80480a0:     bb 01 00 00 00          mov    $0x1,%ebx
 80480a5:     b8 04 00 00 00          mov    $0x4,%eax
 80480aa:     cd 80                   int    $0x80
 80480ac:     b8 01 00 00 00          mov    $0x1,%eax
 80480b1:     cd 80                   int    $0x80

所以,我们要设置断点的地址是 0x8048096。等等,这不是调试器工作的真实姿势,对吧?真正的调试器是在代码行和函数上设置断点,而不是赤裸裸的内存地址?完全正确,但是目前我们仍然还没到那一步,为了更像真正的调试器一样设置断点,我们仍不得不首先理解一些符号和调试信息。所以现在,我们就得面对内存地址。

在这点上,我真想又偏离一下主题。所以现在你有两个选择,如果你真的感兴趣想知道为什么那个地址应该是 0x8048096,它代表着什么,那就看下面的部分。否则你只是想了解断点,你可以跳过这部分。

题外话 —— 程序地址和入口

坦白说,0x8048096 本身没多大意义,仅仅是可执行程序的 text 部分开端偏移的一些字节。如果你看上面导出来的列表,你会看到 text 部分从地址 0x08048080 开始。这告诉 OS 在分配给进程的虚拟地址空间里,将该地址映射到 text 部分开始的地方。在 Linux 上面,这些地址可以是绝对地址(例如,当可执行程序加载到内存中时它不做重定位),因为通过虚拟地址系统,每个进程获得自己的一块内存,并且将整个 32 位地址空间看做自己的(称为 “线性” 地址)。

如果我们使用 readelf 命令检查 ELF 文件头部(ELF,可执行和可链接格式,是 Linux 上用于对象文件、共享库和可执行程序的文件格式),我们会看到:

$ readelf -h traced_printer2
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048080
  Start of program headers:          52 (bytes into file)
  Start of section headers:          220 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         4
  Section header string table index: 3

注意头部里的 Entry point address,它同样指向 0x8048080。所以我们在系统层面解释该 elf 文件的编码信息,它意思是:

  1. 映射 text 部分(包含所给的内容)到地址 0x8048080
  2. 从入口 —— 地址 0x8048080 处开始执行

但是,为什么是 0x8048080 呢?事实证明是一些历史原因。一些 Google 的结果把我引向源头,宣传每个进程的地址空间的前 128M 是保留在栈里的。128M 对应为 0x8000000,该地址是可执行程序其他部分可以开始的地方。而 0x8048080,比较特别,是 Linux ld 链接器使用的默认入口地址。该入口可以通过给 ld 传递 -Ttext 参数改变。

总结一下,这地址没啥特别的,我们可以随意修改它。只要 ELF 可执行文件被合理的组织,并且头部里的入口地址与真正的程序代码(text 部分)开始的地址匹配,一切都没问题。

用 int 3 在调试器中设置断点

为了在被追踪进程的某些目标地址设置一个断点,调试器会做如下工作:

  1. 记住存储在目标地址的数据
  2. 用 int 指令替换掉目标地址的第一个字节

然后,当调试器要求 OS 运行该进程的时候(通过上一篇文章中提过的 PTRACE_CONT),进程就会运行起来直到遇到 int 3,此处进程会停止运行,并且 OS 会发送一个信号给调试器。调试器会收到一个信号表明其子进程(或者说被追踪进程)停止了。调试器可以做以下工作:

  1. 在目标地址,用原来的正常执行指令替换掉 int 3 指令
  2. 将被追踪进程的指令指针回退一步。这是因为现在指令指针位于刚刚执行过的 int 3 之后。
  3. 允许用户以某些方式与进程交互,因为该进程仍然停止在特定的目标地址。这里你的调试器可以让你取得变量值,调用栈等等。
  4. 当用户想继续运行,调试器会小心地把断点放回目标地址去(因为它在第 1 步时被移走了),除非用户要求取消该断点。

让我们来看看,这些步骤是如何翻译成具体代码的。我们会用到第一篇里的调试器 “模板”(fork 一个子进程并追踪它)。无论如何,文末会有一个完整样例源代码的链接

/* Obtain and show child's instruction pointer */
ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
procmsg("Child started. EIP = 0x%08x\n", regs.eip);

/* Look at the word at the address we're interested in */
unsigned addr = 0x8048096;
unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("Original data at 0x%08x: 0x%08x\n", addr, data);

这里调试器从被追踪的进程中取回了指令指针,也检查了在 0x8048096 的字。当开始追踪运行文章开头的汇编代码,将会打印出:

[13028] Child started. EIP = 0x08048080
[13028] Original data at 0x08048096: 0x000007ba

目前为止都看起来不错。接下来:

/* Write the trap instruction 'int 3' into the address */
unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC;
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap);

/* See what's there again... */
unsigned readback_data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("After trap, data at 0x%08x: 0x%08x\n", addr, readback_data);

注意到 int 3 是如何被插入到目标地址的。此处打印:

[13028] After trap, data at 0x08048096: 0x000007cc

正如预料的那样 —— 0xba0xcc 替换掉了。现在调试器运行子进程并等待它在断点处停止:

/* Let the child run to the breakpoint and wait for it to
** reach it
*/
ptrace(PTRACE_CONT, child_pid, 0, 0);

wait(&wait_status);
if (WIFSTOPPED(wait_status)) {
    procmsg("Child got a signal: %s\n", strsignal(WSTOPSIG(wait_status)));
}
else {
    perror("wait");
    return;
}

/* See where the child is now */
ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
procmsg("Child stopped at EIP = 0x%08x\n", regs.eip);

这里打印出:

Hello,
[13028] Child got a signal: Trace/breakpoint trap
[13028] Child stopped at EIP = 0x08048097

注意到 “Hello,” 在断点前打印出来了 —— 完全如我们计划的那样。同时注意到子进程停止的地方 —— 刚好就是单字节中断指令后面。

最后,如早先诠释的那样,为了让子进程继续运行,我们得做一些工作。我们用原来的指令替换掉中断指令,并且让进程从这里继续之前的运行。

/* Remove the breakpoint by restoring the previous data
** at the target address, and unwind the EIP back by 1 to
** let the CPU execute the original instruction that was
** there.
*/
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data);
regs.eip -= 1;
ptrace(PTRACE_SETREGS, child_pid, 0, &regs);

/* The child can continue running now */
ptrace(PTRACE_CONT, child_pid, 0, 0);

这会使子进程继续打印出 “world!”,然后退出。

注意,我们在这里没有恢复断点。通过在单步调试模式下,运行原来的指令,然后将中断放回去,并且只在运行 PTRACE\_CONT 时做到恢复断点。文章稍后会展示 debuglib 如何做到这点。

更多关于 int 3

现在可以回过头去看看 int 3 和因特尔手册里那个神秘的说明,原文如下:

这个一字节格式是非常有用的,因为它可以用于使用断点来替换任意指令的第一个字节 ,包括哪些一字节指令,而不会覆写其它代码

int 指令在 x86 机器上占两个字节 —— 0xcd 紧跟着中断数(细心的读者可以在上面列出的转储中发现 int 0x80 翻译成了 cd 80)。int 3 被编码为 cd 03,但是为其还保留了一个单字节指令 —— 0xcc

为什么这样呢?因为这可以允许我们插入一个断点,而不需要重写多余的指令。这非常重要,考虑下面的代码:

    .. some code ..
    jz    foo
    dec   eax
foo:
    call  bar
    .. some code ..

假设你想在 dec eax 这里放置一个断点。这对应一个单字节指令(操作码为 0x48)。由于替换断点的指令长于一个字节,我们不得不强制覆盖掉下个指令(call)的一部分,这就会篡改 call 指令,并很可能导致一些完全不合理的事情发生。这样一来跳转到 foo 分支的 jz foo 指令会导致什么?就会不在 dec eax 这里停止,CPU 径直去执行后面一些无效的指令了。

而有了单字节的 int 3 指令,这个问题就解决了。 1 字节是在 x86 上面所能找到的最短指令,这样我们可以保证仅改变我们想中断的指令。

封装一些晦涩的细节

很多上述章节样例代码的底层细节,都可以很容易封装在方便使用的 API 里。我已经做了很多封装的工作,将它们都放在一个叫做 debuglib 的通用库里 —— 文末可以去下载。这里我仅仅是想展示它的用法示例,但是绕了一圈。下面我们将追踪一个用 C 写的程序。

追踪一个 C 程序地址和入口

目前为止,为了简单,我把注意力放在了目标汇编代码。现在是时候往上一个层次,去看看我们如何追踪一个 C 程序。

事实证明并不是非常难 —— 找到放置断点位置有一点难罢了。考虑下面样例程序:

#include <stdio.h>

void do_stuff()
{
    printf("Hello, ");
}

int main()
{
    for (int i = 0; i < 4; ++i)
        do_stuff();
    printf("world!\n");
    return 0;
}

假设我想在 do_stuff 入口处放置一个断点。我会先使用 objdump 反汇编一下可执行文件,但是打印出的东西太多。尤其看到很多无用,也不感兴趣的 C 程序运行时的初始化代码。所以我们仅看一下 do_stuff 部分:

080483e4 <do_stuff>:
 80483e4:     55                      push   %ebp
 80483e5:     89 e5                   mov    %esp,%ebp
 80483e7:     83 ec 18                sub    $0x18,%esp
 80483ea:     c7 04 24 f0 84 04 08    movl   $0x80484f0,(%esp)
 80483f1:     e8 22 ff ff ff          call   8048318 <puts@plt>
 80483f6:     c9                      leave
 80483f7:     c3                      ret

那么,我们将会把断点放在 0x080483e4,这是 do_stuff 第一条指令执行的地方。而且,该函数是在循环里面调用的,我们想要在断点处一直停止执行直到循环结束。我们将会使用 debuglib 来简化该流程,下面是完整的调试函数:

void run_debugger(pid_t child_pid)
{
    procmsg("debugger started\n");

    /* Wait for child to stop on its first instruction */
    wait(0);
    procmsg("child now at EIP = 0x%08x\n", get_child_eip(child_pid));

    /* Create breakpoint and run to it*/
    debug_breakpoint* bp = create_breakpoint(child_pid, (void*)0x080483e4);
    procmsg("breakpoint created\n");
    ptrace(PTRACE_CONT, child_pid, 0, 0);
    wait(0);

    /* Loop as long as the child didn't exit */
    while (1) {
        /* The child is stopped at a breakpoint here. Resume its
        ** execution until it either exits or hits the
        ** breakpoint again.
        */
        procmsg("child stopped at breakpoint. EIP = 0x%08X\n", get_child_eip(child_pid));
        procmsg("resuming\n");
        int rc = resume_from_breakpoint(child_pid, bp);

        if (rc == 0) {
            procmsg("child exited\n");
            break;
        }
        else if (rc == 1) {
            continue;
        }
        else {
            procmsg("unexpected: %d\n", rc);
            break;
        }
    }

    cleanup_breakpoint(bp);
}

为了避免修改 EIP 标志位和目的进程的内存空间的麻烦,我们仅需要调用 create_breakpointresume_from_breakpointcleanup_breakpoint。让我们来看看追踪上面的 C 代码样例会输出什么:

$ bp_use_lib traced_c_loop
[13363] debugger started
[13364] target started. will run 'traced_c_loop'
[13363] child now at EIP = 0x00a37850
[13363] breakpoint created
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
world!
[13363] child exited

如预期一样!

样例代码

这里是本文用到的完整源代码文件。在归档中你可以找到:

  • debuglib.h 和 debuglib.c - 封装了调试器的一些内部工作的示例库
  • bp\_manual.c - 这篇文章开始部分介绍的“手动”设置断点的方法。一些样板代码使用了 debuglib 库。
  • bpuselib.c - 大部分代码使用了 debuglib 库,用于在第二个代码范例中演示在 C 程序的循环中追踪。

引文

在准备本文的时候,我搜集了如下的资源和文章:


via: http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints

作者:Eli Bendersky 译者:wi-cuckoo 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出