上篇回顾:一文Linux内核调试方法(一)

KGDB

kgdb提供了一种使用gdb调试Linux内核的机制。使用KGDB可以象调试普通的应用程序那样,在内核中进行设置断点、检查变量值、单步跟踪程序运行等操作。使用KGDB调试时需要两台机器,一台作为开发机(DevelopmentMachine),另一台作为目标机(TargetMachine),两台机器之间通过串口或者以太网口相连。串口连接线是一根RS-232接口的电缆,在其内部两端的第2脚(TXD)与第3脚(RXD)交叉相连,第7脚(接地脚)直接相连。调试过程中,被调试的内核运行在目标机上,gdb调试器运行在开发机上。
目前,kgdb发布支持i386、x86_64、32-bitPPC、SPARC等几种体系结构的调试器。

嵌入式进阶教程分门别类整理好了,看的时候十分方便,由于内容较多,这里就截取一部分图吧。

需要的朋友私信【内核】即可领取。


kgdb的调试原理

安装kgdb调试环境需要为Linux内核应用kgdb补丁,补丁实现的gdb远程调试所需要的功能包括命令处理、陷阱处理及串口通讯3个主要的部分。kgdb补丁的主要作用是在Linux内核中添加了一个调试Stub。调试Stub是Linux内核中的一小段代码,提供了运行gdb的开发机和所调试内核之间的一个媒介。gdb和调试stub之间通过gdb串行协议进行通讯。gdb串行协议是一种基于消息的ASCII码协议,包含了各种调试命令。当设置断点时,kgdb负责在设置断点的指令前增加一条trap指令,当执行到断点时控制权就转移到调试stub中去。此时,调试stub的任务就是使用远程串行通信协议将当前环境传送给gdb,然后从gdb处接受命令。gdb命令告诉stub下一步该做什么,当stub收到继续执行的命令时,将恢复程序的运行环境,把对CPU的控制权重新交还给内核

Kgdb的安装与设置

下面我们将以内核为例详细介绍kgdb调试环境的建立过程。

软硬件准备

以下软硬件配置取自笔者进行试验的系统配置情况:
kgdb补丁的版本遵循如下命名模式:Linux-A-kgdb-B,其中A表示Linux的内核版本号,B为kgdb的版本号。以试验使用的kgdb补丁为例,linux内核的版本为,补丁版本为。
物理连接好串口线后,使用以下命令来测试两台机器之间串口连接情况,stty命令可以对串口参数进行设置:
在development机上执行:

sttyispeed115200ospeed115200-F/dev/ttyS0在target机上执行:sttyispeed115200ospeed115200-F/dev/ttyS0在developement机上执行:echohello/dev/ttyS0

在target机上执行:

cat/dev/ttyS0

如果串口连接没问题的话在将在target机的屏幕上显示"hello"。

安装与配置

下面我们需要应用kgdb补丁到Linux内核,设置内核选项并编译内核。这方面的资料相对较少,笔者这里给出详细的介绍。下面的工作在开发机(developement)上进行,以上面介绍的试验环境为例,某些具体步骤在实际的环境中可能要做适当的改动:

内核的配置与编译

[root@lisltmp][root@lisltmp]patch-p1..//

如果内核正确,那么应用补丁时应该不会出现任何问题(不会产生*.rej文件)。为Linux内核添加了补丁之后,需要进行内核的配置。内核的配置可以按照你的习惯选择配置Linux内核的任意一种方式。

[root@lisltmp]make

编译内核之前请注意Linux目录下Makefile中的优化选项,默认的Linux内核的编译都以-O2的优化级别进行。在这个优化级别之下,编译器要对内核中的某些代码的执行顺序进行改动,所以在调试时会出现程序运行与代码顺序不一致的情况。可以把Makefile中的-O2选项改为-O,但不可去掉-O,否则编译会出问题。为了使编译后的内核带有调试信息,注意在编译内核的时候需要加上-g选项。

不过,当选择"Kerneldebugging-Compilethekernelwithdebuginfo"选项后配置系统将自动打开调试选项。另外,选择"kerneldebuggingwithremotegdb"后,配置系统将自动打开"Compilethekernelwithdebuginfo"选项。

内核编译完成后,使用scp命令进行将相关文件拷贝到target机上(当然也可以使用其它的网络工具,如rcp)。

[root@lisltmp]@192.168.6.13:/boot/

如果系统启动使所需要的某些设备驱动没有编译进内核的情况下,那么还需要执行如下操作:

[root@lisltmp]@192.168.6.13:/boot/

