动态跟踪主要包括内核空间,用户空间和硬件三个层面。在不通的层面有着不同的技术支撑我们去或许一些有用的信息。次篇文章目的便是介绍一下动态跟踪用到的一些技术。其实,好多技术我也是刚刚接触,不对的地方望指出。希望能打开一扇窗,让大家看到一个不一样的世界。
1.技术介绍
无论是老的技术perf,SystemTap,dtrace,还是新的技术ebpf,bcc,bpftrace,其实他们依赖的基础技术都没差太多。而这些技术是能实现动态跟踪的根本。主要包含调用栈跟踪和事件源两部分。
2.堆栈跟踪
基于帧指针的堆栈
栈帧结构是编译器用来实现过程/函数调用的一种数据结构,栈帧就是利用EBP(栈帧指针)寄存器访问局部变量、参数、函数返回地址等的手段。每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)。当发生函数调用时,首先将子函数的参数按照逆序依次压入栈内,然后将父函数进行调用之后的下一条指令地址作为返回地址压入栈内。接着把父函数的栈帧指针压入栈内。并把栈帧指针指向当前栈定的地址。之后再把子函数的局部变量等数据压入栈内。
这样我们就可以通过帧指针地址,把整个函数的调用栈构建出来。但是我们最常用gcc编译器默认开启了优化,省略了帧指针。导致没发把函数层层调用的调用栈串联起来。所有某些情况下,以帧指针为基础的调用栈没发跟踪。具体关于函数调用栈和帧栈相关的详细内容可参考pwnable.kr-bof WP
debuginfo
debuginfo文件中会包含原单和行号信息。使用一些调试器,例如gdb,即使没有栈帧指针的情况下,也可以遍历堆栈跟踪。debuginfo带来的显著问题就是会导致可执行文件或库占用的空间变特别大。linux中的库比如glibc,如果需要通过debuginfo进行调试,则需要单独安装对应的debuginfo包。
最近分支记录 (Last branch record,LBR)
处理器支持的技术,用于记录cpu的跳转记录。并把分支信息保存在寄存器中,记录的形式为:from -> to的形式,比如A函数调用B函数: A -> B。这种记录形式有一个明显的缺点,就是寄存器的数量是有限的。如果调用栈过长的话,只能记录部分信息。
ORC (oops rewind capability)
用足够详细地描述可执行程序,以及它与源原始代码的关系,使得一个调试器可以向程序员提供有用的信息。是一个很复杂的事情。不然可执行文件的大小怎么会比debuginfo文件差别那么多。有复杂的东西,就会有新的东西来简化它。ORC就是这样东西。ORC的数据格式更简单,使得ORC更加简单和快速。在禁用帧指针的情况下开启ORC,即可以得到一个很好性能,也可以有一个可靠的堆栈跟踪。具体的信息可参考内核文档: ORC unwinder,阿里基础软件团队也有相关的中文介绍文档:The Orcs Are Coming
Synbols
可以通过linux内核符号表,把人类不可读的地址信息,转换成人类可以读的符号信息。比如函数名和变量名。
3.事件源
更具体的信息可参考内核文档:内核文档:trace
tracepoints
tracepoints是放置在代码中的钩子,可以在运行时调用探测函数。tracepoint可以用于跟踪和性能计数。tracepoint可以处于关闭和开启两种状态,处于关闭状态时,几乎不影响代码性能。处于开启状态时,每次tracepoints被执行都会调用提供的探测函数。如果直接使用tracepoints的话,你需要在头文件中定一个tracepoints,在c文件中定义tracepoints的语句。需要包含三个部分,tracepoints名,函数原型和函数参数。通过注册将一个函数(探针)关联到跟踪点。跟踪完毕,再通过注销函数取消关联。这种情况下,你要考虑一些安全问题,比如要在确保没有调用者再使用这个探针。tracepoints通过创建自定义内核模块以注册探针功能,或者基于事件跟踪。
tracepoints用于内核静态跟踪。是开发在内核代码中加入的跟踪点。相比kprobes,tracepoints更加稳定。基于tracepoints可以在不同的内核版本中使用。而kprobes可能会崩溃。所以能使用tracepoints尽量使用。把kprobes作为后备选项。具体的使用可参考内核文档: tracepint-analysis.txt
kprobes
kprobes与tracepoints类似,也是用于内核的跟踪。kprobes可以监测内核的任何事件,和函数指令。无需加载任何内核模块,除非函数有__kprobes/nokprobe_inline的注释,或者有NOKPROBE_SYMBOL标记。我们可以通过/sys/kernel/debug/tracing/kprobe_events加入探测点,通过/sys/kernel/debug/tracing/events/kprobes/<EVENT>/enabled开启探测。<EVENT>为你在/sys/kernel/debug/tracing/kprobe_events中定义的事件名。定义完事件后会在/sys/kernel/debug/tracing/events/kprobes/<EVENT>/目录下生成一些文件。enabled:通过写入1或0来启动或者关闭探测。format:展示了探测事件的格式。filter:这个事件的过滤规则。id:这个事件的id。具体使用可参考:kprobetrace.txt,在bcc中可以监测函数的开始,和加了偏移的函数指令,bpftrace则只支持函数的开始。
kretprobes
与kprobes相同,只是kretprobes在函数返回的时候监测,可以获取到函数返回值。通过kprobe和kretprobes组合使用就可以计算出某个函数的运行时间。在eBPF中可通过映射保存中间数据。比如调用函数的起始时间,然后在kretprobes中计算函数的运行时间。
uprobes
用户级别的动态跟踪,与kprobes类似。与kprobes不同的是,uprobe期望用户计算探测点在对象中的偏移。
USDT
用户级的静态跟踪。是一个用户级的tracepoints。不少开源应用和库都加入了USDT,用于调试。比如mysql,java,pgsql,node.js,GUN C Library
PMCS
性能监控计数器
性能计数器允许用户对选定的处理器参数进行监视和统计,根据这些统计值进行系统调优。比较凄惨的是,云环境基本无法使用。
PERF_EVENTS
perf_events是linux内核内建的系统性能分析工具。事件的来源有许多:比如性能计数器,中断,系统调用,kprobe,uprobe,tracepoint等等。perf_events是eBPF中建议的,内核与用户空间通信的方式。在BCC中需要同时在内核和用户空间代码定义event的内容。在内核空间的结构体中填充数据,填充完毕后发送到用户空间。相关的文档可参考:Brendan Gregg的博客Linux perf Examples,中文文档:使用 perf_events 分析程式效能
4.后记
动态跟踪需要的用到的知识点实在太多,每个点深入都需要花不少时间。我也只是刚开始接触。倒是希望能把自己的一点点所得分享出来。知识所限,可能有不少有问题的地方,只能通过贴链接的形式解决。算是抛砖引玉吧。
网友评论