1.3 软件调试的关键技术——断点
在某种意义上,断点可以说是软件调试技术的基础。通过设置断点,调试器才得以控制被调程序的整个运行过程。断点可以分为软件断点和硬件断点,也可以分为指令断点与数据断点。下面将结合具体的平台讨论断点的作用和工作原理。
1.3.1 软件断点
软件断点是最常用的断点技术,它通常是一种特殊类型的机器指令,如在x86平台上是INT 3,而在ARM平台上可能是一条非法指令,如0xFFFFFFFF,下文中统称其为断点指令。断点指令将被插入到程序的代码段中。当处理器执行断点指令时,将触发特殊的处理程序,此时调试器将冻结被调程序的执行,将控制权交给用户,于是用户可以查看和修改被调程序当前的执行状态。
软件断点的实现需要满足某些条件。首先,处理器必须提供一种机制保证在发生自陷(trap)或异常(exception)的时候能够跳转到某个确定的位置执行相应的处理代码;其次,软件断点需要修改程序的代码段,把原来位于该位置的正常指令替换为一条自陷指令或非法指令,使得处理器在命中该断点时跳转到异常处理程序中把控制权交给调试器,在从异常处理程序返回时又恢复原来的正常指令。于是,这就要求程序代码段必须是可写的。在嵌入式环境下,这个要求有时是无法满足的,因为嵌入式环境中存储器的组织方式多种多样。很有可能被调程序恰恰位于不可擦写的EPROM或ROM中。在这种情况下,软件断点就无能为力了。
1.3.2 硬件断点
在说明硬件断点之前,先来看另一种和数据相关联的断点——数据断点。数据断点会在CPU企图访问(尤其是写)某个特定的数据时触发断点,就好像在所有访问此项数据的指令上设置了断点一样。对于嵌入式系统,数据断点显得尤其重要。为什么这么说呢?很多嵌入式系统都没有内存管理单元(MMU),因此也就没有划分地址空间一说,系统内核和用户进程运行在同一地址空间中(如μC/OS及μCLinux)。没有了内存保护机制,用户进程的微小错误都可能导致内核空间中的数据被破坏。如果没有数据断点,那么开发者只能眼睁睁地看着内核中的某个变量莫名其妙地被改变了,而不知道“凶手”究竟是何方神圣。有了数据断点之后,只需在被改变的变量上设置好数据断点,当CPU执行会改写此变量的指令时,会立即触发一个自陷或异常,而“犯罪现场”则被保存了下来,据此就有了可以发现“罪魁祸首”的线索。鉴于数据断点颇有“守株待兔”的意味,因此通常把数据断点称为监视点(watchpoint),而把和指令相关联的断点称为断点(breakpoint),以示区别。
硬件断点常常被用做数据监视点。在Intel的StrongARM/XScale系列处理器及x86处理器家族中,都设计了专门的硬件断点寄存器。在x86系列中,可以设置4个硬件断点[8],而且这4个硬件断点是可以自由分配的,既可以把所有4个硬件断点寄存器都用于指令断点,也可以把它们全部用于数据断点。当然,部分用做指令断点部分用做数据断点也是可以的,同时还可以控制断点的读写属性和数据大小等,非常灵活。此外,x86系列处理器中的硬件监视点不仅能监视访问内存的操作,还能监视I/O端口上的输入输出。
硬件断点的原理其实很容易理解。因为无论指令断点还是数据断点,都必然会涉及地址操作。那么,在将内存地址或I/O地址放到地址总线上之前,CPU就可以把地址单元中的地址和硬件断点寄存器中的地址进行比对。如果比对的结果相符,那么立即触发一次自陷或异常,之后的事情就交给软件去处理了。由于地址比对由硬件完成,因此硬件断点的时间开销很小,对被调程序的影响也比较小。
虽然硬件断点拥有速度快的优势,但是其缺陷也很显著,那就是任何处理器所能提供的硬件断点寄存器都是很有限的。比如,x86系列处理器提供的硬件断点寄存器有4个,XScale处理器提供的硬件断点寄存器也是4个。相对于几乎可以无限设置的软件断点而言,硬件断点的个数实在太少了。因此,软件断点和硬件断点之间各有长短,互为补充。常见的做法是指令断点采用软件断点,而硬件断点则用做数据监视点。