kgdb的启动

在将编译出的内核拷贝到target机器之后,需要配置系统引导程序,加入内核的启动选项。以下是kgdb内核引导参数的说明:
如表中所述,在版本之后内核的引导参数已经与以前的版本有所不同。使用grub引导程序时,直接将kgdb参数作为内核vmlinuz的引导参数。下面给出引导器的配置示例。

(hd0,0)kernel/boot/=/dev/hda1kgdbwaitkgdb8250=1,115200

在使用lilo作为引导程序时,需要把kgdb参放在由app修饰的语句中。下面给出使用lilo作为引导器时的配置示例。

image=/boot/=kgdbread-onlyroot=/dev/hda3app="gdbgdbttyS=1gdbbaud=115200"

保存好以上配置后重新启动计算机,选择启动带调试信息的内核,内核将在短暂的运行后在创建init内核线程之前停下来,打印出以下信息,并等待开发机的连接。

Waitingforconnectionfromremotegdb

在开发机上执行:

gdbfilevmlinuxsetremotebaud115200targetremote/dev/ttyS0

其中vmlinux是指向源代码目录下编译出来的Linux内核文件的链接,它是没有经过压缩的内核文件,gdb程序从该文件中得到各种符号地址信息。

这样,就与目标机上的kgdb调试接口建立了联系。一旦建立联接之后,对Linux内的调试工作与对普通的运用程序的调试就没有什么区别了。任何时候都可以通过键入ctrl+c打断目标机的执行,进行具体的调试工作。
在之前的版本中,编译内核后在arch/i386/kernel目录下还会生成可执行文件gdbstart。将该文件拷贝到target机器的/boot目录下,此时无需更改内核的启动配置文件,直接使用命令:

[root@lislboot]

查看模块加载信息文件modaddr如下:

.this00000060c88d80002**2.text00000035c88d80602**2.rodata00000069c88d80a02**5…….data00000000c88d833c2**2.bss00000000c88d833c2**2……

在这些信息中,我们关心的只有4个段的地址:.text、.rodata、.data、.bss。在development机上将以上地址信息加入到gdb中,这样就可以进行模块功能的测试了。

(gdb)

这种方法也存在一定的不足,它不能调试模块初始化的代码,因为此时模块初始化代码已经执行过了。而如果不执行模块的加载又无法获得模块插入地址,更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。
在target机上用上述方法得到模块加载的地址信息,然后再用rmmod卸载模块。在development机上将得到的模块地址信息导入到gdb环境中,在内核代码的调用初始化代码之前设置断点。这样,在target机上再次插入模块时,代码将在执行模块初始化之前停下来,这样就可以使用gdb命令调试模块初始化代码了。
另外一种调试模块初始化函数的方法是:当插入内核模块时,内核模块机制将调用函数sys_init_module(kernel/)执行对内核模块的初始化,该函数将调用所插入模块的初始化函数。程序代码片断如下:

…………if(mod-init!=NULL)ret=mod-init();…………

在该语句上设置断点,也能在执行模块初始化之前停下来。

在内核中的内核模块调试方法

之后的内核中,由于module-init-tools工具的更改,insmod命令不再支持-m参数,只有采取其他的方法来获取模块加载到内核的地址。通过分析ELF文件格式,我们知道程序中各段的意义如下:
.text(代码段):用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。
.data(数据段):数据段用来存放可执行文件中已初始化全局变量,也就是存放程序静态分配的变量和全局变量。
.bss(BSS段):BSS段包含了程序中未初始化全局变量,在内存中bss段全部置零。
.rodata(只读段):该段保存着只读数据,在进程映象中构造不可写的段。
通过在模块初始化函数中放置一下代码,我们可以很容易地获得模块加载到内存中的地址。

……intbss_var;staticinthello_init(void){printk(KERN_ALERT"(CodeSegment):%p",hello_init);staticintdata_var=0;printk(KERN_ALERT"(DataSegment):%p",data_var);printk(KERN_ALERT"BSSLocation:.bss(BSSSegment):%p",bss_var);……}Module_init(hello_init);

这里,通过在模块的初始化函数中添加一段简单的程序,使模块在加载时打印出在内核中的加载地址。.rodata段的地址可以通过执行命令,取得.rodata在文件中的偏移量并加上段的align值得出。

为了使读者能够更好地进行模块的调试,kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似。
硬件断点

