介绍
Branch Trace Store(BTS)是目前广泛被intel CPU所提供的一种硬件辅助调试功能,因为在MSRA项目需要,所以作了一些基于它的应用。虽然Intel的CPU开发手册[1]提供了比较详细的使用方法,但是由于比较笼统,且缺少win32下的相关资料。所以我打算把其中的一些tricky的事情和大家分享下。这些内容当然也是我自己查找公开资料得到的,因此也不算什么秘密吧 :-)
BTS简单的说就是允许CPU将自己实际执行到的分支指令(jmp/jxx/call/int/etc.)的相关信息保存下来的功能。一般CPU都会保存每个分支指令的源地址和目标地址,该地址在保护模式下是虚拟地址形式表示的。利用这个功能,可以实时地了解当前CPU正在执行代码的实际流程情况,很多分析软件,如Intel的Vtune或者profiling库,如*nix平台下的perfmon[2]都用它来做一些程序性能分析。当然,还可以做很多其他有趣的事,比如逆向工程,具体我就不说了。
不过目前很少有资料具体介绍如何在win32下开启该功能并实现一个可用的BTS捕捉引擎。当然可以参考perfmon的代码,但BTS在其中只是很小一个部分,同时为了实现跨平台,对于新手来说难度较大。因此我这里重点介绍对于单核心的NetBurst构架的CPU(通俗地说就是P4这类)在win32下的具体实现细节。其他的构架,比如现在的Core Due,大家可以参考intel的开发手册举一反三。
工作框架
实际上,上面所说的BTS只是intel对于分支指令信息捕获机制的一个分支,就P4而言,大致提供了下列几种分支指令的捕获手段
Last Branch Record(LBR)
故名思意,该方式将记录最后几个分支信息,实际上NetBurst CPU中内建了若干个MSR寄存器用于记录Last Branch Record,称为LBR Stack。在该模式下运行的CPU会采用Round Robin的方式循环填充那几个MSR寄存器。
该方式常用在调试器的Call Stack分析上,我们这里就不涉及了
Branch Trace Messages
该方式和我们将介绍的Branch Trace Store大致类似,与LBR不同的是,CPU会把分支信息发送到系统总线上供第三方硬件在总线上接收数据。而BTS则直接将数据保存到由程序制定的内存单元中。我们这里也不讨论该方式。
Branch Trace Store
简单的说,BTS就是将分支信息保存到了有程序(实际就是我们)制定的一块内存空间中。而当这块内存用尽时,CPU可以采用Round Robin的方式循环填充,这就和LBR类似,但可以指定内存块的大小来决定最大捕捉量。另一种处理方式是在内存快用尽的时候,一个事先由我们设定的处理中断将触发来完成对当前内存块中BTS数据的保存工作。这样就可以记录任意多的分支信息了。我们这里主要考虑的就是这种方式的BTS。
按照intel开发手册的描述,BTS开启后的CPU执行模式如下图所示:

每当CPU执行到一个分支指令后,它就会产生一个如上左图的BTS记录项。这里举个例子:
0x80001234 mov eax,[esp]
0x80001238 or eax,eax
0x80001240 jnz 0x80002000
0x80001245 nop
...
当执行上面这段执行,其中遇到了jnz指令,如果他的跳转条件成立,那么将产生一个从0x80001240到0x80002000的跳转,那么产生的BTS记录项就是{From:0x80001240,To:0x80002000,...}
对于P4的CPU,这个BTS记录项的结构如下:
struct BTS_ITEM_BLOCK_P4{ ULONG dwBranchFrom; ULONG dwBranchTo; ULONG dwFlags; };
每次CPU执行到分支指令的时候,只要BTS开启,它就会产生一个如上结构得数据块,然后把它存储到事先定义好的一块内存当中去。而这块内存是比较灵活的,如上图所示,CPU需要知道这块内存的起始地址、结束地址、以及当记录到第几个记录时候需要触发一个中断程序来负责将现有的BTS记录保存下来防止内存溢出。当BTS记录存储到图中那个灰底色的“BTS记录#m”时,就会触发中断,可以发现它往往并不是整块记录内存的末端,具体原因就不解释了。
为了让CPU知道我们设置的内存空间的信息,需要提供一个叫做DS_BUFFER_MGR_BLOCK的结构来描述这块内存,他包括的内容如图所示,然后我们需要设置一个名为IA32_DS_AREA的MSR寄存器来指向这个DS_BUFFER_MGR_BLOCK数据的地址。
P4下DS_BUFFER_MGR_BLOCK的完整结构是:
struct DS_BUFFER_MGR_BLOCK_P4{ PVOID pBTSBufferBase; PVOID pBTSBufferIndex; PVOID pBTSMaxSize; PVOID pBTSIntThresold; PVOID pPEBSBufferBase; PVOID pPEBSIndex; PVOID pPEBSMaxSize; PVOID pPEBSIntThresold; ULONG dwPEBSCounterReset; ULONG Reserved; };
注意上面黑体的条目,这些部分是BTS需要设置的,后面的是PEBS采用机制所需要的信息,这里我们忽略他们。这些条目需要设置成保存BTS内存的相关地址(虚拟内存地址)。
同时,还需要让CPU知道用于处理BTS溢出的中断号。目前的Local APIC控制器上有一个称为Performance Mon. Counter的寄存器项。当BTS记录项接近溢出时,就会通过这个记录项中存储的中断向量值去触发对应的中断程序。这个Performance Mon. Counter的格式如下图所示(图片来自intel手册):
由于APIC芯片寄存器映射到了物理内存空间0xFEE00000H处,因此,对于其中寄存器的读写操作可以像标准内存操作那样进行。不过,由于采用虚拟内存的操作系统环境下,需要将该空间映射到对应的虚拟内存地址下,同时要避免操作系统的缓存行为。该部分细节我会在后面介绍。对于APIC芯片和LVT的一些具体细节可以参考intel手册[3]。
以上便是整个BTS工作的框架,如果不考虑具体的操作系统环境,对于他们的设置不存在很大的难度。不过由于OS引入了各类抽象机制或者保护手段,所以有一些tricky的问题需要处理。下面我讲介绍一个具体的实现流程。
待续......