Linux 内核调试器内幕
KDB 入门指南
简介: 调试内核问题时,能够跟踪内核执行情况并查看其内存和数据结构是非常有用的。Linux 中的内置内核调试器 KDB 提供了这种功能。在本文中您将了解如何使用 KDB 所提供的功能,以及如何在 Linux 机器上安装和设置 KDB。您还将熟悉 KDB 中可以使用的命令以及设置和显示选项。
Linux 内核调试器(KDB)允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。KDB 的主要优点之一就是它不需要用另一台机器进行调试:您可以调试正在运行的内核。
设置一台用于 KDB 的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译(在一定程度上还要熟悉内核内部机理),但是如果您需要编译内核方面的帮助,请参阅本文结尾处的参考资料一节。
在本文中,我们将从有关下载 KDB 补丁、打补丁、(重新)编译内核以及启动 KDB 方面的信息着手。然后我们将了解 KDB 命令并研究一些较常用的命令。最后,我们将研究一下有关设置和显示选项方面的一些详细信息。
入门
KDB 项目是由 Silicon Graphics 维护的(请参阅 参考资料以获取链接),您需要从它的 FTP 站点下载与内核版本有关的补丁。(在编写本文时)可用的最新 KDB 版本是 4.2。您将需要下载并应用两个补丁。一个是“公共的”补丁,包含了对通用内核代码的更改,另一个是特定于体系结构的补丁。补丁可作为 bz2 文件获取。例如,在运行 2.4.20 内核的 x86 机器上,您会需要 kdb-v4.2-2.4.20-common-1.bz2 和 kdb-v4.2-2.4.20-i386-1.bz2。
这里所提供的所有示例都是针对 i386 体系结构和 2.4.20 内核的。您将需要根据您的机器和内核版本进行适当的更改。您还需要拥有 root 许可权以执行这些操作。
将文件复制到 /usr/src/linux 目录中并从用 bzip2 压缩的文件解压缩补丁文件:
#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2 |
您将获得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。
现在,应用这些补丁:
#patch -p1 <kdb-v4.2-2.4.20-common-1#patch -p1 <kdb-v4.2-2.4.20-i386-1 |
这些补丁应该干净利落地加以应用。查找任何以 .rej 结尾的文件。这个扩展名表明这些是失败的补丁。如果内核树没问题,那么补丁的应用就不会有任何问题。
接下来,需要构建内核以支持 KDB。第一步是设置 CONFIG_KDB
选项。使用您喜欢的配置机制(xconfig 和 menuconfig 等)来完成这一步。转到结尾处的“Kernel hacking”部分并选择“Built-in Kernel Debugger support”选项。
您还可以根据自己的偏好选择其它两个选项。选择“Compile the kernel with frame pointers”选项(如果有的话)则设置 CONFIG_FRAME_POINTER
标志。这将产生更好的堆栈回溯,因为帧指针寄存器被用作帧指针而不是通用寄存器。您还可以选择“KDB off by default”选项。这将设置CONFIG_KDB_OFF
标志,并且在缺省情况下将关闭 KDB。我们将在后面一节中对此进行详细介绍。
保存配置,然后退出。重新编译内核。建议在构建内核之前执行“make clean”。用常用方式安装内核并引导它。
回页首
初始化并设置环境变量
您可以定义将在 KDB 初始化期间执行的 KDB 命令。需要在纯文本文件 kdb_cmds 中定义这些命令,该文件位于 Linux 源代码树(当然是在打了补丁之后)的 KDB 目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您更改了文件之后需要重新构建并重新安装内核。
回页首
激活 KDB
如果编译期间没有选中 CONFIG_KDB_OFF
,那么在缺省情况下 KDB 是活动的。否则,您需要显式地激活它 - 通过在引导期间将kdb=on
标志传递给内核或者通过在挂装了 /proc 之后执行该工作:
#echo "1" >/proc/sys/kernel/kdb |
倒过来执行上述步骤则会取消激活 KDB。也就是说,如果缺省情况下 KDB 是打开的,那么将 kdb=off
标志传递给内核或者执行下面这个操作将会取消激活 KDB:
#echo "0" >/proc/sys/kernel/kdb |
在引导期间还可以将另一个标志传递给内核。 kdb=early
标志将导致在引导过程的初始阶段就把控制权传递给 KDB。如果您需要在引导过程初始阶段进行调试,那么这将有所帮助。
调用 KDB 的方式有很多。如果 KDB 处于打开状态,那么只要内核中有紧急情况就自动调用它。按下键盘上的 PAUSE 键将手工调用 KDB。调用 KDB 的另一种方式是通过串行控制台。当然,要做到这一点,需要设置串行控制台(请参阅参考资料以获取这方面的帮助)并且需要一个从串行控制台进行读取的程序。按键序列 Ctrl-A 将从串行控制台调用 KDB。
回页首
KDB 命令
KDB 是一个功能非常强大的工具,它允许进行几个操作,比如内存和寄存器修改、应用断点和堆栈跟踪。根据这些,可以将 KDB 命令分成几个类别。下面是有关每一类中最常用命令的详细信息。
内存显示和修改
这一类别中最常用的命令是 md
、 mdr
、 mm
和 mmW
。
md
命令以一个地址/符号和行计数为参数,显示从该地址开始的 line-count
行的内存。如果没有指定line-count
,那么就使用环境变量所指定的缺省值。如果没有指定地址,那么 md
就从上一次打印的地址继续。地址打印在开头,字符转换打印在结尾。
mdr
命令带有地址/符号以及字节计数,显示从指定的地址开始的 byte-count
字节数的初始内存内容。它本质上和md
一样,但是它不显示起始地址并且不在结尾显示字符转换。 mdr
命令较少使用。
mm
命令修改内存内容。它以地址/符号和新内容作为参数,用 new-contents
替换地址处的内容。
mmW
命令更改从地址开始的 W
个字节。请注意, mm
更改一个机器字。
示例
显示从 0xc000000 开始的 15 行内存:
[0]kdb> md 0xc000000 15 |
将内存位置为 0xc000000 上的内容更改为 0x10:
[0]kdb> mm 0xc000000 0x10 |
寄存器显示和修改
这一类别中的命令有 rd
、 rm
和 ef
。
rd
命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了 c
参数,则 rd
显示处理器的控制寄存器;如果带有 d
参数,那么它就显示调试寄存器;如果带有 u
参数,则显示上一次进入内核的当前任务的寄存器组。
rm
命令修改寄存器的内容。它以寄存器名称和 new-contents
作为参数,用 new-contents
修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。
ef
命令以一个地址作为参数,它显示指定地址处的异常帧。
示例
显示通用寄存器组:
[0]kdb> rd |
[0]kdb> rm %ebx 0x25 |
断点
常用的断点命令有 bp
、 bc
、 bd
、 be
和bl
。
bp
命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。 bpa
命令对 SMP 系统中的所有处理器应用断点。 bph
命令强制在支持硬件寄存器的系统上使用它。 bpha
命令类似于bpa
命令,差别在于它强制使用硬件寄存器。
bd
命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从 0 开始,根据可用性顺序分配给断点。
be
命令启用断点。该命令的参数也是断点号。
bl
命令列出当前的断点集。它包含了启用的和禁用的断点。
bc
命令从断点表中除去断点。它以具体的断点号或 *
作为参数,在后一种情况下它将除去所有断点。
示例
对函数 sys_write()
设置断点:
[0]kdb> bp sys_write |
列出断点表中的所有断点:
[0]kdb> bl |
清除断点号 1:
[0]kdb> bc 1 |
>堆栈跟踪
主要的堆栈跟踪命令有 bt
、 btp
、 btc
和 bta
。
bt
命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址,那么它采用当前寄存器来回溯堆栈。否则,它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了CONFIG_FRAME_POINTER
选项,那么就用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯。如果没有设置 CONFIG_FRAME_POINTER
,那么 bt
命令可能会产生错误的结果。
btp
命令将进程标识作为参数,并对这个特定进程进行堆栈回溯。
btc
命令对每个活动 CPU 上正在运行的进程执行堆栈回溯。它从第一个活动 CPU 开始执行 bt
,然后切换到下一个活动 CPU,以此类推。
bta
命令对处于某种特定状态的所有进程执行回溯。若不带任何参数,它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下:
- D:不可中断状态
- R:正运行
- S:可中断休眠
- T:已跟踪或已停止
- Z:僵死
- U:不可运行
这类命令中的每一个都会打印出一大堆信息。请查阅下面的 参考资料以获取这些字段的详细文档。
示例
跟踪当前活动线程的堆栈:
[0]kdb> bt |
跟踪标识为 575 的进程的堆栈:
[0]kdb> btp 575 |
其它命令
下面是在内核调试过程中非常有用的其它几个 KDB 命令。
id
命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量 IDCOUNT
确定要显示多少行输出。
ss
命令单步执行指令然后将控制返回给 KDB。该指令的一个变体是 ssb
,它执行从当前指令指针地址开始的指令(在屏幕上打印指令),直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有call
、 return
和 jump
。
go
命令让系统继续正常执行。一直执行到遇到断点为止(如果已应用了一个断点的话)。
reboot
命令立刻重新引导系统。它并没有彻底关闭系统,因此结果是不可预测的。
ll
命令以地址、偏移量和另一个 KDB 命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。
示例
反汇编从例程 schedule 开始的指令。所显示的行数取决于环境变量
IDCOUNT
:
[0]kdb> id schedule |
执行指令直到它遇到分支转移条件(在本例中为指令
jne
)为止:
[0]kdb> ssb0xc0105355 default_idle+0x25: cli0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax0xc0105359 default_idle+0x29: test %eax, %eax0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31 |
回页首
技巧和诀窍
调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。
结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。
调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。
因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。
技巧 #1
在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。
示例
表明函数
sys_read
从地址 0xc013db4c 开始:
[0]kdb> 0xc013db4c0xc013db4c = 0xc013db4c (sys_read) |
同样,
同样,表明
sys_write
位于地址 0xc013dcc8:
[0]kdb> sys_writesys_write = 0xc013dcc8 (sys_write) |
这些有助于在分析堆栈时找到全局数据和函数地址。
技巧 #2
在编译带 KDB 的内核时,只要 CONFIG_FRAME_POINTER
选项出现就使用该选项。为此,需要在配置内核时选择“Kernel hacking”部分下面的“Compile the kernel with frame pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在 i386 机器上,%ebp 寄存器可以用来回溯整个堆栈。
例如,在函数 rmqueue()
上执行第一个指令后,堆栈看上去类似于下面这样:
[0]kdb> md %ebp0xc74c9f38 c74c9f60 c0136c40 000001f0 000000000xc74c9f48 08053328 c0425238 c04253a8 000000000xc74c9f58 000001f0 00000246 c74c9f6c c0136a250xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc0xc74c9f78 c014fe45 c74c8000 00000000 08053328[0]kdb> 0xc0136c400xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)[0]kdb> 0xc0136a250xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)[0]kdb> 0xc0136d6d0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd) |
我们可以看到 rmqueue()
被 __alloc_pages
调用,后者接下来又被 _alloc_pages
调用,以此类推。
每一帧的第一个双字(double word)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。
技巧 #3
go
命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用 rm
命令修改指令指针寄存器,然后只要输入 go
。如果您想跳过似乎会引起问题的某个特定指令或一组指令,这就会很有用。但是,请注意,该指令使用不慎会造成严重的问题,系统可能会严重崩溃。
技巧 #4
您可以利用一个名为 defcmd
的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。defcmd
允许您定义自己的命令,该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:
[0]kdb> defcmd name "usage" "help"[0]kdb> [defcmd] type the commands here[0]kdb> [defcmd] endefcmd |
例如,可以定义一个(简单的)新命令 hari
,它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈:
[0]kdb> defcmd hari "" "no arguments needed"[0]kdb> [defcmd] md 0xc000000 1[0]kdb> [defcmd] rd[0]kdb> [defcmd] md %ebp 1[0]kdb> [defcmd] endefcmd |
该命令的输出会是:
[0]kdb> hari[hari]kdb> md 0xc000000 10xc000000 00000001 f000e816 f000e2c3 f000e816[hari]kdb> rdeax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000.......[hari]kdb> md %ebp 10xc0467fbc c0467fd0 c01053d2 00000002 000a0200[0]kdb> |
技巧 #5
可以使用 bph
和 bpha
命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。
示例
每当将四个字节写入地址 0xc0204060 时就进入内核调试器:
[0]kdb> bph 0xc0204060 dataw 4 |
在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器:
[0]kdb> bph 0xc000000 datar 2 |
回页首
结束语
对于执行内核调试,KDB 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
- 请在 Documentation/kdb 目录中查找 KDB 手册页。
- 有关设置串行控制台的信息,请查找 Documentation 目录中的 serial-console.txt。
- 请在 SGI 的内核调试器项目网站上 下载 KDB。
- 有关几个基于方案的 Linux 调试技术的概述,请阅读“ 掌握 Linux 调试技术”( developerWorks,2002 年 8 月)。
- 教程“ 编译 Linux 内核”( developerWorks,2000 年 8 月)让您完整地了解配置、编译和安装内核的过程。
- IBM AIX 用户可以在 KDB Kernel Debugger and Command页面上获取有关用于 AIX 的 KDB 的使用帮助。
- 那些寻求有关调试 OS/2 信息的读者应该阅读 IBM 红皮书 The OS/2 Debugging Handbook(共四卷)的 第 II 卷。
- 在 developerWorksLinux 专区中查找更多 针对 Linux 开发人员的参考资料。
关于作者
Hariprasad Nellitheertha 在印度班加罗尔(Bangalore)的 IBM Linux 技术中心工作。他目前正在 Linux Change Team 从事修正内核和其它 Linux 错误的工作。Hari 研究过 OS/2 内核和文件系统。他的兴趣包括 Linux 内核内部机理、文件系统和自主计算。可以通过nharipra@in.ibm.com与 Hari 联系。
嵌入式操作系统的调试
简介: 调试是开发过程中必不可少的环节,通用的桌面操作系统与嵌入式操作系统在调试环境上存在明显的差别。前者,调试器与被调试的程序往往是运行在同一台机器、相同的操作系统上的两个进程,调试器进程通过操作系统专门提供的调用接口(早期UNIX系统的ptrace调用、如今的进程文件系统等)控制、访问被调试进程。后者(又称为远程调试),为了向系统开发人员提供灵活、方便的调试界面,调试器还是运行于通用桌面操作系统的应用程序,被调试的程序则运行于基于特定硬件平台的嵌入式操作系统(目标操作系统)。这就带来以下问题:调试器与被调试程序如何通信,被调试程序产生异常如何及时通知调试器,调试器如何控制、访问被调试程序,调试器如何识别有关被调试程序的多任务信息并控制某一特定任务,调试器如何处理某些与目标硬件平台相关的信息(如目标平台的寄存器信息、机器代码的反汇编等)。
我们介绍两种远程调试的方案,看它们怎样解决这些问题。
调试方案
一 插桩(stub)
第一种方案是在目标操作系统和调试器内分别加入某些功能模块,二者互通信息来进行调试。上述问题可通过以下途径解决:
- 调试器与被调试程序的通信
调试器与目标操作系统通过指定通信端口(串口、网卡、并口)遵循远程调试协议进行通信。 - 被调试程序产生异常及时通知调试器
目标操作系统的所有异常处理最终都要转向通信模块,告知调试器当前的异常号;调试器据此向用户显示被调试程序产生了哪一类异常。 - 调试器控制、访问被调试程序
调试器的这类请求实际上都将转换成对被调试程序的地址空间或目标平台的某些寄存器的访问,目标操作系统接收到这样的请求可以直接处理。对于没有虚拟存储概念的简单的嵌入式操作系统而言,完成这些任务十分容易。 - 调试器识别有关被调试程序的多任务信息并控制某一特定任务
由目标操作系统提供相关接口。目标系统根据调试器发送的关于多任务的请求,调用该接口提供相应信息或针对某一特定任务进行控制,并返回信息给调试器。 - 调试器处理与目标硬件平台相关的信息
第2条所述调试器应能根据异常号识别目标平台产生异常的类型也属于这一范畴,这类工作完全可以由调试器独立完成。支持多种目标平台正是GNUGDB的一大特色。
综上所述,这一方案需要目标操作系统提供支持远程调试协议的通信模块(包括简单的设备驱动)和多任务调试接口,并改写异常处理的有关部分。另外目标操作系统还需要定义一个设置断点的函数;因为有的硬件平台提供能产生特定调试陷阱异常(debugtrap)的断点指令以支持调试(如X86的INT3),而另一些机器没有类似的指令,就用任意一条不能被解释执行的非法(保留)指令代替。目标操作系统添加的这些模块统称为"插桩"(见下图),驻留于ROM中则称为ROMmonitor。通用操作系统也有具备这类模块的:编译运行于Alpha、Sparc或PowerPC平台的LINUX内核时若将kgdb开关打开,就相当于加入了插桩。
图1
运行于目标操作系统的被调试的应用程序要在入口处调用这个设置断点的函数以产生异常,异常处理程序调用调试端口通信模块,等待主机(host)上的调试器发送信息。双方建立连接后调试器便等待用户发出调试命令,目标系统等待调试器根据用户命令生成的指令。这一过程如下图所示。
图2
这一方案的实质是用软件接管目标系统的全部异常处理(exceptionhandler)及部分中断处理,在其中插入调试端口通信模块,与主机的调试器交互。它只能在目标操作系统初始化,特别是调试通信端口初始化完成后才起作用,所以一般只用于调试运行于目标操作系统之上的应用程序,而不宜用来调试目标操作系统,特别是无法调试目标操作系统的启动过程。而且由于它必然要占用目标平台的某个通信端口,该端口的通信程序就无法调试了。最关键的是它必须改动目标操作系统,这一改动即使没有对操作系统在调试过程中的表现造成不利影响,至少也会导致目标系统多了一个不用于正式发布的调试版。
二 片上调试(On Chip Debugging)及EmbeddedPowerPC Background Debug Mode
片上调试是在处理器内部嵌入额外的控制模块,当满足了一定的触发条件时进入某种特殊状态。在该状态下,被调试程序停止运行,主机的调试器可以通过处理器外部特设的通信接口访问各种资源(寄存器、存储器等)并执行指令。为了实现主机通信端口与目标板调试通信接口各引脚信号的匹配,二者往往通过一块简单的信号转换电路板连接(如下图所示)。内嵌的控制模块以基于微码的监控器(microcodemonitor)或纯硬件资源的形式存在,包括一些提供给用户的接口(如断点寄存器等)。具体产品有MotorolaCPU16、CPU32、Coldfire系列的BDM(Background Debug Mode),MotorolaPowerPC 5xx、8xx系列的EPBDM(Embedded PowerPC Background DebugMode),IBM、TI的JTAG(Joint Test ActionDebug,IEEE标准),还有OnCE、MPSD等等。下面以MPC860的EPBDM为例介绍片上调试方式。
图3
EPBDM的运作相当于用处理器内嵌的调试模块接管中断及异常处理。用户通过设置调试许可寄存器(debugenableregister)来指定哪些中断或异常发生后处理器直接进入调试状态,而不是操作系统的处理程序。进入调试状态后,内嵌调试模块向外部调试通信接口发出信号,通知一直在通信接口监听的主机调试器,然后调试器便可通过调试模块使处理器执行任意系统指令(相当于特权态)。所有指令均通过调试模块获取,所有load/store均直接访问内存,缓存(cache)及存储管理单元(MMU)均不可用;数据寄存器被映射为一个特殊寄存器DPDR,通过mtspr和mfspr指令访问。调试器向处理器送rfi(returnfrom interrupt)指令便结束调试状态,被调试程序继续运行。
与插桩方式的缺点相对应,OCD不占用目标平台的通信端口,无需修改目标操作系统,能调试目标操作系统的启动过程,大大方便了系统开发人员。随之而来的缺点是软件工作量的增加:调试器端除了需补充对目标操作系统多任务的识别、控制等模块,还要针对使用同一芯片的不同开发板编写各类ROM、RAM的初始化程序。
下面就以调试运行于MPC860的LINUX为例,说明用OCD方式调试OS启动的某些关键细节。
首先,LINUX内核模块以压缩后的zImage形式驻留于目标板的ROM,目标板上电后先运行ROM中指定位置的程序将内核移至RAM并解压缩,然后再跳转至内核入口处运行。要调试内核,必须在上电后ROM中的指令执行之前获得系统的控制权,即进入调试状态、设断点,这样才能开展调试过程。MPC860的EPBDM提供了这一手段。
MPC860没有类似X86的INT3那样能产生特定调试陷阱异常的指令,而操作系统内核往往具有针对非法指令的异常处理;为了使对内核正常运行的干扰降至最小,调试时应尽量设置硬件断点,而不是利用非法指令产生异常的"软"断点。
LINUX实现了虚存管理,嵌入式LINUX往往也有这一功能。地址空间从实到虚的转换在内核启动过程中便完成了,不论调试内核还是应用程序,调试器都无法回避对目标系统虚地址空间的访问,否则断点命中时根本无法根据程序计数器的虚地址显示当前指令,更不用说访问变量了。由于调试状态下转换旁视缓冲器(TranslationLookasideBuffer)无法利用,只能仿照LINUX内核TLB失效时的异常处理程序,根据虚地址中的页表索引位访问特定寄存器查两级页表得出物理页面号,从而完成虚实地址的转换。MPC860采用哈佛结构(Harvardarchitecture),指令和数据缓存分离设置(因为程序的指令段和数据段是分离的,这种结构可以消除取指令和访问数据之间的冲突),二者的TLB也分离设置;然而TLB失效时查找页表计算物理地址的过程是相同的,因为页表只有一个,不存在指令、数据分离的问题。虚实地址转换这一任务虽然完全落在了调试器一方,由于上述原因,再加上调试对象是嵌入式系统,一般不会有外存设备,不必考虑内存访问缺页的情况,所以增加的工作量并不大。
回页首
深入话题
传统的调试方法可概括为如下过程:设断点--程序暂停--观察程序状态--继续运行。被调试的如果是实时系统,即使调试器支持批处理命令避免了用户输入命令、观察结果带来的延迟,它与目标系统之间的通信也完全可能错过对目标平台外设信号的响应。于是,针对某些调试器(如GDB)提供的监视点(tracepoint)这一特殊调试手段,目标方的插桩在原有的基础上被改进,称为代理(agent)。调试时用户首先在调试器设置监视点,以源代码表达式的形式指定感兴趣的对象名。为了减少代理解析表达式的工作,调试器将表达式转换为简单的字节码,传送至代理。程序运行后命中监视点、唤醒代理,代理根据字节码记录用户所需数据存入特定缓冲区(不仅仅是表达式的最终结果,还有中间结果),令程序继续运行;这一步骤无需与调试器通信。当调试器再度得到控制时,就可以发出命令,向代理查询历次监视记录。较之于插桩,代理增加了对接受到的字节码的分析模块,相应的目标代码体积只有大约3K字节;当然,监视记录缓冲区也要占用目标平台的存储空间,不过缓冲区的大小可在代理生成时由用户决定。总之,这一改进以有限的目标系统资源为代价,为实时监视提供了一个低成本的可行方案。
调试并不仅仅意味着设断点--程序暂停--观察--继续这一过程,往往还需要profiling、跟踪(trace)等多种手段,而现代微处理器的技术进步却为这些调试手段的实行带来了困难。以跟踪为例,其目的无非是记录真实的程序运行流;可现代处理器指令缓存都集成于芯片内(RISC处理器尤为如此),运行指令时"取指"这一操作大多在芯片内部针对指令缓存进行,芯片外部总线上只能观察到多条指令的预取(prefetch),预取的指令并不一定执行(由于跳转等原因);另外,指令往往经过动态调度后在流水线中乱序执行,如何再现其原始顺序也是个问题。解决方案大致有以下三种:
- 有的处理器除了正常运行外,还能以串行方式运行,所有的取指周期都可呈现于片外总线(相当于禁用缓存与流水线)。这样一来,跟踪容易多了,处理器性能也大大降低了,根本不适用于实时要求严格的系统。
- 编译器自动在指定的分支及函数出入口插入对特定内存区域的写指令(与gprof等profiling工具采用的手段类似),它们都是不通过缓存而直接向内存写的,这就能反映于芯片外总线从而被外接的逻辑分析仪记录,最终由主机端的调试工具分析并结合符号表重构程序流。这种方法虽被广泛使用,但毕竟是干扰式的(intrusive),对系统性能也有影响。
- 像上文所述的片上调试那样,也有处理器在片内附加了跟踪电路,收集程序流运行时的"不连贯"(discontinuities)信息(分支和异常处理的跳转目的及源地址等),压缩后送至特定端口,再由逻辑分析仪捕获送至主机端调试工具重构程序流。该方案对系统性能影响最小。
总之,处理器厂家提供集成于片内的调试电路为高档嵌入式系统开发提供各种非干扰式的调试手段早已是大势所趋。为了解决该领域标准化的需要,一些处理器厂家、工具开发公司和仪器制造商于1998年组成了Nexus5001Forum,这是一个旨在为嵌入式控制应用产生和定义嵌入式处理器调试接口标准的联合组织,以前的名称是GlobalEmbedded Processor Debug Interface StandardConsortium(全球嵌入式处理器调试接口标准协会)。Nexus现在有24个成员单位,包括创始成员Motorola、InfineonTechnologies、日立、ETAS和HP等公司。该组织首先处理的是汽车动力应用所需要的调试,现在已发展成为调试数据通信、无线系统和其他实时嵌入式应用的通用接口。
参考资料
- MPC860 PowerQUICC User's Manual MOTOROLA
- http://www.vas-gmbh.de/software/mpcbdm/
- http://www.metrowerks.com/tools/documentation/embedded/zenofbdm
- http://www.redhat.com/support/wpapers/cygnus_heinsenberg/trace.html
- http://www.ednmag.com/reg/2000/05112000/10tt.htm
关于作者
熊竞,任职于中科院计算所嵌入式系统软件组。主要从事开发嵌入式操作系统的仿真器、调试器及集成开发环境。您可以通过电子邮件jxiong@ict.ac.cn与他联系。