kgdb提供对硬件调试寄存器的支持。在kgdb中可以设置三种硬件断点:执行断点(ExecutionBreakpoint)、写断点(WriteBreakpoint)、访问断点(AccessBreakpoint)但不支持I/O访问的断点。目前,kgdb对硬件断点的支持是通过宏来实现的,最多可以设置4个硬件断点,这些断点的用法如下:
在有些情况下,硬件断点的使用对于内核的调试是非常方便的。

在VMware中搭建调试环境,kgdb调试环境需要使用两台微机分别充当development机和target机,使用VMware后我们只使用一台计算机就可以顺利完成kgdb调试环境的搭建。

以windows下的环境为例,创建两台虚拟机,一台作为开发机,一台作为目标机。
虚拟机之间的串口连接

虚拟机中的串口连接可以采用两种方法。一种是指定虚拟机的串口连接到实际的COM上,例如开发机连接到COM1,目标机连接到COM2,然后把两个串口通过串口线相连接。另一种更为简便的方法是:在较高一些版本的VMware中都支持把串口映射到命名管道,把两个虚拟机的串口映射到同一个命名管道。例如,在两个虚拟机中都选定同一个命名管道\.pipecom_1,指定target机的COM口为server端,并选择"Theotherisavirtualmachine"属性;指定development机的COM口端为client端,同样指定COM口的"Theotherisavirtualmachine"属性。

对于IOmode属性,在target上选中"YieldCPUonpoll"复选择框,development机不选。这样,可以无需附加任何硬件,利用虚拟机就可以搭建kgdb调试环境。即降低了使用kgdb进行调试的硬件要求,也简化了建立调试环境的过程。

VMware的使用技巧

VMware虚拟机是比较占用资源的,尤其是象上面那样在Windows中使用两台虚拟机。因此,最好为系统配备512M以上的内存,每台虚拟机至少分配128M的内存。这样的硬件要求,对目前主流配置的PC而言并不是过高的要求。

出于系统性能的考虑,在VMware中尽量使用字符界面进行调试工作。同时,Linux系统默认情况下开启了sshd服务,建议使用SecureCRT登陆到Linux进行操作,这样可以有较好的用户使用界面。
在Linux下的虚拟机中使用kgdb

对于在Linux下面使用VMware虚拟机的情况,笔者没有做过实际的探索。从原理上而言,只需要在Linux下只要创建一台虚拟机作为target机,开发机的工作可以在实际的Linux环境中进行,搭建调试环境的过程与上面所述的过程类似。由于只需要创建一台虚拟机,所以使用Linux下的虚拟机搭建kgdb调试环境对系统性能的要求较低。(vmware已经推出了Linux下的版本)还可以在development机上配合使用一些其他的调试工具,例如功能更强大的cgdb、图形界面的DDD调试器等,以方便内核的调试工作。

kgdb的一些特点和不足

使用kgdb作为内核调试环境最大的不足在于对kgdb硬件环境的要求较高,必须使用两台计算机分别作为target和development机。尽管使用虚拟机的方法可以只用一台PC即能搭建调试环境,但是对系统其他方面的性能也提出了一定的要求,同时也增加了搭建调试环境时复杂程度。

另外,kgdb内核的编译、配置也比较复杂,需要一定的技巧,笔者当时做的时候也是费了很多周折。当调试过程结束后时,还需要重新制作所要发布的内核。使用kgdb并不能进行全程调试,也就是说kgdb并不能用于调试系统一开始的初始化引导过程。


不过,kgdb是一个不错的内核调试工具,使用它可以进行对内核的全面调试,甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下,对内核的调试将更方便。

使用SkyEye构建Linux内核调试环境

SkyEye是一个开源软件项目(OPenSourceSoftware),SkyEye项目的目标是在通用的Linux和Windows平台上模拟常见的嵌入式计算机系统。SkyEye实现了一个指令级的硬件模拟平台,可以模拟多种嵌入式开发板,支持多种CPU指令集。SkyEye的核心是GNU的gdb项目,它把gdb和ARMSimulator很好地结合在了一起。加入ARMulator的功能之后,它就可以来仿真嵌入式开发板,在它上面不仅可以调试硬件驱动,还可以调试操作系统。Skyeye项目目前已经在嵌入式系统开发领域得到了很大的推广。

SkyEye的安装和μcLinux内核编译

SkyEye的安装

SkyEye的安装不是本文要介绍的重点,目前已经有大量的资料对此进行了介绍。有关SkyEye的安装与使用的内容请查阅参考资料[11]。由于skyeye面目主要用于嵌入式系统领域,所以在skyeye上经常使用的是μcLinux系统,当然使用Linux作为skyeye上运行的系统也是可以的。由于介绍μ在skyeye上编译的相关资料并不多,所以下面进行详细介绍。
μ的编译

