本章介绍函数边界跟踪 (Function Boundary Tracing, FBT) 提供器,该提供器提供了与进入和返回 Solaris 内核中的大多数函数相关的探测器。函数是程序文本的基本组成单元。在设计完善的系统中,每个函数会对指定的对象或一系列类似对象执行独立和定义完善的操作。所以,即使在最小型的 Solaris 系统中,FBT 也将提供 20,000 个探测器。
与其他 DTrace 提供器类似,在没有显式启用 FBT 提供器时,它不会产生探测效果。启用时,FBT 仅在被探测的函数中产生探测效果。虽然 FBT 实现高度特定于指令集体系结构,但在 SPARC 和 x86 平台上都已实现 FBT。对于每一个指令集,都有少量函数不调用其他函数,且由无法通过 FBT 检测的编译器(所谓的叶函数)高度优化。DTrace 中不存在这些函数的探测器。
要有效使用 FBT 探测器,需要对操作系统的实现有所了解。所以,建议您仅在开发内核软件或者其他提供器不满足要求时,才使用 FBT。其他 DTrace 提供器(包括 syscall、sched、proc 和 io)都可用于回答大多数系统分析问题,不需要了解操作系统实现的知识。
在内核中大多数函数的边界,FBT 都提供了探测器。进入函数和从函数返回都会跨越函数边界。因此,FBT 为内核中的每个函数提供了两个探测器:一个用于进入函数时,一个用于从函数返回时。这两个探测器的名称分别为 entry 和 return。函数名称和模块名称都指定为探测器的一部分。所有 FBT 探测器都指定了函数名称和模块名称。
entry 探测器的参数与相应操作系统内核函数的参数相同。可以使用 args[] 数组以键入的方式访问这些参数。可以使用 arg0 ..argn 变量,以访问 int64_t 参数的方式访问这些参数。
虽然给定的函数仅有一个进入点,但它可能有许多不同的位置可返回到其调用方。通常,您可能只关心函数所返回的值或函数是否返回,而不关心所采用的具体返回路径。因此,FBT 将函数的多个返回位置收集到单个 return 探测器中。如果您对确切的返回路径感兴趣,可以检查 return 探测器的 args[0] 值,该值表示函数文本中返回指令的偏移量(以字节为单位)。
如果函数具有返回值,则该返回值存储在 args[1] 中。如果函数不具有返回值,则不会定义 args[1]。
可以使用 FBT 轻松地了解内核的实现。以下示例脚本记录来自任何 xclock 进程的第一个 ioctl(2),以及一直到内核的后续代码路径:
/* * To make the output more readable, we want to indent every function entry * (and unindent every function return). This is done by setting the * "flowindent" option. */ #pragma D option flowindent syscall::ioctl:entry /execname == "xclock" && guard++ == 0/ { self->traceme = 1; printf("fd: %d", arg0); } fbt::: /self->traceme/ {} syscall::ioctl:return /self->traceme/ { self->traceme = 0; exit(0); }
运行此脚本将会生成与以下示例类似的输出:
# dtrace -s ./xioctl.d dtrace: script './xioctl.d' matched 26254 probes CPU FUNCTION 0 => ioctl fd: 3 0 -> ioctl 0 -> getf 0 -> set_active_fd 0 <- set_active_fd 0 <- getf 0 -> fop_ioctl 0 -> sock_ioctl 0 -> strioctl 0 -> job_control_type 0 <- job_control_type 0 -> strcopyout 0 -> copyout 0 <- copyout 0 <- strcopyout 0 <- strioctl 0 <- sock_ioctl 0 <- fop_ioctl 0 -> releasef 0 -> clear_active_fd 0 <- clear_active_fd 0 -> cv_broadcast 0 <- cv_broadcast 0 <- releasef 0 <- ioctl 0 <= ioctl |
此输出说明,在看上去与套接字关联的文件说明符中,xclock 进程调用了 ioctl()。
尝试了解内核驱动程序时,也可以使用 FBT。例如,ssd(7D) 驱动程序有很多代码路径,通过这些路径可以返回 EIO。使用 FBT 可以轻松地确定导致错误的准确代码路径,如下例所示:
fbt:ssd::return /arg1 == EIO/ { printf("%s+%x returned EIO.", probefunc, arg0); }
为了获取所返回的任何一个 EIO 的更多信息,您可能希望推理跟踪所有 fbt 探测器,然后根据特定函数的返回值执行 commit()(或 discard())函数。有关推理跟踪的详细信息,请参见第 13 章。
或者,您可以使用 FBT 来了解指定模块中调用的函数。以下示例列出了 UFS 中调用的所有函数。
# dtrace -n fbt:ufs::entry'{@a[probefunc] = count()}' dtrace: description 'fbt:ufs::entry' matched 353 probes ^C ufs_ioctl 1 ufs_statvfs 1 ufs_readlink 1 ufs_trans_touch 1 wrip 1 ufs_dirlook 1 bmap_write 1 ufs_fsync 1 ufs_iget 1 ufs_trans_push_inode 1 ufs_putpages 1 ufs_putpage 1 ufs_syncip 1 ufs_write 1 ufs_trans_write_resv 1 ufs_log_amt 1 ufs_getpage_miss 1 ufs_trans_syncip 1 getinoquota 1 ufs_inode_cache_constructor 1 ufs_alloc_inode 1 ufs_iget_alloced 1 ufs_iget_internal 2 ufs_reset_vnode 2 ufs_notclean 2 ufs_iupdat 2 blkatoff 3 ufs_close 5 ufs_open 5 ufs_access 6 ufs_map 8 ufs_seek 11 ufs_addmap 15 rdip 15 ufs_read 15 ufs_rwunlock 16 ufs_rwlock 16 ufs_delmap 18 ufs_getattr 19 ufs_getpage_ra 24 bmap_read 25 findextent 25 ufs_lockfs_begin 27 ufs_lookup 46 ufs_iaccess 51 ufs_imark 92 ufs_lockfs_begin_getpage 102 bmap_has_holes 102 ufs_getpage 102 ufs_itimes_nolock 107 ufs_lockfs_end 125 dirmangled 498 dirbadname 498 |
如果您知道内核函数的用途或参数,可以使用 FBT 来了解调用函数的方式或原因。例如,putnext(9F) 接受指向 queue(9S) 结构的指针作为其第一个成员。queue 结构的 q_qinfo 成员是指向 qinit(9S) 结构的指针。qinit 结构的 qi_minfo 成员具有一个指向 module_info(9S) 结构(其 mi_idname 成员中包含模块名称)的指针。以下示例在 putnext 中使用 FBT 探测器将这些信息收集到一起,以便根据模块名称跟踪 putnext(9F) 调用:
fbt::putnext:entry { @calls[stringof(args[0]->q_qinfo->qi_minfo->mi_idname)] = count(); }
运行上面的脚本将会生成与以下示例类似的输出:
# dtrace -s ./putnext.d ^C iprb 1 rpcmod 1 pfmod 1 timod 2 vpnmod 2 pts 40 conskbd 42 kb8042 42 tl 58 arp 108 tcp 126 ptm 249 ip 313 ptem 340 vuid2ps2 361 ttcompat 412 ldterm 413 udp 569 strwhead 624 mouse8042 726 |
也可以使用 FBT 确定特定函数花费的时间。以下示例说明了如何确定 DDI 延迟例程 drv_usecwait(9F) 和 delay(9F) 的调用方。
fbt::delay:entry, fbt::drv_usecwait:entry { self->in = timestamp } fbt::delay:return, fbt::drv_usecwait:return /self->in/ { @snoozers[stack()] = quantize(timestamp - self->in); self->in = 0; }
在引导期间运行此示例脚本特别有意义。第 36 章介绍了在系统引导期间执行匿名跟踪的过程。重新引导时,您可能会看到与以下示例类似的输出:
# dtrace -ae ata`ata_wait+0x34 ata`ata_id_common+0xf5 ata`ata_disk_id+0x20 ata`ata_drive_type+0x9a ata`ata_init_drive+0xa2 ata`ata_attach+0x50 genunix`devi_attach+0x75 genunix`attach_node+0xb2 genunix`i_ndi_config_node+0x97 genunix`i_ddi_attachchild+0x4b genunix`devi_attach_node+0x3d genunix`devi_config_one+0x1d0 genunix`ndi_devi_config_one+0xb0 devfs`dv_find+0x125 devfs`devfs_lookup+0x40 genunix`fop_lookup+0x21 genunix`lookuppnvp+0x236 genunix`lookuppnat+0xe7 genunix`lookupnameat+0x87 genunix`cstatat_getvp+0x134 value ------------- Distribution ------------- count 2048 | 0 4096 |@@@@@@@@@@@@@@@@@@@@@ 4105 8192 |@@@@ 783 16384 |@@@@@@@@@@@@@@ 2793 32768 | 16 65536 | 0 kb8042`kb8042_wait_poweron+0x29 kb8042`kb8042_init+0x22 kb8042`kb8042_attach+0xd6 genunix`devi_attach+0x75 genunix`attach_node+0xb2 genunix`i_ndi_config_node+0x97 genunix`i_ddi_attachchild+0x4b genunix`devi_attach_node+0x3d genunix`devi_config_one+0x1d0 genunix`ndi_devi_config_one+0xb0 genunix`resolve_pathname+0xa5 genunix`ddi_pathname_to_dev_t+0x16 consconfig_dacf`consconfig_load_drivers+0x14 consconfig_dacf`dynamic_console_config+0x6c consconfig`consconfig+0x8 unix`stubs_common_code+0x3b value ------------- Distribution ------------- count 262144 | 0 524288 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 221 1048576 |@@@@ 29 2097152 | 0 usba`hubd_enable_all_port_power+0xed usba`hubd_check_ports+0x8e usba`usba_hubdi_attach+0x275 usba`usba_hubdi_bind_root_hub+0x168 uhci`uhci_attach+0x191 genunix`devi_attach+0x75 genunix`attach_node+0xb2 genunix`i_ndi_config_node+0x97 genunix`i_ddi_attachchild+0x4b genunix`i_ddi_attach_node_hierarchy+0x49 genunix`attach_driver_nodes+0x49 genunix`ddi_hold_installed_driver+0xe3 genunix`attach_drivers+0x28 value ------------- Distribution ------------- count 33554432 | 0 67108864 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3 134217728 | 0 |
当一个函数以调用另一个函数而结束时,编译器可能会进行尾部调用优化,优化后,被调用的函数将重用调用方的栈帧。此过程通常用于 SPARC 体系结构中,在此情况下,编译器在被调用的函数中重用调用方的注册窗口,以便使注册窗口的压力降到最低。
进行此优化会使调用函数的 return 探测器在被调用函数的 entry 探测器之前触发。这种触发顺序会导致比较严重的混乱情况。例如,如果要记录从某个特定函数调用的所有函数,以及此函数调用的所有函数,则可以使用以下脚本:
fbt::foo:entry { self->traceme = 1; } fbt:::entry /self->traceme/ { printf("called %s", probefunc); } fbt::foo:return /self->traceme/ { self->traceme = 0; }
但是,如果 foo() 以优化的尾部调用结束,那么,在尾部调用的函数以及它调用的任何函数都不会被捕获。不能即时动态取消内核优化,DTrace 不希望总是考虑如何虚构代码结构。所以,应清楚什么时候可以使用尾部调用优化。
可以在类似以下示例的源代码中使用尾部调用优化:
return (bar());
或者在类似以下示例的源代码中使用尾部调用优化:
(void) bar(); return;
相反地,结尾方式与以下示例类似的函数源代码不能优化对 bar() 的调用,因为对 bar() 的调用不是尾部调用:
bar(); return (rval);
可以使用以下方法确定是否已对某个调用进行了尾部调用优化:
在运行 DTrace 时,跟踪所考虑的 return 探测器的 arg0。arg0 包含函数中返回指令的偏移量。
在 DTrace 停止后,使用 mdb(1) 查看该函数。如果跟踪的偏移量包含对另一个函数的调用,而不是从函数返回的指令,则说明已对调用进行了尾部调用优化。
由于指令集体系结构,尾部调用优化在 SPARC 系统上要比在 x86 系统上更常见。以下示例使用 mdb 发现内核的 dup() 函数中的尾部调用优化:
# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}' |
运行此命令时,将运行执行 dup(2) 的程序(如 bash 进程)。上面的命令会提供与以下示例类似的输出:
dup+0x10 ^C |
现在使用 mdb 检查函数:
# echo "dup::dis" | mdb -k dup: sra %o0, 0, %o0 dup+4: mov %o7, %g1 dup+8: clr %o2 dup+0xc: clr %o1 dup+0x10: call -0x1278 <fcntl> dup+0x14: mov %g1, %o7 |
该输出说明,dup+0x10 是对 fcntl() 函数的调用而不是 ret 指令。所以,fcntl() 就是尾部调用优化的一个示例。
您可能会发现,有时似乎只进入某些函数但却不返回,或者出现相反的情况。这种函数很稀少,通常是手动编码的汇编例程,这些例程通常分支到其他手动编码的汇编函数中间。这些函数不应妨碍分析:分支到的目标函数仍然必须返回到其来源函数的调用方。即,如果启用所有 FBT 探测器,则应看到进入某个函数,同时从相同栈深度的另一个函数返回。
一些函数无法通过 FBT 进行检测。不可检测函数的准确特性特定于指令集体系结构。
在 x86 系统上不创建栈帧的函数无法通过 FBT 进行检测。因为 x86 的寄存器集非常小,大多数函数必须将数据放入栈中,从而创建栈帧。但是,一些 x86 函数不创建栈帧,因此无法对这些函数进行检测。x86 平台上无法进行检测的函数的实际数量不固定,但通常少于百分之五。
无法通过 FBT 对 SPARC 系统上以汇编语言进行手动编码的叶例程进行检测。大多数内核用 C 语言编写,所有用 C 语言编写的函数都可以通过 FBT 进行检测。
FBT 通过动态修改内核文本进行工作。由于内核断点也通过修改内核文本进行工作,所以如果在装入 DTrace 之前将内核断点放在入口或返回位置,FBT 将拒绝提供用于函数的探测器,即使随后删除了内核断点也是如此。如果在装入 DTrace 之后放置内核断点,则内核断点和 DTrace 探测器将对应于文本中相同的位置。在此情况下,断点将首先触发,然后在调试器恢复内核时,探测器将触发。建议不要同时使用内核断点和 DTrace。如果必须使用断点,请改为使用 DTrace breakpoint() 操作。
Solaris 内核可以动态装入和卸载内核模块。当装入 FBT 并动态装入模块时,FBT 将自动提供与新模块关联的新探测器。如果装入的模块未启用 FBT 探测器,则可以卸载模块;卸载模块时,相应的探测器将被破坏。如果装入的模块已启用 FBT 探测器,则该模块将被视为正忙,无法卸载该模块。
FBT 提供器使用 DTrace 的稳定性机制描述其稳定性,如下表所示。有关稳定性机制的更多信息,请参见第 39 章。
元素 |
名称稳定性 |
数据稳定性 |
相关性类 |
---|---|---|---|
提供器 |
发展中 |
发展中 |
ISA |
模块 |
专用 |
专用 |
未知 |
功能 |
专用 |
专用 |
未知 |
名称 |
发展中 |
发展中 |
ISA |
参数 |
专用 |
专用 |
ISA |
FBT 显示内核实现时,所有关于实现的内容都不“稳定”-模块和函数名称以及数据稳定性都明确地显示为“专用”。提供器和名称的数据稳定性为“发展中”,但是所有其他数据稳定性都为“专用”:它们是当前实现的产物。FBT 的相关类为 ISA:虽然在当前的所有指令集体系结构中都可以使用 FBT,但不能保证在将来的任意指令集体系结构中都可以使用 FBT。