概念
何谓“动态跟踪技术”?
对于调试用户态程序经常使用的是gdb或者lldb工具,但其都会阻断程序运行不能模拟真实的使用场景,而动态跟踪技术就可以无缝调试用户态或者内核态进程,并且可以观察或者修改其运行行为。
动态跟踪技术是一种后现代的高级调试技术,全称为Dynamic Trace,属于系统内核实现的,可以对内核态和用户态程序进行动态跟踪且性能损耗很小不会对系统运行构成任何危险。它可以帮助软件工程师以非常低的成本,在非常短的时间内,回答一些很难的关于软件系统方面的问题,从而更快速地排查和解决问题。
Dtrace和System Tap
dtrace
说到动态追踪就不能不提到 DTrace。DTrace 算是现代动态追踪技术的鼻祖了,它于 21 世纪初诞生于 Solaris 操作系统,是由原来的 Sun Microsystems 公司的工程师编写的。
它提供了一种很像 C 语言的脚本语言,叫做 D语言。基于 DTrace 的调试工具都是使用这种语言编写的。D 语言支持特殊的语法用以指定“探针”,这个“探针”通常有一个位置描述的信息。你可以把它定位在某个内核函数的入口或出口,抑或是某个用户态进程的 函数入口或出口,甚至是任意一条程序语句或机器指令上面。
systemtap
SystemTap是一个诊断Linux系统性能或功能问题的开源软件。这是由 Red Hat 公司的工程师创建的较为独立的动态追踪框架。SystemTap 提供了自己的一种小语言,和 D 语言并不相同。它使得对运行时的Linux系统进行诊断调式变得更容易、更简单。有了它,开发者或调试人员不再需要重编译、安装新内核、重启动等烦人的步骤。
SystemTap 的优点是它有非常成熟的用户态调试符号的自动加载,同时也有循环这样的语言结构可以去编写比较复杂的探针处理程序,可以支持很多很复杂的分析处理。
当然,SystemTap 也是有缺点的。首先,它并不是 Linux 内核的一部分,就是说它并没有与内核紧密集成,所以它需要一直不停地追赶主线内核的变化。另一个缺点是,它通常是把它的“小语言”脚本(有点像 D 语言哦)动态编译成一个 Linux 内核模块的 C 源码,因此经常需要在线部署 C 编译器工具链和 Linux 内核的头文件。出于这些原因,SystemTap 脚本的启动相比 DTrace 要慢得多,和 JVM 的启动时间倒有几分类似。虽然存在这些缺点[3],但总的来说,SystemTap 还是一个非常成熟的动态追踪框架,原理图如下:
image.png
OS X与iOS
正如你现在可能已经猜到的,DTrace 只能在 OS X 上运行。苹果也在 iOS 上使用 DTrace,用以支持像 Instruments 这样的工具,但对于第三方开发者,DTrace 只能运行于 OS X 或 iOS 模拟器。
Dtrace
DTrace工具组件包括提供器和探测器:
1、提供器:由dtrace内核驱动命令及附加在上面的dtrace脚本组成(后缀名.d)。Mac OS X默认就安装了dtrace工具;脚本使用D语言编写,也叫d脚本,Mac OS X系统的 /usr/share/examples/DTTk/ 目录下有很多例子,xcode内存分析工具instruments的基础就是dtrace,具体路径/usr/sbin/dtrace;
sudo /usr/sbin/dtrace -s xxx.d //运行原始的d脚本
man -k dtrace //查看系统的原始d脚本;
2、探测器(即探针):由提供器启动,可标识所检测的模块和函数,其名称标准格式为提供器:模块:函数:名称,每个探针还具有一个唯一的整数标识符。在苹果开源的xnu内核中可以看到苹果版的DTrace源码,打包为内核模块来收集跟踪数据,它提供接口通过 dtrace 内核驱动命令访问内核数据,在内核源码中很多带有provider关键字都属于标识某个模块数据的探针。其定义如下:
// 探针定义
typedef struct sdt_provider {
const char *sdtp_name; /* name of provider */
const char *sdtp_prefix; /* prefix for probe names */
dtrace_pattr_t *sdtp_attr; /* stability attributes */
dtrace_provider_id_t sdtp_id; /* provider ID */
} sdt_provider_t;
// xnu中在使用的一些探针
sdt_provider_t sdt_providers[] = {
{ "vtrace", "__vtrace____", &vtrace_attr, 0 },
{ "sysinfo", "__cpu_sysinfo____", &info_attr, 0 },
{ "vminfo", "__vminfo____", &info_attr, 0 },
{ "fpuinfo", "__fpuinfo____", &fpu_attr, 0 },
{ "sched", "__sched____", &stab_attr, 0 },
{ "proc", "__proc____", &stab_attr, 0 },
{ "io", "__io____", &stab_attr, 0 },
{ "ip", "__ip____", &stab_attr, 0 },
{ "tcp", "__tcp____", &stab_attr, 0 },
{ "mptcp", "__mptcp____", &stab_attr, 0 },
{ "mib", "__mib____", &stab_attr, 0 },
{ "fsinfo", "__fsinfo____", &fsinfo_attr, 0 },
{ "nfsv3", "__nfsv3____", &stab_attr, 0 },
{ "nfsv4", "__nfsv4____", &stab_attr, 0 },
{ "sysevent", "__sysevent____", &stab_attr, 0 },
{ "sdt", "__sdt____", &sdt_attr, 0 },
{ "boost", "__boost____", &stab_attr, 0},
{ NULL, NULL, NULL, 0 }
};
D语言
这里的D语言语法大部分跟C语言非常相似(这就带来了很好的可移植性),但总体架构是不同的。每个脚本由若干个探针语句组成。它们都符合如下的形式:
probe descriptions
/ predicate /
{
action statements
}
断言 (predicate) 和动作语句 (action statement) 部分都是可选的。
probe descriptions 即探针描述定义了语句匹配什么类型的探针,结构就是之前提到的提供器:模块:函数:名称—provider:module:function:name,所有的部分都可以省略。其中 BEGIN 语句在所有探针开始之前运行,END 语句在脚本退出时候执行。
语法很简单,设计很复杂,更详细的介绍参见: https://docs.oracle.com/cd/E23824_01/html/E22973/glghi.html#scrolltoc
常用的如下:
内建变量:
pid —— 当前进程的进程id
tid —— 当前线程的线程id
uid —— 当前进程的用户id
timestamp —— 一个纳秒级的计数器的当前时间戳
probemod —— 当前探针描述的模块名称部分
probefunc —— 当前探针描述的函数名称部分
常用函数:
void trace(expression) —— 最基本的操作,将将 D 表达式用作其参数并跟踪结果到定向缓冲区
void printf(string format, ...) —— 与trace操作一样,printf跟踪 D 表达式。不过,printf允许格式化输出
当运行 dtrace 工具时,我们传入的脚本被编译成字节码。接着字节码被传入安插了探针的代码中(通常是kernel)。在 kernel 中有一个解释器来运行这些字节码。当将静态探针加入可执行程序 (一个 app 或 framework),它们被作为S_DTRACE_DOF(Dtrace Object Format)部分被加入,并且在程序运行时被加载进kernel。这样DTrace就知道当前的静态探针。
DTrace示例
使用dtrace -l可以查看Mac OS X系统上的所有探针,dtrace -l -m 可以查看指定模块的探针。
dtrace: system integrity protection is on, some features will not be available //可以从安全模式关闭csrutil disable
示例1:追踪ECAgent进程的select系统调用,并打印出输入参数fd及timeout,d脚本如下:
#!/usr/sbin/dtrace -s
#pragma D option flowindent
syscall::select:entry
/ execname == "ECAgent" /
{
printf("pid:%d, tid:%d, uid:%u, timestamp:%lu, probefunc:%s \n", pid, tid, uid, timestamp, probefunc);
printf("fd:%d, timeout:%d \n", arg0, arg4);
}
syscall::select:return
{
printf("select return");
}
执行结果如下:
image.png
其实也可以通过dtruss 工具查看:
sudo dtruss -n ECAgent -t select -a
示例2:假设需求是要跟踪系统malloc
方法的所有分配内存大小,可以设计探针的定义文件DTraceDemo.probe
provider DTraceDemo {
probe malloc_log(void *ptr, size_t size);
};
然后,执行下面的命令生成探针的头文件,后面带入测试工程中编译
sudo dtrace -h -o DTraceDemo.h -s DTraceDemo.probe
测试代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger count = 0;
BOOL stop = NO;
while ( !stop ) {
// 分配10次,每次10M内存
size_t len = 10*1024*1024;
void *ptr = (void *)malloc(len);
memset(ptr, '\0', len);
// 插入探针
DTRACEDEMO_MALLOC_LOG(ptr, len);
if (count > 10) { stop = YES; }
count += 1;
sleep(2);
}
}
return 0;
}
最后,创建自己的脚本vim DTraceDemo.d
,文件名必须以 .d 后缀结尾,这是 D 脚本的约定结尾。更多d脚本例子参见。
#!/usr/sbin/dtrace -s
#pragma D option quiet
BEGIN
{
trace("Begin trace malloc!\n");
}
DTraceDemo*:::malloc_log
{
printf("malloc ptr:0x%p size:%lld thread:%lu\n", arg0, arg1, tid);
}
END
{
printf("End trace!");
}
使用sudo dtrace -s DTraceDemo.d
命令开始 dtrace 测试,接着启动测试功能得到输出如下:
Begin trace malloc!
malloc ptr:0x105000000 size:10485760 thread:381125
malloc ptr:0x105a00000 size:10485760 thread:381125
malloc ptr:0x106400000 size:10485760 thread:381125
malloc ptr:0x106e00000 size:10485760 thread:381125
malloc ptr:0x107800000 size:10485760 thread:381125
malloc ptr:0x108200000 size:10485760 thread:381125
malloc ptr:0x108c00000 size:10485760 thread:381125
malloc ptr:0x109600000 size:10485760 thread:381125
malloc ptr:0x10a000000 size:10485760 thread:381125
malloc ptr:0x10aa00000 size:10485760 thread:381125
malloc ptr:0x10b400000 size:10485760 thread:381125
malloc ptr:0x10be00000 size:10485760 thread:381125
^C
End trace
附录
dtruss 工具是一个底层为DTrac的工具,允许跟踪系统调用时打印出C风格的形式,显示系统调用、参数及返回值。druss支持3种使用模式:
- 通过druss 允许一个进程:在druss 的参数后面指定命令参数
- 附加到某个正在允许的进程示例:在druss -p 参数指定进程的PID
- 附加到命名的进程:在druss -n 参数指定进程的名字
druss 的另一个有用的特性是能够自动锁定子进程(指定 -f 参数), 还可以同时当场跟踪器和剖析器使用。
image.png
网友评论