要在SkyEye中调试操作系统内核,首先必须使被调试内核能在SkyEye所模拟的开发板上正确运行。因此,正确编译待调试操作系统内核并配置SkyEye是进行内核调试的第一步。下面我们以SkyEye模拟基于AtmelAT91X40的开发板,并运行μ为例介绍SkyEye的具体调试方法。
安装交叉编译环境

先安装交叉编译器。尽管在一些资料中说明使用工具链,但是由于arm-elf-xxx与arm-linux-xxx对宏及链接处理的不同,经验证明使用arm-elf-xxx工具链在链接vmlinux的最后阶段将会出错。所以这里我们使用的交叉编译工具链是:,关于该交叉编译工具链的下载地址请参见[6]。注意以下步骤最好用root用户来执行。

[root@lisltmp]./

安装交叉编译工具链之后,请确保工具链安装路径存在于系统PATH变量中。

制作μcLinux内核

得到μcLinux发布包的一个最容易的方法是直接访问站点[7]。该站点发布的内核版本可能不是最新的,但你能找到一个最新的μcLinux补丁以及找一个对应的Linux内核版本来制作一个最新的μcLinux内核。这里,将使用这种方法来制作最新的μcLinux内核。目前(笔者记录编写此文章时),所能得到的发布包的最新版本是。

下载,文件的下载地址请参见[7]。
下载,文件的下载地址请参见[8]。
下载,文件的下载地址请参见[9]。
现在我们得到了整个的源代码,以及所需的内核补丁。请准备一个有2GB空间的目录里来完成以下制作μcLinux内核的过程。

[root@lisltmp][root@lisluClinux-dist][root@lisluClinux-dist]

执行以上过程后,将在/arch目录下生成一个补丁目录-armnommu。删除原来μcLinux目录里的(即那个),并将我们打好补丁的Linux内核目录更名为。

[root@lisluClinux-dist]

配置和编译μcLinux内核

因为只是出于调试μcLinux内核的目的,这里没有生成uClibc库文件及rom文件。在发布μcLinux时,已经预置了某些常用嵌入式开发板的配置文件,因此这里直接使用这些配置文件,过程如下:

[root@lisluClinux-dist]makeARCH=armnommuCROSS_COMPILE=arm-uclinux-atmel_deconfig

atmel_deconfig文件是μcLinux发布时提供的一个配置文件,存放于目录/arch/armnommu/configs/中。

[root@]makeARCH=armnommuCROSS_COMPILE=arm-uclinux-v=1

一般情况下,编译将顺利结束并在/目录下生成未经压缩的μcLinux内核文件vmlinux。需要注意的是为了调试μcLinux内核,需要打开内核编译的调试选项-g,使编译后的内核带有调试信息。打开编译选项的方法可以选择:
"Kerneldebugging-Compilethekernelwithdebuginfo"后将自动打开调试选项。也可以直接修改目录下的Makefile文件,为其打开调试开关。方法如下:。

CFLAGS+=-g

最容易出现的问题是找不到arm-uclinux-gcc命令的错误,主要原因是PATH变量中没有包含arm-uclinux-gcc命令所在目录。在arm-linux-gcc的缺省安装情况下,它的安装目录是/root/bin/arm-linux-tool/,使用以下命令将路径加到PATH环境变量中。

ExportPATH=$PATH:/root/bin/arm-linux-tool/bin

文件系统的制作

Linux内核在启动的时的最后操作之一是加载根文件系统。根文件系统中存放了嵌入式系统使用的所有应用程序、文件及其他一些需要用到的服务。出于文章篇幅的考虑,这里不打算介绍文件系统的制作方法,读者可以查阅一些其他的相关资料。值得注意的是,由配置文件指定了装载到内核中的跟踪文件系统。

使用SkyEye调试

编译完μcLinux内核后,就可以在SkyEye中调试该ELF执行文件格式的内核了。前面已经说过利用SkyEye调试内核与使用gdb调试运用程序的方法相同。
需要提醒读者的是,SkyEye的配置文件-记录了模拟的硬件配置和模拟执行行为。该配置文件是SkyEye系统中一个及其重要的文件,很多错误和异常情况的发生都和该文件有关。在安装配置SkyEye出错时,请首先检查该配置文件然后再进行其他的工作。此时,所有的准备工作已经完成,就可以进行内核的调试工作了。

使用SkyEye调试内核的特点和不足

在SkyEye中可以进行对Linux系统内核的全程调试。由于SkyEye目前主要支持基于ARM内核的CPU,因此一般而言需要使用交叉编译工具编译待调试的Linux系统内核。另外,制作SkyEye中使用的内核编译、配置过程比较复杂、繁琐。不过,当调试过程结束后无需重新制作所要发布的内核。
SkyEye只是对系统硬件进行了一定程度上的模拟,所以在SkyEye与真实硬件环境相比较而言还是有一定的差距,这对一些与硬件紧密相关的调试可能会有一定的影响,例如驱动程序的调试。不过对于大部分软件的调试,SkyEye已经提供了精度足够的模拟了。
SkyEye的下一个目标是和eclipse结合,有了图形界面,能为调试和查看源码提供一些方便。

KDB

Linux内核调试器(KDB)允许您调试Linux内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。KDB的主要优点之一就是它不需要用另一台机器进行调试:您可以调试正在运行的内核。

设置一台用于KDB的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB的用户应当熟悉Linux内核的编译(在一定程度上还要熟悉内核的内部机理)。
在本文中,我们将从有关下载KDB补丁、打补丁、(重新)编译内核以及启动KDB方面的信息着手。然后我们将了解KDB命令并研究一些较常用的命令。最后,我们将研究一下有关设置和显示选项方面的一些详细信息。

入门

KDB项目是由SiliconGraphics维护的,您需要从它的FTP站点下载与内核版本有关的补丁。(在编写本文时)可用的最新KDB版本是4.2。您将需要下载并应用两个补丁。一个是“公共的”补丁,包含了对通用内核代码的更改,另一个是特定于体系结构的补丁。补丁可作为bz2文件获取。例如,在运行2.4.20内核的x86机器上,您会需要和。
这里所提供的所有示例都是针对i386体系结构和2.4.20内核的。您将需要根据您的机器和内核版本进行适当的更改。您还需要拥有root许可权以执行这些操作。
将文件复制到/usr/src/linux目录中并从用bzip2压缩的文件解压缩补丁文件:

您将获得和文件。
现在,应用这些补丁:

这些补丁应该干净利落地加以应用。查找任何以.rej结尾的文件。这个扩展名表明这些是失败的补丁。如果内核树没问题,那么补丁的应用就不会有任何问题。

接下来,需要构建内核以支持KDB。第一步是设置CONFIG_KDB选项。使用您喜欢的配置机制(xconfig和menuconfig等)来完成这一步。转到结尾处的“Kernelhacking”部分并选择“Built-inKernelDebuggersupport”选项。

您还可以根据自己的偏好选择其它两个选项。选择“Compilethekernelwithframepointers”选项(如果有的话)则设置CONFIG_FRAME_POINTER标志。这将产生更好的堆栈回溯,因为帧指针寄存器被用作帧指针而不是通用寄存器。您还可以选择“KDBoffbydefault”选项。这将设置CONFIG_KDB_OFF标志,并且在缺省的情况下将关闭KDB。我们将在后面一节中对此进行详细介绍。

保存配置,然后退出。重新编译内核。建议在构建内核之前执行“makeclean”。用常用的方式安装内核并引导它。

初始化并设置环境变量

您可以定义将在KDB初始化期间执行的KDB命令。需要在纯文本文件kdb_cmds中定义这些命令,该文件位于Linux源代码树(当然是在打了补丁之后)的KDB目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您更改了文件之后需要重新构建并重新安装内核。

激活KDB

如果编译期间没有选中CONFIG_KDB_OFF,那么在缺省情况下KDB是活动的。否则,您需要显式地激活它-通过在引导期间将kdb=on标志传递给内核或者通过在挂装了/proc之后执行该工作:

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]kdbmd0xc00000015

将内存位置为0xc000000上的内容更改为0x10:

[0]kdbmm0xc0000000x10


寄存器显示和修改

这一类别中的命令有rd、rm和ef。rd命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了c参数,则rd显示处理器的控制寄存器;如果带有d参数,那么它就显示调试寄存器;如果带有u参数,则显示上一次进入内核的当前任务的寄存器组。

rm命令修改寄存器的内容。它以寄存器名称和new-contents作为参数,用new-contents修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。
ef命令以一个地址作为参数,它显示指定地址处的异常帧。
示例

显示通用寄存器组:[0]kdbrd[0]kdbrm%ebx0x25

断点

常用的断点命令有bp、bc、bd、be和bl。
bp命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予KDB。该命令有几个有用的变体。bpa命令对SMP系统中的所有处理器应用断点。bph命令强制在支持硬件寄存器的系统上使用它。bpha命令类似于bpa命令,差别在于它强制使用硬件寄存器。
bd命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从0开始,根据可用性顺序分配给断点。
be命令启用断点。该命令的参数也是断点号。
bl命令列出当前的断点集。它包含了启用的和禁用的断点。
bc命令从断点表中除去断点。它以具体的断点号或*作为参数,在后一种情况下它将除去所有断点。
示例

对函数sys_write()设置断点:

[0]kdbbpsys_write

列出断点表中的所有断点:

[0]kdbbl

清除断点号1:

[0]kdbbc1


堆栈跟踪

主要的堆栈跟踪命令有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:不可运行

这类命令中的每一个都会打印出一大堆信息。

示例


其它命令

下面是在内核调试过程中非常有用的其它几个东西KDB命令。
id命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量IDCOUNT确定要显示多少行输出。

ss命令单步执行指令然后将控制返回给KDB。该指令的一个变体是ssb,它执行从当前指令指针地址开始的指令(在屏幕上打印指令),直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有call、return和jump。

go命令让系统继续正常执行。一直执行到遇到断点为止(如果已经应用了一个断点的话)。

reboot命令立刻重新引导系统。它并没有彻底关闭系统,因此结果是不可预测的。
ll命令以地址、偏移量和另一个KDB命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。
示例

反汇编从例程schedule开始的指令。所显示的行数取决于环境变量IDCOUNT:

[0]kdbidschedule

执行指令直到它遇到分支转移条件(在本例中为指令jne)为止:

[0]kdbssb0xc0105355default_idle+0x25:cli0xc0105356default_idle+0x26:mov0x14(%edx),%eax0xc0105359default_idle+0x29:test%eax,%eax0xc010535bdefault_idle+0x2b:jne0xc0105361default_idle+0x31

技巧和诀窍

调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理NULL指针错误地引用,却没有查出无效引用的真正原因。

结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。
调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试器获取必要的信息,然后继续分析代码以识别起因。
因此,这里为您介绍了一些技巧,以便您能使用KDB快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核的内部机理等)。
技巧2

在编译带KDB的内核时,只要CONFIG_FRAME_POINTER选项出现就使用该选项。为此,需要在配置内核时选择“Kernelhacking”部分下面的“Compilethekernelwithframepointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在i386机器上,%ebp寄存器可以用来回溯整个堆栈。

例如,在函数rmqueue()上次执行第一个指令后,堆栈看上去类似于下面这样:

[0]kdbmd%ebp0xc74c9f38c74c9f60c0136c40000001f0000000000xc74c9f4808053328c0425238c04253axc74c9f58000001f000000246c74c9f6cc0136a250xc74c9f68c74c8000c74c9f74c0136d6dc74c9fbc0xc74c9f78c014fe45c74c0008053328[0]kdb0xc0136c400xc0136c40=0xc0136c40(__alloc_pages+0x44)[0]kdb0xc0136a250xc0136a25=0xc0136a25(_alloc_pages+0x19)[0]kdb0xc0136d6d0xc0136d6d=0xc0136d6d(__get_free_pages+0xd)

我们可以看到rmqueue()被__alloc_pages调用,后者接下来又被动_alloc_pages调用,以此类推。

每一帧的第一个双字(doubleword)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。
技巧4

您可以利用一个名为defcmd的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。defcmd允许您定义自己的命令,该命令可以包含一个或多个预定义的KDB命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:

[0]kdbdefcmdname"usage""help"[0]kdb[defcmd]typethecommandshere[0]kdb[defcmd]efcmd

例如,可以定义一个(简单的)新命令hari,它显示从地址0xc000000开始的一行内存、显示寄存器的内容并转储堆栈:

[0]kdbdefcmdhari"""noargumentsneeded"[0]kdb[defcmd]md0xc0000001[0]kdb[defcmd]rd[0]kdb[defcmd]md%ebp1[0]kdb[defcmd]efcmd

该命令的输出会是:

[0]kdbhari[hari]kdbmd0xc00000010xc00000000000001f000e816f000e2c3f000e816[hari]kdbrdeax=0x00000000ebx=0xc0105330ecx=0xc0466000edx=0xc0466000.[hari]kdbmd%ebp10xc0467fbcc0467fd0c01053d200000002000a0200[0]kdb


技巧#5

可以使用bph和bpha命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。
示例
每当将四个字节写入地址0xc0204060时间就进入内核调试器:

[0]kdbbph0xc0204060dataw4

在读取从0xc000000开始的至少两个字节的数据时进入内核调试器:

[0]kdbbph0xc000000datar2

结束语

对于执行内核调试,KDB是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。

Kprobes

Kprobes是Linux中的一个简单的轻量级装置,让您可以将断点插入到正在运行的内核之中。Kprobes提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口。使用Kprobes可以轻松地收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用Kprobes来修改寄存器值和全局数据结构的值。
为完成这一任务,Kprobes向运行的内核中给定地址写入断点指令,插入一个探测器。执行被探测的指令会导致断点错误。Kprobes钩住(hookin)断点处理器并收集调试信息。Kprobes甚至可以单步执行被探测的指令。

1安装

要安装Kprobes,需要从Kprobes主页下载最新的补丁。打包的文件名称类似于。解开补丁并将其安装到Linux内核:

$$cd/usr/src/$patch-p1../

Kprobes利用了SysRq键,这个DOS时代的产物在Linux中有了新的用武之地。您可以在ScrollLock键左边找到SysRq键;它通常标识为PrintScreen。要为Kprobes启用SysRq键,需要安装补丁:

$patch-p1../

使用makexconfig/makemenuconfig/makeoldconfig配置内核,并启用CONFIG_KPROBES和CONFIG_MAGIC_SYSRQ标记。编译并引导到新内核。您现在就已经准备就绪,可以插入printk并通过编写简单的Kprobes模块来动态而且无干扰地收集调试信息。

2编写Kprobes模块

对于每一个探测器,您都要分配一个结构体structkprobekp;(参考include/linux/以获得关于此数据结构的详细信息)。
清单9.定义pre、post和fault处理器

/*pre_handler:thisiscalledjustbeforetheprobedinstructionis*executed.*/inthandler_pre(structkprobe*p,structpt_regs*regs){printk("pre_handler:p-addr=0x%p,eflags=0x%lx",p-addr,regs-eflags);return0;}/*post_handler:thisiscalledaftertheprobedinstructionisexecuted*(providednoexceptionisgenerated).*/voidhandler_post(structkprobe*p,structpt_regs*regs,unsignedlongflags){printk("post_handler:p-addr=0x%p,eflags=0x%lx",p-addr,regs-eflags);}/*fault_handler:thisiscalledifanexceptionisgeneratedforany*instructionwithinthefault-handler,orwhenKprobes*single-stepstheprobedinstruction.*/inthandler_fault(structkprobe*p,structpt_regs*regs,inttrapnr){printk("fault_handler:p-addr=0x%p,eflags=0x%lx",p-addr,regs-eflags);return0;}


2.1获得内核例程的地址
在注册过程中,您还需要指定插入探测器的内核例程的地址。使用这些方法中的任意一个来获得内核例程的地址:

从文件直接得到地址。

例如,要得到do_fork的地址,可以在命令行执行$grepdo_fork/usr/src/linux/。

使用nm命令。

$nmvmlinuz|grepdo_fork

从/proc/kallsyms文件获得地址。

$cat/proc/kallsyms|grepdo_fork

使用kallsyms_lookup_name()例程。

这个例程是在kernel/文件中定义的,要使用它,必须启用CONFIG_KALLSYMS编译内核。kallsyms_lookup_name()接受一个字符串格式内核例程名,返回那个内核例程的地址。例如:kallsyms_lookup_name("do_fork");

然后在init_moudle中注册您的探测器:

清单10.注册一个探测器

/*specifypre_handleraddress*/_handler=handler_pre;/*specifypost_handleraddress*/_handler=handler_post;/*specifyfault_handleraddress*/_handler=handler_fault;/*specifytheaddress/offsetwhereyouwanttoinsertprobe.*Youcangettheaddressusingoneofthemethodsdescribedabove.*/=(kprobe_opcode_t*)kallsyms_lookup_name("do_fork");/*checkifthekallsyms_lookup_name()returnedthecorrectvalue.*/if(==NULL){printk("kallsyms_lookup_namecouldnotfindaddressforthespecifiedsymbolname");return1;}/*orspecifyaddressdirectly.*$grep"do_fork"/usr/src/linux/*or*$cat/proc/kallsyms|grepdo_fork*or*$nmvmlinuz|grepdo_fork*/=(kprobe_opcode_t*)0xc01441d0;/*AllsettoregisterwithKprobes*/register_kprobe(kp);

一旦注册了探测器,运行任何shell命令都会导致一个对do_fork的调用,您将可以在控制台上或者运行dmesg命令来查看您的printk。做完后要记得注销探测器:
unregister_kprobe(kp);
下面的输出显示了kprobe的地址以及eflags寄存器的内容:

$tail-5/var/log/messagesJun1418:21:18llm05kernel:pre_handler:p-addr=0xc01441d0,eflags=0x202Jun1418:21:18llm05kernel:post_handler:p-addr=0xc01441d0,eflags=0x196


获得偏移量

您可以在例程的开头或者函数中的任意偏移位置插入printk(偏移量必须在指令范围之内)。下面的代码示例展示了如何来计算偏移量。首先,从对象文件中反汇编机器指令,并将它们保存为一个文件:

$objdump-D/usr/src/linux/kernel/

其结果是:
清单11.反汇编的fork

000022b0:22b0:55push%ebp22b1:89e5mov%esp,%ebp22b3:57push%edi22b4:89c7mov%eax,%edi22b6:56push%esi22b7:89d6mov%edx,%esi22b9:53push%ebx22ba:83ec38sub$0x38,%esp22bd:c745d000000000movl$0x0,0xffffffd0(%ebp)22c4:89cbmov%ecx,%ebx22c6:89442404mov%eax,0x4(%esp)22ca:c704240a000000movl$0xa,(%esp)22d1:e8fcffffffcall22d20x2222d6:b800e0ffffmov$0xffffe000,%eax22db:21e0and%esp,%eax22dd:8b00mov(%eax),%eax

要在偏移位置0x22c4插入探测器,先要得到与例程的开始处相对的偏移量0x22c4-0x22b0=0x14,然后将这个偏移量添加到do_fork的地址0xc01441d0+0x14。(运行$cat/proc/kallsyms|grepdo_fork命令以获得do_fork的地址。)
您还可以将do_fork的相对偏移量0x22c4-0x22b0=0x14添加到kallsyms_lookup_name("do_fork");的输入,即:0x14+kallsyms_lookup_name("do_fork");
转储内核数据结构

现在,让我们使用修改过的用来转换数据结构的Kprobepost_handler来转储运行在系统上的所有作业的一些组成部分:
清单12.用来转储数据结构的修改过的Kpropepost_handler

voidhandler_post(structkprobe*p,structpt_regs*regs,unsignedlongflags){structtask_struct*task;read_lock(tasklist_lock);for_each_process(task){printk("pid=%xtask-info_ptr=%lx",task-pid,task-thread_info);printk("thread-infoelementstatus=%lx,flags=%lx,cpu=%lx",task-thread_info-status,task-thread_info-flags,task-thread_info-cpu);}read_unlock(tasklist_lock);}

这个模块应该插入到do_fork的偏移位置。
清单13.pid1508和1509的结构体thread_info的输出

$tail-10/var/log/messagesJun2218:14:25llm05kernel:thread-infoelementstatus=0,flags=0,cpu=1Jun2218:14:25llm05kernel:pid=5e4task-info_ptr=f5948000Jun2218:14:25llm05kernel:thread-infoelementstatus=0,flags=8,cpu=0Jun2218:14:25llm05kernel:pid=5e5task-info_ptr=f5eca000

启用奇妙的SysRq键

为了支持SysRq键,我们已经进行了编译。这样来启用它:

$echo1/proc/sys/kernel/sysrq

现在,您可以使用Alt+SysRq+W在控制台上或者到/var/log/messages中去查看所有插入的内核探测器。
清单14./var/log/messages显示出在do_fork插入了一个Kprobe

Jun2310:24:48linux-udp4749545udskernel:SysRq:ShowkprobesJun2310:24:48linux-udp4749545udskernel:Jun2310:24:48linux-udp4749545udskernel:[]do_fork+0x0/0x1de

使用Kprobes更好地进行调试

由于探测器事件处理器是作为系统断点中断处理器的扩展来运行,所以它们很少或者根本不依赖于系统工具——这样可以被植入到大部分不友好的环境中(从中断时间和任务时间到禁用的上下文间切换和支持SMP的代码路径)——都不会对系统性能带来什么负面影响。

使用Kprobes的好处有很多。不需要重新编译和重新引导内核就可以插入printk。为了进行调试可以记录处理器寄存器的日志,甚至进行修改——不会干扰系统。类似地,同样可以无干扰地记录Linux内核数据结构的日志,甚至是进行修改。您甚至可以使用Kprobes调试SMP系统上的静态条件——避免了您自己重新编译和重新引导的所有麻烦。您将发现内核调试比以往更为快速和简单。