1:前言:
最近几天学习Linux-2.6平台上的设备驱动,所以要建立内核及内核模块的调试平台.虽然网上有很多相关教程,但多是基于2.6.26以前的通过补丁安装的,过程非常复杂,而且问题比较多.linux从 2.6.26开始已经集成了kgdb,只需要重新编译2.6.26(或更高)内核即可.kgdb安装及模块调试过程也遇到不少问题,网上网下不断的搜索与探索,才算调通.现在记录下来,供朋友们参考.
首先说一下,开始本打算安装kdb进行内核调试,后来听说kdb只能进行汇编级别的调试,所以放弃,改用kgdb.
2: 系统环境:
虚拟环境: VMWare Workstation 5.5(英文版)
操作系统: CentOS-4.6-i386(原内核2.6.9,将会把内核升级至2.6.26)
注:CentOS 是RedHat的一个社区版本.
(由于我们采用的linux kernel 2.6.26已经集成kgdb,kgdb再不需要单独下载)
3:系统的安装:
在VMWare中新建一台计算机:
- 点击 Next
- 选中Custom 点击 Next
- 选中 New-Workstation 5,点击Next
- 选中Linux ,Version选中Other Linux 2.6.x kernel 点击Next
- Virtual machine name 输入Client(Development) 点击Next
- Processors 选中 One, 点击Next
- Memory 输入256,点击Next
- Network connection 选中Use network address translation(NAT) (选第一个貌似也可以) 点击Next
- I/O adapter types
- SCSI Adapters 选中默认的LSI Logic(这里如果你后面使用了SCSI格式的Disk,编译内核时需要添加相应的驱动,我选择的是IDE的硬盘,kernel默认就支持了)
- Disk 选中Create a new virtual disk 点击Next
- Virtual Disk Type 选中IDE,点击Next
- Disk capacity Disk size 输入80G (下面的Allocate all disk space now不要选中,表示在真正使用才分配磁盘空间, Split disk into 2 GB files项,可不选,如果你的系统分区为fat32格式,最好选中) 点击Next.
- Disk file ,输入Disk的名称,如:disk1.vmdk ,点击Finish.完成
安装CentOS 4.6(过程略过)
安装完成后,关闭计算机,然后Clone一台同样的计算机.步骤如下:
- 点击VM->Clone
- 选中默认的From current state,点击Next
- 选中Create a full clone, 点击Next
- Virtual Machine name 输入Server(Targe),将克隆的机器命令为目标机.
说明一下,kgdb 需要两台计算机通过串口进行远程调试,两台计算机分别为:
- Client(Development):开发机,也称客户机,将在该计算机上进行程序的开发,GDB将在本计算机上运行.用于输入命令控制Server(target)的运行.
- Server(Target): 目标机,也称服务器,就是被调试的计算机,在Development机上开发好的内核模块程序将拷贝到Target上运行,其运行受到Development命令的控制.
分别为两个系统增加一个串口,以"
Output to named pipe
"方式,其中:
- Client端选择"this end is the client", "the other end is a virtual machine"
- Server端选择"this end is the server", "the other end is a virtual machine"
- 备注: 两个pipe的名称要相同,并且选中下面的Connect at power on,及Advanced里面的Yield CPU on poll
以后的部分,Server上的操作与Client上的操作将以不同的背景色显示,输入的命令将以不同的字体颜色并带下划线显示.请注意:
- Server(Target) 输入: cat /dev/ttyS0
系统输出的信息: hello
- Client(Development) 输入: echo "hello" >/dev/ttuS0
串口添加完成后,使用如果命令测试:
- 在Server上cat /dev/ttyS0
- 然后到Client上 echo "hello" > /dev/ttyS0
- 这时回到Server上,如果能看到输入的hello,说明串口通讯正常.
4:升级内核2.6.26(添加关于KGDB的选项)
说明一下,这里是要升级Server的内核,因为kgdb是要Server上运行的,但是编译需要在Client完成(或者你也可以在Server上编译,之后再拷贝到Client上),因为调试时Client上的gdb会用到编译的内核及源代码.Client也需要升级,保证Client同Server上的内核一致,这样Client上编译的模块拿到Server上加载就不会有什么问题.
首先下载kernel 2.6.26
我习惯在windows上下载,然后共享,再到linux使用smbclient连接,拷贝到Linux上.你也可以直接在linux上下载.
smbclient用法 : smbclient //192.168.0.100/share -Uadministrator 回车后,会提示输入admin的密码.之后就可以通过get获取了 192.168.0.100是我在windows主机的IP,share为共享名,-U后面是用户名
编译Kernel2.6.26
进入Client(Development)系统,将linux-2.6.26.tar.bz2拷贝到/usr/src目录下:
- cd /usr/src
- tar jxvf linux-2.6.26.tar.bz2
- ln -s linux-2.6.26 linux
- cd linux
-
make menuconfig
- File System --> 下面把ext3,ext2都编译进内核(就是把前面的M变成*)
- Kernel Hacking -->
选中Compile the kernel with frame pointers
选中KGDB:kernel debugging with remote gdb
并确认以下两项也是选中的(他们应该默认是选中的)
> kernel debugging
> Compile the kernel with debug info
- 对于其它选项,请按实际情况,或你的要求定制.
- 在其它网友的说明里面,会有Serial port number for KGDB等选项,但是我使用的版本未找到这些选项,所以忽略过.
- 保存退出
-
make -j10 bzImage
-j10表示使用10个线程进行编译.
-
make modules
编译内核模块
-
make modules_install
安装内核模块
-
make install
安装内核
将Client系统中的linux-2.6.26整个目录同步到Server上.
在Client系统上运行下列命令:
-
cd /usr/src/linux
-
scp -r linux-2.6.26 root@Server(Target)IP:/usr/src/
系统会提示输入root的密码,输入完了就会开始复制文件了,(这里要注意,如果原系统已经是2.6.26的内核,可以只拷贝arch/i386/boot/bzImage,及System.map文件到Server上,然后修改/boot/grub/grub.conf,但由于我是从2.6.9升级上来,所以需要将整个linux原代码目录拷贝到Server上进行升级)
升级Srever系统的内核
进入Server(Target)系统,usr/src目录:
-
ln -s linux-2.6.26 linux
-
cd linux
-
make modules_install
安装内核模块
-
make install
安装内核
安装完成后,在/boot/目录下会有即个新添加的文件./boot/grub/grub.conf文件也会添加一个新的启动项,我的如下(行号不是文件的一部分):
1 # grub.conf generated by anaconda
2 #
3 # Note that you do not have to rerun grub after making changes to this file
4 # NOTICE: You have a /boot partition. This means that
5 # all kernel and initrd paths are relative to /boot/, eg.
6 # root (hd0,0)
7 # kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
8 # initrd /initrd-version.img
9 #boot=/dev/hda
10 default=0
11 timeout=5
12 splashimage=(hd0,0)/grub/splash.xpm.gz
13 hiddenmenu
14 title CentOS (2.6.26)
15 root (hd0,0)
16 kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00
17 initrd /initrd-2.6.26.img
18 title CentOS-4 i386 (2.6.9-67.ELsmp)
19 root (hd0,0)
20 kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
21 initrd /initrd-2.6.9-67.ELsmp.img
|
注意里面的NOTICE说明,我的系统/boot是一个独立的分区,所以下面配置的文件路径都是相对于/boot/目录的,像 /vmlinuz-2.6.26,实际到它的绝对位置应该是/boot/vmlinuz-2.6.26. 在你的系统上请根据实际情况处理.
修改一下grub.conf,修改成下面这样:
1 # grub.conf generated by anaconda
2 #
3 # Note that you do not have to rerun grub after making changes to this file
4 # NOTICE: You have a /boot partition. This means that
5 # all kernel and initrd paths are relative to /boot/, eg.
6 # root (hd0,0)
7 # kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
8 # initrd /initrd-version.img
9 #boot=/dev/hda
10 default=0
11 timeout=5
12 splashimage=(hd0,0)/grub/splash.xpm.gz
13 hiddenmenu
14 title CentOS (2.6.26)
15 root (hd0,0)
16 kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200
17 initrd /initrd-2.6.26.img
18 title CentOS (2.6.26) Wait...(kernel debug)
19 root (hd0,0)
20 kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200 kgdbwait
21 initrd /initrd-2.6.26.img
22 title CentOS-4 i386 (2.6.9-67.ELsmp)
23 root (hd0,0)
24 kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
25 initrd /initrd-2.6.9-67.ELsmp.img
|
说明:
- 第一个启动项在原来的基础上添加了kgdb的参数kgdboc=ttyS0,115200
kgdboc 的意思是 kgdb over console,这里将kgdb连接的console设置为ttyS0,波特率为115200,如果不在内核启动项中配置该参数,可以在进入系统后执行命令:
echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
- 第二个启动项,同第一个使用同一个内核,只是添加了kgdbwait参数
kgdbwait 使 kernel 在启动过程中等待 gdb 的连接。
我的启动菜单如下:
- CentOS(2.6.26)
- CentOS(2.6.26)Wait...(kernel debug)
- CentOS-4 i386(2.6.9-67.ELsmp)
调用内核模块,就选择第一个,如果要调试内核启动过程,选择第二个.
5.内核调试
重启Server,通过启动菜单第二项CentOS(2.6.26)Wait...(kernel debug)进入系统. 只到系统出现:
kgdb: Registered I/O driver kgdboc
kgdb: Waiting for connection from remote gdb
进入Client系统.
-
cd /usr/src/linux
-
gdb vmlinux
启动gdb开始准备调试:输出大致如下:
GNU gdb Red Hat Linux (6.3.0.0-1.153.el4rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db
library "/lib/tls/libthread_db.so.1"
- (gdb) set remotebaud 115200
- (gdb) target remote /dev/ttyS0
注意:有的文章讲:因为vmware的named piped不能被gdb直接使用,需要使用 socat -d -d /tmp/com_1 /dev/ttyS0转换,然后使用转换后的设备. socat需要自己下载安装. 但在我的环境中,直接使用并没有出错.
执行该命令后输出如下: Remote debugging using /dev/ttyS0
kgdb_breakpoint () at kernel/kgdb.c:1674
1674 wmb(); /*Sync point after breakpoint */
warning: shared library handler failed to enable breakpoint
看到上面的内容说明已经连接成功,但Server上依然是假死状态,这时你可以像使用本地gdb一样设置断点(break),单步执行(step),或其它命令.
- (gdb) cont
继续执行,Server就继续下面的系统初始化了.
系统启动完成后的内核调试:
进入Server后,执行命令
-
echo g > /proc/sysrq-trigger
系统同样会中断,进入假死状态,等待远程gdb的连接.KGDB可能会输出如下信息: SysRq: GDB
上面的命令(echo g > /proc/sysrq-trigger)可以有一个快捷键(ALT-SysRq-G)代替,当然前提是你编译内核时需要选中相关选项,并且需要修改配置文件:/etc/sysctl.conf , 我用了一下,不太好用.因为有的桌面系统中PrintScreen/SysRq键是用于截屏的.所以还是直接用命令来的好!
我在~/.bashrc中添加了一句(添加完保存后,要执行source ~/.bashrc应用该配置):
alias debug='echo g > /proc/sysrq-trigger'
之后就可以直接输入debug来使内核进入调试状态.
Server进入调试状态后,转换到Client系统,重复上面的步骤.
6. Linux内核模块(设备驱动)的调试
编写内核模块,及Makefile
我使用的例子是Linux Device Driver 3中的例子scull. 你可以从这里下载LDD3的所有例子程序.
进入Server系统输入:
-
cd /root/
-
insmod scull.ko
加载scull模块.
-
cat /sys/module/globalmem/sections/.text
显示scull模块的.text段地址.运行该命令后,会返回一个16进制的地址,如: 0xd099a000
-
echo g > /proc/sysrq-trigger
现在Server系统变成等待状态.
再次进入Client系统:
-
cd /usr/src/linux
-
gdb vmlinux
- (gdb) set remotebaud 115200
- (gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
kgdb_breakpoint () at kernel/kgdb.c:1674
1674 wmb(); /*Sync point after breakpoint */
warning: shared library handler failed to enable breakpoint
出现上面的信息表示连接成功,但此时还不可以设置断点.我试了N次,现在设置端点后,无论Server上对scull做什么操作,Client上的gdb都不会停止.也有人说这是gdb的BUG,gdb无法正常解析内核模块的符号信息. 说是下载kgdb网站上的gdbmod可以解决该问题,但我试了之后发现根本没有用. 所以只能通过命令手动加载相关符号信息:
- (gdb) add-symbol-file /root/scull/scull.ko 0xd099a000
注意: 0xd099a000地址是在上面获取的,命令输入完了,系统会有如下提示信息(第二行后面的y是自己输入的: add symbol table from file "/root/scull/scull.ko" at .text_addr = 0xd099a000
(y or n)y
Reading symbols from /root/scull/scull.ko...done.
- (gdb)break scull_write
Breakpoint 1 at 0xd099a2d9: file /root/scull/main.c,line 338.
- (gdb)break scull_read
Breakpoint 2 at 0xd099a1a2: file /root/scull/main.c,line 294
- (gdb)cont
Continuing
Client上的工作暂停,回到Server系统上:
回到Client系统,应该能看到gdb的输出信息:
Breakpoint 1, scull_write (filp=0xce5870c0,buf=0xb7f44000
"this is a test/nias | /usr/bin/which --tty-only --read-alias
--show-dot --show-tilde'/n",count=15,f_pos=0xce5c5f9c)
at /root/scull/main.c:338
338 {
(gdb)_
现在就可以像调试本地程序一样调试scull.ko模块了.
以同样的方法也可以调试其它函数.(初始化函数暂时没想到办法调试,因为使用这种方法需要先加载后,才可以进行调试)
你可以*使用和转载本文档,转载时请注明出处. (jie123108@163.com)
如果可以,我想写一个脚本自动完成这个添加符号文件的过程,简化调试过程.
1 开启虚拟机,虚拟机运行到 kgdb: Waiting for connection from remote gdb
2. 在Host机上运行: socat tcp-listen:8888 /tmp/vbox2, 其中/tmp/vbox2为管道文件,它是目标机串口的重定向目的文件,socat将这个管道文件又重定向到tcp socket的8888端口。
3. 开启一个新的虚拟终端,cd path/to/kernel/source/tree, 然后执行gdb ./vmlinux
输出
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
(gdb) set-remote
set remote baud rate to 115200c/s
set remote target to local tcp socket
kgdb_breakpoint () at kernel/kgdb.c:1721
1721 wmb(); /* Sync point after breakpoint */
(gdb) c
Continuing.
目标机会一直启动,直到提示输入用户名密码。
4. 进入目标机,输入用户名密码(推荐使用字符界面下的root用户),输入g命令,目标机被断下,控制移交到Host机中的gdb中。(目标机root的用户目录中的.bashrc中添加一行alias g='echo g>/proc/sysrq-trigger')
5. 在Host机中的gdb中
(gdb) set-mod-break
set breakpoint in system module init function
Breakpoint 1 at 0xc014bac5: file kernel/module.c, line 2288.
(gdb) c
Continuing.
6. 在目标机中
insmod klogger2.ko
目标机再次断下,控制权移交Host机中的gdb
7. 在Host机中的gdb中
[New Thread 4693]
[Switching to Thread 4693]
Breakpoint 1, sys_init_module (umod=0x0, len=0, uargs=0x0)
at kernel/module.c:2288
2288 if (mod->init != NULL)
(gdb) print-mod-segment
Name:.note.gnu.build-id Address:0xdf977058
Name:.text Address:0xdf975000
Name:.rodata Address:0xdf977080
Name:.rodata.str1.4 Address:0xdf9774b4
Name:.rodata.str1.1 Address:0xdf977522
Name:.parainstructions Address:0xdf977a00
Name:.data Address:0xdf978440
Name:.gnu.linkonce.this_module Address:0xdf978480
Name:.bss Address:0xdf978a00
Name:.symtab Address:0xdf977a08
Name:.strtab Address:0xdf978078
(gdb) add-symbol-file /home/done/programs/linux-kernel/vlogger/klogger2.ko 0xdf975000 -s .data 0xdf978440 -s .bss 0xdf978a00
add symbol table from file "/home/done/programs/linux-kernel/vlogger/klogger2.ko" at
.text_addr = 0xdf975000
.data_addr = 0xdf978440
.bss_addr = 0xdf978a00
(y or n) y
Reading symbols from /home/done/programs/linux-kernel/vlogger/klogger2.ko...done.
(gdb) b hook_init
Breakpoint 2 at 0xdf976d19: file /home/done/programs/linux-kernel/vlogger/hook.c, line 255.
(gdb)
你可以调试自己些的LKM模块了
附gdb的初始化配置文件~/.gdbinit
define set-remote
echo set remote baud rate to 115200c/s\n
set remotebaud 115200
echo set remote target to local tcp socket\n
target remote tcp:localhost:8888
end
define set-mod-break
echo set breakpoint in system module init function\n
break kernel/module.c:2288
end
define print-mod-segment
set $sect_num=mod->sect_attrs->nsections
set $cur=0
while $cur < $sect_num
printf "Name:%-s Address:0x%x\n",mod->sect_attrs->attrs[$cur]->name,mod->sect_attrs->attrs[$cur]->address
set $cur=$cur+1
end
end
后记:gdb的调试脚本真难写,简单的字符串变量连接和等价判断都显得十分困难,不知道是我水平太差还是gdb的脚本功能太弱,总之比起Windbg来说,内核调试困难程度上了个等级。
使用kgdb调试linux内核及内核模块
创建时间:2005-09-09
文章属性:原创
文章提交:xcspy (xcspy.com_at_gmail.com)
作者:xcspy成员 ladybug
E-mail:xcspy.com@gmail.com
主页:www.xcspy.com
1. 几种内核调试工具比较
kdb:只能在汇编代码级进行调试;
优点是不需要两台机器进行调试。
gdb:在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括反汇编内核函数。
kgdb:能很方便的在源码级对内核进行调试,缺点是kgdb只能进行远程调试,它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试)
使用kdb和gdb调试内核的方法相对比较简单,这里只描述如何使用kgdb来调试内核。
2.软硬件准备
环境:
一台开发机developer(192.168.16.5 com1),一台测试机target(192.168.16.30 com2),都预装redhat 9;一根串口线
下载以下软件包:
linux内核2.4.23 linux-2.4.23.tar.bz2
kgdb内核补丁1.9版 linux-2.4.23-kgdb-1.9.patch
可调试内核模块的gdb gdbmod-1.9.bz2
3.ok,开始
3.1 测试串口线
物理连接好串口线后,使用一下命令进行测试,stty可以对串口参数进行设置
在developer上执行:
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
echo hello > /dev/ttyS0
在target上执行:
stty ispeed 115200 ospeed 115200 -F /dev/ttyS1
cat /dev/ttyS1
串口线没问题的话在target的屏幕上显示hello
3.2 安装与配置
3.2.1 安装
下载linux-2.4.23.tar.bz2,linux-2.4.23-kgdb-1.9.patch,gdbmod-1.9.bz2到developer的/home/liangjian目录
*在developer机器上
#cd /home/liangjian
#bunzip2 linux-2.4.23.tar.bz2
#tar -xvf linux-2.4.23.tar
#bunzip2 gdbmod-1.9.bz2
#cp gdbmod-1.9 /usr/local/bin
#cd linux-2.4.23
#patch -p1 < ../linux-2.4.23-kgdb-1.9.patch
#make menuconfig
在Kernel hacking配置项中将以下三项编译进内核
KGDB: Remote (serial) kernel debugging with gdb
KGDB: Thread analysis
KGDB: Console messages through gdb
注意在编译内核的时候需要加上-g选项
#make dep;make bzImage
使用scp进行将相关文件拷贝到target上(当然也可以使用其它的网络工具)
#scp arch/i386/boot/bzImage root@192.168.16.30:/boot/vmlinuz-2.4.23-kgdb
#scp System.map root@192.168.16.30:/boot/System.map-2.4.23-kgdb
#scp arch/i386/kernel/gdbstart root@192.168.16.30:/sbin
gdbstart为kgdb提供的一个工具,用于激活内核钩子,使内核处于调试状态
3.2.2 配置
*在developer机器上
在内核源码目录下编辑一文件.gdbinit(该文件用以对gdb进行初始化),内容如下:
#vi .gdbinit
define rmt
set remotebaud 115200
target remote /dev/ttyS0
end
#
以上在.gdbinit中定义了一个宏rmt,该宏主要是设置使用的串口号和速率
*在target机器上
编辑/etc/grub.conf文件,加入以下行:
#vi /etc/grub.conf
title Red Hat Linux (2.4.23-kgdb)
root (hd0,0)
kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1
#
在root目录下建立一个脚本文件debugkernel,内容如下:
#vi debug
#!/bin/bash
gdbstart -s 115200 -t /dev/ttyS1 <<EOF
EOF
#chmod +x debugkernel
这个脚本主要是调用gdbstart程序设置target机上使用的串口及其速率,并使内核处于调试状态
3.3 开始调试
target上的内核或内核模块处于调试状态时,可以查看其变量、设置断点、查看堆栈等,并且是源码级的调试,和用gdb调试用户程序一样
3.3.1 内核启动后调试
*在target机器上
重启系统,选择以 2.4.23-kgdb内核启动,启动完成后运行debugkenel,
这时内核将停止运行,在控制台屏幕上显示信息,并等待来自developer的
串口连接
#./debug
About to activate GDB stub in the kernel on /dev/ttyS1
Waiting for connection from remote gdb...
*在developer机器上
#cd /home/liangjian/linux-2.4.23
# gdb vmlinux
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
执行rmt宏
(gdb) rmt
breakpoint () at kgdbstub.c:1005
1005 atomic_set(&kgdb_setting_breakpoint, 0);
这时target上的内核处于调试状态,可以查看其变量、设置断点、查看堆栈等,和用gdb调试用户程序一样
查看堆栈
(gdb) bt
#0 breakpoint () at kgdbstub.c:1005
#1 0xc0387f48 in init_task_union ()
#2 0xc01bc867 in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc0387f98) at
gdbserial.c:158
#3 0xc010937b in handle_IRQ_event (irq=3, regs=0xc0387f98, action=0xce5a9860)
at irq.c:452
#4 0xc0109597 in do_IRQ (regs=
{ebx = -1072671776, ecx = -1, edx = -1070047232, esi = -1070047232, edi
= -1070047232, ebp = -1070039092, eax = 0, xds
= -1070071784, xes = -1070071784, orig_eax = -253, eip = -1072671729, xcs =
16, eflags = 582, esp = -1070039072, xss = -1072671582}) at irq.c:639
#5 0xc010c0e8 in call_do_IRQ ()
查看jiffies变量的值
(gdb) p jiffies
$1 = 76153
如果想让target上的内核继续运行,执行continue命令
(gdb) continue
Continuing.
3.3.2 内核在引导时调试
kgdb可以在内核引导时就对其进行调试,但并不是所有引导过程都是可调试的,如在kgdb 1.9版中,它在init/main.c的start_kernel()函数中插入以下代码:
start_kernel()
{
......
smp_init();
#ifdef CONFIG_KGDB
if (gdb_enter) {
gdb_hook(); /* right at boot time */
}
#endif
......
}
所以在smp_init()之前的初始化引导过程是不能调试的。
另外要想让target的内核在引导时就处于调试状态,需要修改其/etc/grub.conf文件为如下形式:
title Red Hat Linux (2.4.23-kgdb)
root (hd0,0)
kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1 gdb gdbttyS=1 gdbbaud=115200
*在target机器上
引导2.4.23-kgdb内核,内核将在短暂的运行后暂停并进入调试状态,打印如下信息:
Waiting for connection from remote gdb...
*在developer机器上
#cd /home/liangjian/linux-2.4.23
# gdb vmlinux
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
执行rmt宏
(gdb) rmt
breakpoint () at kgdbstub.c:1005
1005 atomic_set(&kgdb_setting_breakpoint, 0);
查看当前堆栈
(gdb) bt
#0 breakpoint () at kgdbstub.c:1005
#1 0xc0387fe0 in init_task_union ()
#2 0xc01bc984 in gdb_hook () at gdbserial.c:250
#3 0xc0388898 in start_kernel () at init/main.c:443
在do_basic_setup函数处设置断点,并让内核恢复运行
(gdb) b do_basic_setup
Breakpoint 1 at 0xc0388913: file current.h, line 9.
(gdb) continue
Continuing.
[New Thread 1]
[Switching to Thread 1]
Breakpoint 1, do_basic_setup () at current.h:9
9 __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
内核在do_basic_setup断点处停止运行后查看当前堆栈
(gdb) bt
#0 do_basic_setup () at current.h:9
(gdb)
3.3.3 内核模块调试调试
要想调试内核模块,需要相应的gdb支持,kgdb的主页上提供了一个工具gdbmod,它修正了gdb 6.0在解析模块地址时的错误,可以用来正确的调试内核模块
*在developer机器上
写了个测试用的内核模块orig,如下:
void xcspy_func()
{
printk("<1>xcspy_func\n");
printk("<1>aaaaaaaaaaa\n");
}
int xcspy_init()
{
printk("<1>xcspy_init_module\n");
return 0;
}
void xcspy_exit()
{
printk("<1>xcspy_cleanup_module\n");
}
module_init(xcspy_init);
module_exit(xcspy_exit);
编译该模块:
#cd /home/liangjian/lkm
#gcc -D__KERNEL__ -DMODULE -I/home/liangjian/linux-2.4.23/include -O -Wall -g -c -o orig.o orig.c
#scp orig.o root@192.168.16.30:/root
开始调试:
# gdbmod vmlinux
GNU gdb 6.0
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu"...
设置符号文件的搜索路径
(gdb) set solib-search-path /home/liangjian/lkm
执行rmt宏
(gdb) rmt
breakpoint () at kgdbstub.c:1005
1005 atomic_set(&kgdb_setting_breakpoint, 0);
设置断点使得可以调试内核模块的init函数,查内核源码可知,内核是通过module.c文件的第566行(sys_init_module函数中)mod->init来调用模块的init函数的
(gdb) b module.c:566
Breakpoint 1 at 0xc011cd83: file module.c, line 566.
(gdb) c
Continuing.
[New Thread 1352]
[Switching to Thread 1352]
这时在target机器上执行insmod orig.o,developer则相应的在断点处被暂停,如下
Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001",
mod_user=0x80904d8) at module.c:566
566 if (mod->init && (error = mod->init()) != 0) {
使用step命令进入模块的init函数
(gdb) step
xcspy_init () at orig.c:12
12 printk("<1>xcspy_init_module\n");
(gdb) n
15 }
(gdb)
说明:
调试内核模块的非init函数相对比较简单,只要先在target上执行insmod orig.o,这时由于模块的符号被加载,可以直接在developer的gdb中对想调试的模块函数设置断点,如bt xcspy_func,后面当xcspy_func被调用时就进入了调试状态。
如果想调试内核模块的init函数,由于在执行insmod之前模块的符号还没有被加载,不能直接对模块的init函数设置断点,所以相对来说要困难一些。可以采用两种变通的方法:1,采用上面介绍的在内核调用模块的init函数被调用之前的某处插入断点,如bt sys_init_module()或bt module.c:566;2,在developer上让内核处于运行状态,在target上先执行一遍insmod orig.o,这时orig.o的符号已经被加载到内存中,可以直接在developer的gdb中对模块的init函数设置断点,如bt xcspy_init,然后在target上rmmod orig.o,当下次在target上重新加载orig.o时就进入了调试状态,developer在xcspy_init处被暂停。
重启服务器
- # echo 1 > /proc/sys/kernel/sysrq
- # echo b > /proc/sysrq-trigger
1. /proc/sys/kernel/sysrq
向sysrq文件中写入1是为了开启SysRq功能。根据linux/Documentations/sysrq.txt中所说:SysRq代表的是Magic System Request Key。开启了这个功能以后,只要内核没有挂掉,它就会响应你要求的任何操作。但是这需要内核支持(CONFIG_MAGIC_SYSRQ选项)。向/proc/sys/kernel/sysrq中写入0是关闭sysrq功能,写入1是开启,其他选项请参考sysrq.txt。需要注意的是,/proc/sys/kernel/sysrq中的值只影响键盘的操作。
那么怎么使用SysRq键呢?
在x86平台上,组合键”<ALT> + SysRq + <command key>“组成SysRq键以完成各种功能。但是,在一些键盘上可能没有SysRq键。SysRq键实际上就是”Print Screen“键。并且可能有些键盘不支持同时按三个按键,所以你可以”按住ALT键“,”按一下SysRq键“,再”按一下<command key>键“,如果你运气好的话,这个会有效果的。不过放心,现在的键盘一般都支持同时按3个或3个以上的键。
<command key>有很多,这里只挑几个来说,其他的可以参考sysrq.txt文件。
- 'b' —— 将会立即重启系统,并且不会管你有没有数据没有写回磁盘,也不卸载磁盘,而是完完全全的立即关机
- 'o' —— 将会关机
- 's' —— 将会同步所有以挂在的文件系统
- 'u' —— 将会重新将所有的文件系统挂在为只读属性
2. /proc/sysrq-trigger
从文件名字就可以看出来这两个是有关系的。写入/proc/sysrq-trigger中的字符其实就是sysrq.txt中说的<command key>键所对应的字符,其功能也和上述一样。
所以,这两行命令先开启SysRq功能,然后用'b'命令让计算机立刻重启。
/proc/sysrq-trigger该文件能做些什么事情呢?
# 立即重新启动计算机 (Reboots the kernel without first unmounting file systems or syncing disks attached to the system)
echo "b" > /proc/sysrq-trigger
# 立即关闭计算机(shuts off the system)
echo "o" > /proc/sysrq-trigger
# 导出内存分配的信息 (可以用/var/log/message 查看)(Outputs memory statistics to the console)
echo "m" > /proc/sysrq-trigger
# 导出当前CPU寄存器信息和标志位的信息(Outputs all flags and registers to the console)
echo "p" > /proc/sysrq-trigger
# 导出线程状态信息 (Outputs a list of processes to the console)
echo "t" > /proc/sysrq-trigger
# 故意让系统崩溃 ( Crashes the system without first unmounting file systems or syncing disks attached to the system)
echo "c" > /proc/sysrq-trigger
# 立即重新挂载所有的文件系统 (Attempts to sync disks attached to the system)
echo "s" > /proc/sysrq-trigger
# 立即重新挂载所有的文件系统为只读 (Attempts to unmount and remount all file systems as read-only)
echo "u" > /proc/sysrq-trigger
呵呵,此外还有两个,类似于强制注销的功能
e — Kills all processes except init using SIGTERM
i — Kills all processes except init using SIGKILL
由于很多原因,Linus没有将调试器放在内核中,这也使得Linux内核调试变得比较困难,不过还好有类似KGDB等辅助工具,通过它们,我们也可以完成Linux内核的调试。
一、环境准备
目标机系统:Ubuntu 8.04 i386 desktop 32bit,目标机使用的是虚拟机系统
开发机系统:Ubuntu 8.04 i386 desktop 32bit(开发机系统用什么不是很重要,不一定必须相同)。
1) 目标机环境准备
在目标机上需要做的主要是编译并使用调试版本的内核。
1. 编译内核,2.6.26以上的内核内置KGDB,推荐使用gconfig,并保证Kernel Hacking下的enable KGDB,enable the Magic SysRq key, enable Compile the kernel with debug info选中;
2. 使用调试版本的内核,并设置为启动时等待GDB连接
以上两点可以参考文章:《VirtualBox下Ubuntu8.10的KGDB内核调试》
可以将目标机的启动方式改为字符界面启动,用如下的方法:在/etc/目录下建立文件inittab,并在其中加入下面一行:
id:3:initdefault:
然后将/etc/rc3.d/下的S30gdm改成K30gdm:
sudo mv /etc/rc3.d/S30gdm /etc/rc3.d/K30gdm
由于使用了字符界面启动,因此Magic SysRq key将只能以命令的形式使用,Alt+G+SysRq组合键不再有效,而只能通过echo g>/proc/sysrq-trigger命令。可以在bash中设置一个alias,以免每次都要输入一长串命令。由于上述命令需要root权限,因此推荐修改root用户的bash配置文件.bashrc,在最后加入一行:
alias g=’echo g > /proc/sysrq-trigger’
2) 开发机环境准备
由于目标机使用的是虚拟机,因此开发机需要做一些虚拟机相关的配置尤其是串口设置(可以参考文章:《Ubuntu下VirtualBox虚拟机串口设置》)。其次是gdb调试器的一些设置。
将目标机中编译好的内核树copy到开发机下面(注意符号连接的指向改变),切换到内核树目录下,建立一下三个gdb脚本文件:
set-remote文件(用于gdb连接到目标机的脚本):
echo set remote baud rate to 115200c/s\n
set remotebaud 115200
echo set remote target to local tcp socket\n
target remote tcp:localhost:8888
set-mod-break文件(用于设置模块加载时的断点):
echo set breakpoint in system module init function\n
break kernel/module.c:2288
print-mod-segment文件(用于显示所加载模块在内存中的各个段的地址):
set $sect_num=mod->sect_attrs->nsections
set $cur=0
while $cur < $sect_num
printf "Name:%-s Address:0x%x\n",mod->sect_attrs->attrs[$cur]->name,mod->sect_attrs->attrs[$cur]->address
set $cur=$cur+1
end
三、调试流程
1) 启动虚拟机中的目标机系统,系统在运行到等待gdb连接时停下;
2) 在开发机中使用socat tcp4-listen:8888 /tmp/vbox将主机中的/tmp/vbox管道(与目标机的串口相连)与主机中端口为8888的socket相连;
3) 进入到开发机中的Linux内核树目录下,使用gdb /.vmlinuz进入调试模式;
4) 在gdb中执行source set-remote连接到目标机,并输入c(continue)命令继续启动目标机;
5) 目标机起动后,在目标机中输入g(echo g > /proc/sysr-trigger命令的别名)命令,断下目标机,并将控制权限转交给开发机中的gdb;
6) 进入开发机的gdb,执行命令source set-mod-break,在模块加载处设置断点,并输入c命令继续执行目标机;
7) 在目标机中使用命令insmod xxx.ko加载LKM模块,会自动产生断点,执行再次返回到开发机中的gdb;
8) 在开发机的gdb中执行source print-mod-segment,显示刚加载的模块xxx.ko的各个段在内存中的地址,然后继续在gdb中执行命令add-symbol-file /dev-machine/path/to/xxx.ko 0xe11b7000 –s .data 0xe11b5800 –s .bss 0xe11b8050,0xe11b7000是.text段的地址,并确定加载符号;
9) 这样就可以在开发中的gdb中设置断点了,命令b init_module_func将在模块初始化函数入口点设置断点,设置断点后用c命令执行目标机,接着会在init_module_func处触发断点,然后可以用step,next,print等gdb命令进行单步调试和变量查看了。
创建xen虚拟机:
3.5. 常见命令
xm create /path/to/config_file
xm shutdown DomainName
xm reboot DomainName
xm pause DomainName
xm resume DomainName
xm console DomainName
更多命令请使用 xm –help查看
mkdir -p /data0/software/
cd /data0/software/
# 32位系统 centos 5.x
wget http://pkgs.repoforge.org/qemu/qemu-img-0.14.1-2.el5.rfx.i386.rpm
wget http://pkgs.repoforge.org/qemu/qemu-0.14.1-2.el5.rfx.i386.rpm
rpm -ivh qemu-img-0.14.1-2.el5.rfx.i386.rpm
rpm -ivh qemu-0.14.1-2.el5.rfx.i386.rpm
# 64位系统 centos 5.x
wget http://pkgs.repoforge.org/qemu/qemu-img-0.14.1-2.el5.rfx.x86_64.rpm
wget http://pkgs.repoforge.org/qemu/qemu-0.14.1-2.el5.rfx.x86_64.rpm
rpm -ivh qemu-img-0.14.1-2.el5.rfx.x86_64.rpm
rpm -ivh qemu-0.14.1-2.el5.rfx.x86_64.rpm
# 32位系统 centos 6.x
wget http://pkgs.repoforge.org/qemu/qemu-0.15.0-1.el6.rfx.i686.rpm
wget http://pkgs.repoforge.org/qemu/qemu-img-0.15.0-1.el6.rfx.i686.rpm
rpm -ivh qemu-0.15.0-1.el6.rfx.i686.rpm
rpm -ivh qemu-img-0.15.0-1.el6.rfx.i686.rpm
# 64位系统 centos 6.x
wget http://pkgs.repoforge.org/qemu/qemu-0.15.0-1.el6.rfx.x86_64.rpm
wget http://pkgs.repoforge.org/qemu/qemu-img-0.15.0-1.el6.rfx.x86_64.rpm
rpm -ivh qemu-0.15.0-1.el6.rfx.x86_64.rpm
rpm -ivh qemu-img-0.15.0-1.el6.rfx.x86_64.rpm
#安装完毕,qemu的bios的启动信息在 /usr/share/qemu下
#创建个虚拟机目录
mkdir /data0/software/win2003
cd /data0/software/win2003
#创建个10G硬盘镜像
qemu-img create disk.10G 10G
#假设win2003镜像位置在当前目录
#启动虚拟机的命令
#boot d 是从光驱启动 boot c 是硬盘
qemu -L /usr/share/qemu -m 512 -hda disk.10G -localtime -boot d -cdrom ./win2003.iso -localtime -net nic,vlan=0 -net tap,vlan=0,ifname=tap0,script=no -net user
-boot d :从光驱引导 a(软盘引导) c(硬盘引导) d(光驱引导)
-cdrom : ISO文件,也可以直接使用光驱设备(/dev/cdrom)...别忘了插入光盘
-hda : 就是虚拟机里的硬盘啦,也就是刚才
qemu
-img创建出的东东。
-enable-audio : 声卡支持
其它参数:
-full-screen :Start in full screen.
-usb: Enable the USB driver (will be the default soon)
-kernel bzImage:Use bzImage as kernel image.
-append cmdline:Use cmdline as kernel command line
-initrd file:Use file as initial ram disk.
QEMU 提供两种模拟模式。第一种,系统模拟,安装完全的虚拟机。运行在该系统的软件看到的计算机与主机系统完全不同 — 例如,可以在实际的 x86-64 openSUSE 计算机上运行 PowerPC Debian 系统。用户模式模拟没有这么完整。这种方式下,QEMU 模拟库会用于每个二进制文件,他们将主计算机看成自己的,因此 PowerPC 二进制文件能看到主机 x86-64 openSUSE 系统的 /etc 目录和其他配置文件。用户模式模拟能简化对本地资源、网络等的访问。
每个模拟方式都有其安装要求。对于系统模拟,安装客户操作系统就像安装一台单独的计算机(下载并使用预先配置的磁盘镜像是一个替代方法)。对于用户空间模拟,不需要安装整个系统,但要考虑安装使用软件所需的支持库。也许还需要配置跨编译器以生成想要测试的二进制文件。
# 按 ctrl + alt 释放虚拟机中的鼠标
#网络桥虚拟网卡:
yum install tunctl
yum install bridge-utils
#rc.local启动脚本
iptables -t nat -A POSTROUTING -o eth0 -s 10.0.67.0/24 -j MASQUERADE
tunctl -t tap0
ifconfig tap0 10.0.67.1 netmask 255.255.255.0
#虚拟机里网关设置成 10.0.67.1 即可联网
___________________________________________________________
#以下为有问题后检查用
echo 1>/proc/sys/net/ipv4/ip_forward
查看 tun模块
lsmod | grep tun
modprobe tun
本地虚拟机启动方法:
.\qemu-system-i386.exe -L .\Bios -m 512 -hda .\bootmgr-s001.vmdk -localtime -boot d
17.3. 使用 qemu-img
qemu-img 命令行工具是 Xen 和 KVM 用来格式化各种文件系统的。可使用 qemu-img 格式化虚拟客户端映像、附加存储设备以及网络存储。qemu-img 选项及用法如下。
格式化并创建新映像或者设备
创建新磁盘映像文件名为 size,格式为 format。
# qemu-img create [-6] [-e] [-b base_image] [-f format] filename [size]
If base_image is specified, then the image will record only the differences from base_image. No size needs to be specified in this case. base_image will never be modified unless you use the "commit" monitor command.
将现有映像转换成另一种格式
转换选项是将可识别格式转换为另一个映像格式。
命令格式:
# qemu-img convert [-c] [-e] [-f format] filename [-O output_format] output_filename
convert the disk image filename to disk image output_filename using format output_format. it can be optionally encrypted ("-e" option) or compressed ("-c" option).
only the format "qcow" supports encryption or compression. the compression is read-only. it means that if a compressed sector is rewritten, then it is rewritten as uncompressed data.
加密法是使用非常安全的 128 位密钥的 AES 格式。使用长密码(16 个字符以上)获得最大程度的保护。
当使用可增大的映像格式,比如 qcow 或 cow 时,映像转换可帮助您获得较小的映像。在目的映像中可检测并压缩空白字段。
获得映像信息
info 参数显示磁盘映像信息。info 选项的格式如下:
# qemu-img info [-f format] filename
给出磁盘映像文件名信息。使用它可获得在磁盘中保留空间大小,它可能与显示的大小有所不同。如果在磁盘映像中保存有 vm 快照,则此时也会显示。
支持格式
映像格式通常是自动获取的。支持以下格式:
raw
Raw 磁盘映像格式(默认)。这个格式的优点是可以简单、容易地导出到其它模拟器中。如果您的文件系统支持中断(例如在 Linux 中的 ext2 或者 ext3 以及 Windows 中的 NTFS),那么只有写入的字段会占用空间。使用 qemu-img info 了解 Unix/Linux 中映像或者 ls -ls 使用的实际大小。
qcow2
QEMU 映像格式,最万能的格式。使用它可获得较小映像(如果您的系统不支持中断,例如在 Windows 中,它会很有用)、额外的 AES 加密法、zlib 压缩以及对多 VM 快照的支持。
qcow
旧的 QEMU 映像格式。只用于与旧版本兼容。
cow
写入映像格式的用户模式 Linux 副本。包含 cow 格式的目的只是为了与前面的版本兼容。它无法在 Windows 中使用。
vmdk
VMware 3 和 4 兼容映像格式。
cloop
Linux 压缩回送映像,只有在重复使用直接压缩的 CD-ROM 映像时有用,比如在 Knoppix CD-ROM 中。
例如:
将img文件转化为qcow2_cow文件形式:
qemu-img convert root.img -O qcow2 kvm-centos-5.4-64-weibo_duilie_php5.2.14.qcow2_cow
反之一样。
按需支付是云计算服务众所周知的优势之一。
以EC2为例,它所提供的三种服务方式中,On—Demond
instance
提供以Gb/小时为颗粒度的计费单位,无须预付费,也无需承诺试用时长,并可以通过Auto Scaling功能自动增删所租用的虚拟资源,做到了按需支付,我们目前所宣传的也基本上指的是这种模式。
另外一种Reserved
Instance
收费方式与On-demond
instance
本质一样,只不过Reserved
Instance
需要事先承诺合同时长,如一年或三年,并需要交纳一定的一次性费用,但是此后在实际使用中仍然按照小时计费,而且单价要比On-demond
instance
平均降低50%。在服务等级上,Reserved
instance
也要高于On-demond
instance
,亚马逊保证Reserved
instance
用户随时可以获得其所需要的服务资源,而On-demond
instance
则没有这方面的保证,当然,这种情况也很少出现。
最后,说说其第三种
Spot
Instance
,也是最有意思的一种。在这种服务中,用户可以自己定价,定下用户愿意接受的最高价格,来租用EC2服务的闲散资源。亚马逊根据供需情况会周期性的发布即时价格,当用户最高限价高于其即时价格时,服务进行,且实际支付价格为系统即时价格。当用户最高限价低于即时价格时,系统自动终止服务,待即时价格低于用户最高限价时服务再次启动。这对于用户的预算是一个更灵活的保证方式。这种模式更适合于需要大量计算能力但对计算响应要求不高的用户,如科学计算等。当然,用户需要自行保证使用
Spot
instance
的应用对于随时宕机具有调整能力
使用QEMU+GDB能够实现源代码级的内核调试,但是,存在一个问题——当内核允许中断时,单步命令(n与s)会进入时钟中断。通过浏览QEMU的源代码,大体把原因找了出来。 单步命令(n与s)在gdb远程调试通讯协议中是s(参看info gdb),qemu的gdb stub在受到s命令后将虚拟CPU进入单步状态,但是在接收到s命令前,qemu的虚拟CPU是停止的(在等待gdb的命令),注意,这个时
候,虚拟时钟计时并没有停止,所以,很可能在qemu的虚拟CPU还没开始的时候就需要触发时钟中断了,但是虚拟CPU还在停止状态,中断无法触发。接收到s命令后,虚拟CPU开始执行指令。这时,如
果内核允许中断,虚拟时钟就将触发中断,所以s命令执行一条指令后停止在时钟中断处理程序的开始处,而不是希望的函数中下一条指令处。
现在看一下问题的解决方法。在我看来,需要修改gdb远程调试内核时单步命令的语义。有两个方向。
1.在gdb上修改。在处理用户的n与s命令时不是发送协议中的s命令,而是分两步。首先确定下一条指令的开始位置(或者下一行源程序对应的指令的开始位置)。对于有些RISC机器机器指令固定为某个长度,那么确定这个位置比较简单,但是对于像x86这样的变长指令的体系结构就需要稍微麻烦一点(需要确定当前指令的长度等)。然后假如第一步确定的地址是naddr。现在像处理用户的tbreak *naddr一样处理就可以了,接着发送继续运行命令c就可以了。
2.在qemu的gdb stub上修改协议命令s的处理方法。接收到s命令后不是让虚拟CPU进入单步执行状态,而是确定在没有中断的情况下,下一条指令的位置(注意对于当前是跳转指令的情况处理比较复杂),然后在这个位置设置临时断点,在虚拟CPU到达这个断点进入gdb stub后立即将其取消。
这两种处理方法中,我认为1比较好,实现起来清晰明了,但是需要对gdb的代码比较熟悉。2方法比较复杂,尤其是在当前指令是跳转指令时,不太容易确定临时断点的位置。
另外作为暂时的权宜之计,我们可以只使用tbreak +offset来代替n与s命令。
1:前言:
最近几天学习Linux-2.6平台上的设备驱动,所以要建立内核及内核模块的调试平台.虽然网上有很多相关教程,但多是基于2.6.26以前的通过补丁 安装的,过程非常复杂,而且问题比较多.linux从 2.6.26开始已经集成了kgdb,只需要重新编译2.6.26(或更高)内核即可.kgdb安装及模块调试过程也遇到不少问题,网上网下不断的搜索与 探索,才算调通.现在记录下来,供朋友们参考.
首先说一下,开始本打算安装kdb进行内核调试,后来听说kdb只能进行汇编级别的调试,所以放弃,改用kgdb.
2: 系统环境:
虚拟环境: VMWare Workstation 5.5(英文版)
操作系统: CentOS-4.6-i386(原内核2.6.9,将会把内核升级至2.6.26)
注:CentOS 是RedHat的一个社区版本.
(由于我们采用的linux kernel 2.6.26已经集成kgdb,kgdb再不需要单独下载)
3:系统的安装:
在VMWare中新建一台计算机:
点击 Next
选中Custom 点击 Next
选中 New-Workstation 5,点击Next
选中 Linux ,Version选中Other Linux 2.6.x kernel 点击Next
Virtual machine name 输入Client(Development) 点击Next
Processors 选中 One, 点击Next
Memory 输入256,点击Next
Network connection 选中Use network address translation(NAT) (选第一个貌似也可以) 点击Next
I/O adapter types
SCSI Adapters 选中默认的LSI Logic(这里如果你后面使用了SCSI格式的Disk,编译内核时需要添加相应的驱动,我选择的是IDE的硬盘,kernel默认就支持了)
Disk 选中Create a new virtual disk 点击Next
Virtual Disk Type 选中IDE,点击Next
Disk capacity Disk size 输入80G (下面的Allocate all disk space now不要选中,表示在真正使用才分配磁盘空间, Split disk into 2 GB files项,可不选,如果你的系统分区为fat32格式,最好选中) 点击Next.
Disk file ,输入Disk的名称,如:disk1.vmdk ,点击Finish.完成
安装CentOS 4.6(过程略过)
安装完成后,关闭计 算机,然后Clone一台同样的计算机.步骤如下:
点击VM->Clone
选中默认的From current state,点击Next
选中Create a full clone, 点击Next
Virtual Machine name 输入Server(Targe),将克隆的机器命令为目标机.
说 明一下,kgdb 需要两台计算机通过串口进行远程调试,两台计算机分别为:
Client(Development):开发机,也称客户机,将 在该计算机上进行程序的开发,GDB将在本计算机上运行.用于输入命令控制Server(target)的运行.
Server(Target): 目标机,也称服务器,就是被调试的计算机,在Development机上开发好的内核模块程序将拷贝到Target上运行,其运行受到 Development命令的控制.
分别为两个系统增加一个串口,以"Output to named pipe"方式,其中:
Client端选择"this end is the client", "the other end is a virtual machine"
Server端选择"this end is the server", "the other end is a virtual machine"
备注: 两个pipe的名称要相同,并且选中下面的Connect at power on,及Advanced里面的Yield CPU on poll
以后的部分,Server上的操作与Client上的操作将以不同的背景色 显示,输入的命令将以不同的字体颜色并带下划线显示.请注意:
Server(Target) 输入: cat /dev/ttyS0
系统输出的信息: hello Client(Development) 输入: echo "hello" >/dev/ttuS0
串 口添加完成后,使用如果命令测试:
在Server上cat /dev/ttyS0
然后到Client上 echo "hello" > /dev/ttyS0
这时回到Server上,如果能看到输入的hello,说明串口通讯正常.
4:升级内核 2.6.26(添加关于KGDB的选项)
说明一下,这里是要升级Server的内核,因为kgdb是要Server上运行的,但是编译需要在Client完成(或者你也可以在Server上编 译,之后再拷贝到Client上),因为调试时Client上的gdb会用到编译的内核及源代码.Client也需要升级,保证Client同 Server上的内核一致,这样Client上编译的模块拿到Server上加载就不会有什么问题.
首先下载kernel 2.6.26
我习惯在windows上下载,然后共享,再到linux使用smbclient连接,拷贝到Linux上.你也可以直接在linux上下载.
smbclient用法 : smbclient //192.168.0.100/share -Uadministrator 回车后,会提示输入admin的密码.之后就可以通过get获取了 192.168.0.100是我在windows主机的IP,share为共享名,-U后面是用户名
编译Kernel2.6.26
进入Client(Development)系统,将linux-2.6.26.tar.bz2拷贝到/usr/src目录下:
cd /usr/src
tar jxvf linux-2.6.26.tar.bz2
ln -s linux-2.6.26 linux
cd linux
make menuconfig
File System --> 下面把ext3,ext2都编译进内核(就是把前面的M变成*)
Kernel Hacking -->
选中Compile the kernel with frame pointers
选中KGDB:kernel debugging with remote gdb
并确认以下两项也是选中的(他们应该默认是选中的)
> kernel debugging
> Compile the kernel with debug info
对 于其它选项,请按实际情况,或你的要求定制.
在其它网友的说明里面,会有Serial port number for KGDB等选项,但是我使用的版本未找到这些选项,所以忽略过.
保存退出
make -j10 bzImage
-j10表示使 用10个线程进行编译.
make modules
编译内核模块
make modules_install
安装内核模 块
make install
安装内核
将Client系统中的linux-2.6.26整个目录同步到Server上.
在Client系统上运行下列命令:
cd /usr/src/linux
scp -r linux-2.6.26 root@Server(Target)IP:/usr/src/
系统会提示输入root的密码,输入完了就会开始复制文件了,(这里要注意,如果原系统已经是2.6.26的内核,可以只拷贝arch/i386 /boot/bzImage,及System.map文件到Server上,然后修改/boot/grub/grub.conf,但由于我是从2.6.9 升级上来,所以需要将整个linux原代码目录拷贝到Server上进行升级)
升级Srever系统的内核
进入 Server(Target)系统,usr/src目录:
ln -s linux-2.6.26 linux
cd linux
make modules_install
安装内核模块
make install
安装内核
安 装完成后,在/boot/目录下会有即个新添加的文件./boot/grub/grub.conf文件也会添加一个新的启动项,我的如下(行号不是文件的 一部分):
1 # grub.conf generated by anaconda
2 #
3 # Note that you do not have to rerun grub after making changes to this file
4 # NOTICE: You have a /boot partition. This means that
5 # all kernel and initrd paths are relative to /boot/, eg.
6 # root (hd0,0)
7 # kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
8 # initrd /initrd-version.img
9 #boot=/dev/hda
10 default=0
11 timeout=5
12 splashimage=(hd0,0)/grub/splash.xpm.gz
13 hiddenmenu
14 title CentOS (2.6.26)
15 root (hd0,0)
16 kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00
17 initrd /initrd-2.6.26.img
18 title CentOS-4 i386 (2.6.9-67.ELsmp)
19 root (hd0,0)
20 kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
21 initrd /initrd-2.6.9-67.ELsmp.img
注意里面的NOTICE说明,我的系统/boot是一个独立的分区,所以下面配置的文件路径都是相对于/boot/目录的,像 /vmlinuz-2.6.26,实际到它的绝对位置应该是/boot/vmlinuz-2.6.26. 在你的系统上请根据实际情况处理.
修改一下grub.conf,修改成下面这样:
1 # grub.conf generated by anaconda
2 #
3 # Note that you do not have to rerun grub after making changes to this file
4 # NOTICE: You have a /boot partition. This means that
5 # all kernel and initrd paths are relative to /boot/, eg.
6 # root (hd0,0)
7 # kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00
8 # initrd /initrd-version.img
9 #boot=/dev/hda
10 default=0
11 timeout=5
12 splashimage=(hd0,0)/grub/splash.xpm.gz
13 hiddenmenu
14 title CentOS (2.6.26)
15 root (hd0,0)
16 kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200
17 initrd /initrd-2.6.26.img
18 title CentOS (2.6.26) Wait...(kernel debug)
19 root (hd0,0)
20 kernel /vmlinuz-2.6.26 ro root=/dev/VolGroup00/LogVol00 kgdboc=ttyS0,115200 kgdbwait
21 initrd /initrd-2.6.26.img
22 title CentOS-4 i386 (2.6.9-67.ELsmp)
23 root (hd0,0)
24 kernel /vmlinuz-2.6.9-67.ELsmp ro root=/dev/VolGroup00/LogVol00
25 initrd /initrd-2.6.9-67.ELsmp.img
说明:
第一个启动项在原来的基础上添加了kgdb的参数kgdboc=ttyS0,115200
kgdboc 的意思是 kgdb over console,这里将kgdb连接的console设置为ttyS0,波特率为115200,如果不在内核启动项中配置该参数,可以在进入系统后执行命 令:
echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
第二个启动 项,同第一个使用同一个内核,只是添加了kgdbwait参数
kgdbwait 使 kernel 在启动过程中等待 gdb 的连接。
我的启动菜单如下:
CentOS(2.6.26)
CentOS(2.6.26)Wait...(kernel debug)
CentOS-4 i386(2.6.9-67.ELsmp)
调用内核模块,就选择第一个,如果要调试内核启动过程,选择第二个.
5.内核调试
重 启Server,通过启动菜单第二项CentOS(2.6.26)Wait...(kernel debug)进入系统. 只到系统出现:
kgdb: Registered I/O driver kgdboc
kgdb: Waiting for connection from remote gdb
进入Client系统.
cd /usr/src/linux
gdb vmlinux
启动gdb开始准备调试:输出大致如下:
GNU gdb Red Hat Linux (6.3.0.0-1.153.el4rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db
library "/lib/tls/libthread_db.so.1" (gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0
注意:有的文章讲:因为vmware的named piped不能被gdb直接使用,需要使用 socat -d -d /tmp/com_1 /dev/ttyS0转换,然后使用转换后的设备. socat需要自己下载安装. 但在我的环境中,直接使用并没有出错.
执行该命令后输出如下:
Remote debugging using /dev/ttyS0
kgdb_breakpoint () at kernel/kgdb.c:1674
1674 wmb(); /*Sync point after breakpoint */
warning: shared library handler failed to enable breakpoint看到上面的内容说明已经连接成功,但Server上依然是假死状态,这时你可以像使用本地gdb一样设置断点(break),单步执 行(step),或其它命令.
(gdb) cont
继续执行,Server就继续下面的系统初始化了.
系统启动完成后的内核 调试:
进入Server后,执行命令
echo g > /proc/sysrq-trigger
系统同样会中断,进入假死状态,等待远程gdb的连接.KGDB可能会 输出如下信息:
SysRq: GDB 上面的命令(echo g > /proc/sysrq-trigger)可以有一个快捷键(ALT-SysRq-G)代替,当然前提是你编译内核时需要选中相关选项,并且需要修改配置 文件:/etc/sysctl.conf , 我用了一下,不太好用.因为有的桌面系统中PrintScreen/SysRq键是用于截屏的.所以还是直接用命令来的好!
我 在~/.bashrc中添加了一句(添加完保存后,要执行source ~/.bashrc应用该配置):
alias debug='echo g > /proc/sysrq-trigger'
之后就可以直接输入debug来使内核进入调试状态.
Server进入调试状态 后,转换到Client系统,重复上面的步骤.
6. Linux内核模块(设备驱动)的调试
编写内核模块,及Makefile
我使用的例子是Linux Device Driver 3中的例子scull. 你可以从这里下载LDD3的所有例子程序.
进入Client系统,解压example.tar.gz,将其中的example/scull目录拷贝到/root/scull
然后执 行:
cd /root/scull
make
编译应该会出错,因为Linux Device Driver 3的例子程序是基于2.6.10内核的,请参靠下面的说明修改这些错误:
编译scull驱动,完成后,scull目录应该多出了scull.ko 及其它中间文件.
scp scull.ko root@targetIp:/root/
将 scull.ko模块文件拷贝到Server上.系统应该会提示输入密码:
root@targetIp's password:输入正确密码后,应该能看到如下信息,表示复制成功了:
scull.ko 100% 258k 258.0kb/s 00:00进入Server系统输入:
cd /root/
insmod scull.ko
加载scull模块.
cat /sys/module/globalmem/sections/.text
显示scull模块的.text段地址.运行该命令后,会返回 一个16进制的地址,如:
0xd099a000echo g > /proc/sysrq-trigger
现在Server 系统变成等待状态.
再次进入Client系统:
cd /usr/src/linux
gdb vmlinux
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
kgdb_breakpoint () at kernel/kgdb.c:1674
1674 wmb(); /*Sync point after breakpoint */
warning: shared library handler failed to enable breakpoint出现上面的信息表示连接成功,但此时还不可以设置断点.我试了N次,现在设置端点后,无论Server上对scull做什么操 作,Client上的gdb都不会停止.也有人说这是gdb的BUG,gdb无法正常解析内核模块的符号信息. 说是下载kgdb网站上的gdbmod可以解决该问题,但我试了之后发现根本没有用. 所以只能通过命令手动加载相关符号信息:
(gdb) add-symbol-file /root/scull/scull.ko 0xd099a000
注意: 0xd099a000地址是在上面获取的,命令输入完了,系统会有如下提示信息(第二行后面的y是自己输入的:
add symbol table from file "/root/scull/scull.ko" at .text_addr = 0xd099a000
(y or n)y
Reading symbols from /root/scull/scull.ko...done. (gdb)break scull_write
Breakpoint 1 at 0xd099a2d9: file /root/scull/main.c,line 338.(gdb)break scull_read
Breakpoint 2 at 0xd099a1a2: file /root/scull/main.c,line 294(gdb)cont
ContinuingClient上的工作暂停,回到Server系统上:
Server现在处于运行状态,在Server上运行下面的命 令:
cat /proc/devices | grep "scull"
查看scull模块分配的major.我的系统中返回如下:
253 scull
253 scullp
253 scullamknod /dev/scull c 253 0
253是刚才查询到的 版本号.
echo "this is a test " > /dev/scull
测试输入函数:scull_write. 该命令输入完成后,进程应该会停下来, 请切换到Client系统.
cat /dev/scull
测试输出函 数:scull_read. 该命令输入完成后,进程应该会停下来, 请切换到Client系统.
回到Client系统,应该能看到gdb的输 出信息:
Breakpoint 1, scull_write (filp=0xce5870c0,buf=0xb7f44000
"this is a test\nias | /usr/bin/which --tty-only --read-alias
--show-dot --show-tilde'\n",count=15,f_pos=0xce5c5f9c)
at /root/scull/main.c:338
338 {
(gdb)_ 现在就可以像调试本地程序一样调试scull.ko模块了.
以同样的方法也可以调试其它函数.(初始化函数暂时没想到办法调试,因为使用这种方 法需要先加载后,才可以进行调试)
你可以*使用和转载本文档,转载时请注明出处. (jie123108@163.com)
如果可以,我想写一个脚本自动完成这个添加符号文件的过程,简化调试过程.
Linux Device Driver 3rd中的scull 例程在2.6.26上编译出错的问题
1。scripts/Makefile.build:46: *** CFLAGS was changed in "examples/scull/Makefile". Fix it to use EXTRA_CFLAGS。 停止。
解决方法:将 Makefile 中的 CFLAGS 改为 EXTRA_CFLAGS
2. examples/scull/main.c:17:26: error: linux/config.h: 没有该文件或目录
解决方法: 将 main.c 中的这条 include 语句注释掉。
3. examples/scull/access.c: 在函数‘scull_u_open’中: examples/scull/access.c:107: 错误: 提领指向不完全类型的指针
解决方法:access.c 中添加:#include <linux/sched.h>
4. examples/scull/access.c: 在函数‘scull_access_setup’中:
examples/scull/access.c:355: 警告: 格式字符串不是一个字面字符串而且没有待格式化的实参
解决方法:将 kobject_set_name(&dev->cdev.kobj, devinfo->name); 改为:
kobject_set_name(&dev->cdev.kobj, "%s", devinfo->name);
因为 kobject_set_name 有一个像 printf 一样的参数表。
补充 : 老外作的改动http://www.cs.fsu.edu/~baker/devices/lxr/source/2.6.25/ldd-examples/基 本上已经可以编译了
摘录自: <<Linux Device Driver 3 中的代码在 2.6.27 中编译不能通过的问题>>
参考资料:
<<Using 2.6.26 Linux Kernel Debugger (KGDB) with VM>>
<<VMware环境下用kgdb调试内核>>
<<Using 2.6.26 Linux Kernel Debugger (KGDB) with VM>>
这是我写的一些关于kgdb在2.6.27里面的源代码分析,欢迎指教。
KGDB
0. 概述
前段时间用kgdb调了一下内核,感觉这个东西还不错,不过更令人感兴趣的是它的工作原理. 内核运行得好好的,那么多线程在好几个cpu上面跑,就是因为踩到一个断点,它就把所有东西都放下来让gdb来调,kgdb是怎样办到的?
本文是本人看kgdb的代码的一些记录,如有错误的地方敬请指出.
一般软件调试的原理是,调试器根据目标文件的调试信息找到源码和机器码之间的映射关系,并把它关心的指令替换成一条断点指令,x86对应的就是asm("int $3"),它的二进制代码是0xcc.当程序执行到这个地方时断点指令被执行,程序被中断,调试器接管它的控制权,这时可以查看内存信息甚至修改内存.当调试器完成任务后把断点指令替换回原来的指令,并把pc寄存器减一,让cpu从被中断的那条指令开始执行.这样程序可以在没有影响执行的情况下被调试.
调试内核也离不开一般原理,只不过内核不同一般程序,不能单靠同一台机的gdb来完成.要依靠kgdb,它是内核里面的一小段程序,它可以和另外一台机器上的gdb进行通信,可以执行gdb过来的一些命令并返回信息,完成调试过程.
关于kgdb的配置,网上有很多,而且非常详细,这里就不重复了.
本文的内容:
1.从内核是怎样把控制权交给kgdb的开始,到
2.kgdb的入口函数,看kgdb怎样处理竞态,再了解
3.kgdb和gdb之间的通信协议,接着是
4.kgdb对具体命令的执行,最后再看看
5.kgdb是在什么时候被启动的.
内核版本是2.6.27.
>>>>>>>>>1. 异步通知>>>>>>>>>
当内核踩到一个断点时,当前进程是用什么方式通知kgdb,控制权又是怎样到kgdb手上的呢?
先回顾一下kgdb的patch在2.4上是怎样让内核通知kgdb的处理代码的,下面是2.4.23的内核打上kgdb补丁的部分代码:
(arch/i386/kernel/traps.c)
#ifdef CONFIG_KGDB
#define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after) \
{ \
if (linux_debug_hook != (gdb_debug_hook *) NULL && !user_mode(regs)) \
{ \
(*linux_debug_hook)(trapnr, signr, error_code, regs) ; \
after; \
} \
}
#else
#define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after)
#endif
#define DO_VM86_ERROR(trapnr, signr, str, name) \
asmlinkage void do_##name(struct pt_regs * regs, long error_code) \
{ \
CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,goto skip_trap)\
do_trap(trapnr, signr, str, 1, regs, error_code, NULL); \
skip_trap: \
return; \
}
DO_VM86_ERROR这个宏是用来生产异常处理函数的。比如你的name参数是int3那就会出来一个do_int3()的函数,用它来处理3号异常,也就是断点异常.而linux_debug_hook是2.4里面指向kgdb主处理函数的函数指针。
从上面代码可以看到,在2.4里面kgdb把它的主处理函数直接插入到异常处理函数的开头,当异常发生时kgdb就直接得到控制权.
说实话,这种做法比较难看.而在2.6里面,做法其实差不多,但我们有了notifier chian这种异步通知机制.什么是notifier chain?它就是一条回调函数的链表,每一条链表代表一种事件,每个关心这个事件的模块或子系统就在这条特定链上面注册自己的回调函数.代码中notifier_block结构体代表链表里面的一个节点,包含一个函数指针和一个整形变量(描述优先级).当事件发生时对应的函数链上的函数就会按优先级被执行.
在内核中根据不同的事件定义了一些不同的链,比如:die_chain,reboot_notifier_list,netlink_chain...等等,更加具体的描述可以在内核Documentation目录中或网上找到.而它们的实现都是对notifier chain进行封装,notifier chain的具体实现请见kernel/notifier.c.
die_chain这条链关心的是中断和异常事件,kgdb在它上面注册了自己的函数.
(arch/x86/kernel/kgdb.c)
515 static struct notifier_block kgdb_notifier = {
516 .notifier_call = kgdb_notify,
517
518 /*
519 * Lowest-prio notifier priority, we want to be notified last:
520 */
521 .priority = -INT_MAX,
522 };
kgdb很谦虚,把自己的优先级设为最低,先让别人执行,最后才考虑自己.
异常发生时die_chain上的函数都会被触发.对于kgdb,最关心的当然是3号异常,它就是程序踩到一个断点时引发的异常.函数notify_die()就是遍历die_chain链,执行所有上面的函数:
(arch/x86/kernel/traps_32.c)
851 void __kprobes do_int3(struct pt_regs *regs, long error_code)
852 {
853 trace_hardirqs_fixup();
854
855 if (notify_die(DIE_INT3, "int3", regs, error_code, 3, SIGTRAP)
856 == NOTIFY_STOP)
857 return;
858 /*
859 * This is an interrupt gate, because kprobes wants interrupts
860 * disabled. Normal trap handlers don't.
861 */
862 restore_interrupts(regs);
863
864 do_trap(3, SIGTRAP, "int3", 1, regs, error_code, NULL);
865 }
kgdb的回调函数kgdb_notify()(定义在arch/x86/kernel/kgdb.c)最后被调用,而它又直接调用__kgdb_notify(),这个函数对某些特殊情况做了处理,我们后面在遇到这些情况时会回到这个地方进行说明,在大多数情况下程序会直接进入kgdb的入口函数kgdb_handle_exception(),它定义在kernel/kgdb.c.
int3中断
|
v
do_int3()
'->notify_die()
|-> .. //其他关心异常的函数们
|-> ..
|-> ..
'-> kgdb_notify()
'->__kgdb_notify() //关中断
'->kgdb_handle_exception() //kgdb入口
所以kgdb是通过异常触发的,而且整个过程都处在异常处理中.当前进程被异常中断,而异常处理通过notifier chain把控制权交给了kgdb。
>>>>>>>>>2. 准备工作>>>>>>>>>
下面来看kgdb的主函数kgdb_handle_exception(),这个函数前面一段只是在做准备工作,为后面和远程的gdb通信打下基础。
在这个地方又有一个问题:在当今多线程和多cpu的情况下,到处都充满了竞态,那个线程都怕被别人搞破坏,那kgdb又是怎样让自己立于不败之地,控制好cpu们的?
(kernel/kgdb.c)
1392 int
1393 kgdb_handle_exception(int evector, int signo, int ecode, struct pt_regs *regs)
1394 {
1395 struct kgdb_state kgdb_var;
1396 struct kgdb_state *ks = &kgdb_var;
1397 unsigned long flags;
1398 int error = 0;
1399 int i, cpu;
1400
1401 ks->cpu = raw_smp_processor_id();
1402 ks->ex_vector = evector;
1403 ks->signo = signo;
1404 ks->ex_vector = evector;
1405 ks->err_code = ecode;
1406 ks->kgdb_usethreadid = 0;
1407 ks->linux_regs = regs;
1408
1409 if (kgdb_reenter_check(ks))
1410 return 0; /* Ouch, double exception ! */
参数evector是中断向量,当这个异常发生时signo是发送给当前进程的信号编号,ecode是错误码,regs是当前进程的寄存器值。
ks的类型为kgdb_state,它记录了这次断点发生的信息。不知道是别有用心还是不小心作者给同一个变量ks->ex_vector赋了两次值.
kgdb_reenter_check()函数在检查是否出现了一种递归触发的现象,也就是当kgdb正在一个cpu上处理一个断点的过程中,嵌套地触发了kgdb,再次进入kgdb_handle_exception()中,kgdb都已经拿到控制权了,谁还那么无聊自己抢自己的东西。如果遇到这种情况,kgdb尝试在kgdb_reenter_check()里面纠正这个错误,并直接结束这次调用,这只是一个特殊的错误处理,我们暂时不关心它。
(kernel/kgdb.c)
1412 acquirelock:
1413 /*
1414 * Interrupts will be restored by the 'trap return' code, except when
1415 * single stepping.
1416 */
1417 local_irq_save(flags);
1418
1419 cpu = raw_smp_processor_id();
1420
1421 /*
1422 * Acquire the kgdb_active lock:
1423 */
1424 while (atomic_cmpxchg(&kgdb_active, -1, cpu) != -1)
1425 cpu_relax();
local_irq_save()保存当前中断状态,然后禁中断。raw_smp_processor_id()获得当前CPU号。
atomic_cmpxchg()函数实现了一个比较+交换的原子操作(原子就是说cpu要不就不做,要做就一定要做完某些操作才能干别的事情,对应这里就是比较和交换要一次过做完).atomic_cmpxchg()比较kgdb_active->count的值是否等用-1,如果是则把cpu的值赋给kgdb_active->count,否则不修改它的值,atomic_cmpxchg返回kgdb_active->count赋值前的值.
kgdb_active是一个全局原子变量,定义在kernel/kgdb.c中,用来记录当前正在执行kgdb代码的cpu号,它起到一个锁的作用,因为同一时间只能有一个cpu执行kgdb的代码,这是可以想象得到的,如果两个cpu在两个不同断点被触发,那究竟是谁和远端gdb通信呢?前一条命令被cpu1拿了,后一条却去了cpu2那里,那还得了。
kgdb_active的初始值为-1,-1表示当前kgdb的处理函数并没有被触发,相反如果kgdb已经在运行,那么kgdb_active就有它自己的值,这些处理都是针对多cpu的,如果只有一个cpu,这个世界就简单多了。这里是防止多个kgdb的实例在不同cpu被触发引起互相干扰。考虑这种情况,在cpu1上有一个断点让kgdb起来,这时,kgdb_active还是-1,cpu1很顺利就给kgdb_active赋值然后进入后面的操作.这时cpu2中kgdb也被触发.它也想进入后面的操作,但是这时候kgdb_active已经不再是-1,cpu2只能不断地比较kgdb_active的值和执行cpu_relax(),宏cpu_relax()可以简化为一条pause汇编,通过引入一个很短的延迟,加快了紧跟在锁后面的代码的执行并减少能源的消耗,实际上就是让cpu2等。当cpu1在退出kgdb_handle_exception()前会把kgdb_active赋回-1,这样cpu2就可以进行后面的操作了。kgdb使用大量的原子操作来完成锁的功能,后面还会看到. atomic操作加上cpu_relax()跟一个自旋锁很相似。
(kernel/kgdb.c)
1427 /*
1428 * Do not start the debugger connection on this CPU if the last
1429 * instance of the exception handler wanted to come into the
1430 * debugger on a different CPU via a single step
1431 */
1432 if (atomic_read(&kgdb_cpu_doing_single_step) != -1 &&
1433 atomic_read(&kgdb_cpu_doing_single_step) != cpu) {
1434
1435 atomic_set(&kgdb_active, -1);
1436 touch_softlockup_watchdog();
1437 clocksource_touch_watchdog();
1438 local_irq_restore(flags);
1439
1440 goto acquirelock;
1441 }
注释告诉了我们大部分事情.它在考虑这样一种情况:cpu1先进入kgdb,并和远端gdb通信中确定要进行单步调试(step),设置好cpu1上面标志寄存器的X86_EFLAGS_TF位(也就是trap位,或单步调试位)好让cpu在运行完后面一条指令后引起一个debug异常(1号异常)再次进入kgdb,接下来cpu1让程序继续下一条指令.这时候cpu2闯进来了.当cpu1退出kgdb时会把kgdb_active设置为-1,这样上面提到的那个锁就解开了。cpu2就可以进来了,但是kgdb在一个cpu处在单步调试时不想让别的cpu进来扰乱,就让cpu2放弃kgdb_active锁,然后让它再拿一次锁,期望cpu1能够先处理,cpu2的事以后再说。kgdb_cpu_doing_single_step用来记录当前那个cpu处于单步调试的状态,-1代表没有cpu在单步调试状态。后面我们将看到当kgdb收到远程gdb单步执行命令时会设置这个变量.
(kernel/kgdb.c)
1443 if (!kgdb_io_ready(1)) {
1444 error = 1;
1445 goto kgdb_restore; /* No I/O connection, so resume the system */
1446 }
检查kgdb要用的I/O驱动模块是否已经加载好了,如果没有准备好就没有必要往下走直接返回,让系统起来。关于I/O的初始化这里暂时就不讨论了。
(kernel/kgdb.c)
1448 /*
1449 * Don't enter if we have hit a removed breakpoint.
1450 */
1451 if (kgdb_skipexception(ks->ex_vector, ks->linux_regs))
1452 goto kgdb_restore;
kgdb_skipexception(),一个挺有意思的函数,注释写着"如果我们命中了一个已经被删除的断点,下面的代码就不要走下去了".这是什么意思?断点被删除了还能够被命中吗?迫不及待看看它的代码,这是一个与体系结构相关的函数,暂时只有x86对它有实质性的实现:
(arch/x86/kernel/kgdb.c)
530 /**
531 *
532 * kgdb_skipexception - Bail out of KGDB when we've been triggered.
533 * @exception: Exception vector number
534 * @regs: Current &struct pt_regs.
535 *
536 * On some architectures we need to skip a breakpoint exception when
537 * it occurs after a breakpoint has been removed.
538 *
539 * Skip an int3 exception when it occurs after a breakpoint has been
540 * removed. Backtrack eip by 1 since the int3 would have caused it to
541 * increment by 1.
542 */
543 int kgdb_skipexception(int exception, struct pt_regs *regs)
544 {
545 if (exception == 3 && kgdb_isremovedbreak(regs->ip - 1)) {
546 regs->ip -= 1;
547 return 1;
548 }
549 return 0;
550 }
这段注释基本上没有说明什么问题,实现更是看不懂,kgdb_isremovedbreak()是干那行的?为什么被删除了的断点还会起作用呢?google搜了一些,基本上只能找到一堆patch的信息,还有什么线索呢?不要忘记邮件列表这个好东西,牛B门们都在上面.在kgdb的邮件列表(http://www.sourceforge.net/mailarchive/forum.php?forum_name=kgdb-bugreport)搜了一下,找到了一个位大侠06年发的这样一封信:
We keep track of removed entries to prevent breakpoints from occuring after
they were removed. Removed breakpoints might have occured on other processors
before there removal from code, which will pop up on continuing and cause
segfaults on i386. kgdb_skipexception function identifies them.
看到"other processors"让人恍然大悟,又是多cpu惹的祸.考虑这样一种情况:有两个cpu,cpu1,cpu2,gdb在之前设置了两个两个断点B1,B2,cpu1命中了B1,并且拿到了前面的kgdb_active锁,在还没有停止其它cpu的活动前(后面我们马上会讲到),cpu2命中了B2,但是由于kgdb_active锁已经被cpu1拿了, cpu2将忙等kgdb_active. cpu1进入了kgdb的执行代码,在和远程gdb通信过程中,gdb要求删除B2,cpu1没法知道正在门外等着的cpu2正是因为B2而来的,cpu1直接把B2删除,然后gdb让程序继续.在cpu1放弃kgdb_active锁之后,cpu2进来了,但是cpu2进来已经没有意义了,如果我们在这里不加以判断,后面就会出大bug了,所以我们需要判断一下触发我们的这个断点是不是已经被删除了,kgdb_isremovedbreak()正是用来作这个判断,如果真的是这种情况,pc寄存器(regs->ip)就退一.
(kernel/kgdb.c)
1458 kgdb_info[ks->cpu].debuggerinfo = ks->linux_regs;
1459 kgdb_info[ks->cpu].task = current;
1460
1461 kgdb_disable_hw_debug(ks->linux_regs);
pre_exception()让驱动在进行连接前有机会去做一些自己特有的初始化,。我们可以先看看关于串口的这个函数.
(drivers/serial/kgdboc.c)
143 static void kgdboc_pre_exp_handler(void)
144 {
145 /* Increment the module count when the debugger is active */
146 if (!kgdb_connected)
147 try_module_get(THIS_MODULE);
148 }
看到了,对于串口来说根本就没有做什么事情,只是增加模块的使用计数。
回到我们的kgdb_handle_exception(),kgdb用kgdb_info数组记录当前进程和它的寄存器,方便日后和远程gdb通信时使用。kgdb_disable_hw_debug()对应到系统结构上是调用arch/x86/kernel/kgdb.c中的kgdb_disable_hw_debug:
(arch/x86/kernel/kgdb.c)
300 /**
301 * kgdb_disable_hw_debug - Disable hardware debugging while we in kgdb.
302 * @regs: Current &struct pt_regs.
303 *
304 * This function will be called if the particular architecture must
305 * disable hardware debugging while it is processing gdb packets or
306 * handling exception.
307 */
308 void kgdb_disable_hw_debug(struct pt_regs *regs)
309 {
310 /* Disable hardware debugging while we are in kgdb: */
311 set_debugreg(0UL, 7);
312 }
就正如注释所说的那样,对于某些体系结构kgdb在运行时需要设置禁止硬件调试,DR7寄存器就是断点控制寄存器,他的低半个字用来允许断点和允许所选择的调试条件,清0就是禁止硬件调试。
再次回到主题kgdb_handle_exception()
(kernel/kgdb.c)
1463 /*
1464 * Get the passive CPU lock which will hold all the non-primary
1465 * CPU in a spin state while the debugger is active
1466 */
1467 if (!kgdb_single_step) {
1468 for (i = 0; i < NR_CPUS; i++)
1469 atomic_set(&passive_cpu_wait[i], 1);
1470 }
在这里出来了一个十分重要的变量kgdb_single_step,名字告诉我们这个变量和单步调试有关,或者可以直接说当kgdb要进行单步调试时就会把这个变量设成1,这个条件有些让人费解,大家可以先想想为什么要有这样一个条件,我们在这里先忽略掉它,就当这里根本没有这个条件,这是可以的.在这节最后我们会对这个条件作更多的说明.
这里又一个用原子变量作为锁的例子,我们先把下面一段代码也贴出来:
(kernel/kgdb.c)
1472 /*
1473 * spin_lock code is good enough as a barrier so we don't
1474 * need one here:
1475 */
1476 atomic_set(&cpu_in_kgdb[ks->cpu], 1);
1477
1478 #ifdef CONFIG_SMP
1479 /* Signal the other CPUs to enter kgdb_wait() */
1480 if ((!kgdb_single_step) && kgdb_do_roundup)
1481 kgdb_roundup_cpus(flags);
1482 #endif
1483
1484 /*
1485 * Wait for the other CPUs to be notified and be waiting for us:
1486 */
1487 for_each_online_cpu(i) {
1488 while (!atomic_read(&cpu_in_kgdb[i]))
1489 cpu_relax();
1490 }
kgdb把正在执行kgdb代码的cpu叫做主cpu(primary cpu),其他的cpu叫从cpu(passive cpu).passive_cpu_wait[]和cpu_in_kgdb[]两个数组存的都是原子变量,都是每个cpu占一个元素。它们组合在一起又形成了一道锁,而这个锁锁的是从cpu,让他们进入忙等待的状态。具体看看它的实现过程。1469行把所有cpu的passive_cpu_wait[]都设成了1,从名字上就可以看出它的意思(passive:被动的),就是kgdb想让那个cpu进入等待状态,这里把自己也选上没关系吗?是的,没有关系,后面自然会明白。我们再跳到1481,kgdb_roundup_cpus()被调用,现在来看看这个函数:
(arch/x86/kernel/kgdb.c)
348 void kgdb_roundup_cpus(unsigned long flags)
349 {
350 send_IPI_allbutself(APIC_DM_NMI);
351 }
再找send_IPI_allbutself,发现一大堆的实现,什么bigsmp,es7000,numaq,summit。不过不用担心,其实它们所做的事情都是一样,只不过是硬件不同有不同的软件实现,有兴趣的同学可以在arch/x86/Kconfig中可以看到他们的说明。从名字我们可以猜出这个函数的作用:send发送,IPI(interprocessor interrupt)处理器间的中断,allbutself除了自己,那串起来就是向除了自己之外的cpu发中断,了解中断的同学应该不会对APIC(Advanced Programmable Interrupt Controller)陌生,APIC能够让一个cpu向另外一个发送一个中断。kgdb向所有从cpu(passive cpu)发送了一个NMI(不可屏蔽)中断,用来通知它们,kgdb要正式干活了,其它同志们暂时歇会,不要进来捣乱.那么从cpu在被NMI中断后有什么反应呢?让我们暂时离开主函数,在次回到kgdb在被触发时的情景.和int3一样,对于NMI,x86对NMI也设立了相关的处理函数do_nmi()(定义在arch/x86/kernel/traps_32.c).do_nmi()调用default_do_nmi(),它也会引起die_chain的注意。我们又回到前面的kgdb在die_chain中注册的处理函数__kgdb_notify()
(arch/x86/kernel/kgdb.c)
442 static int __kgdb_notify(struct die_args *args, unsigned long cmd)
443 {
444 struct pt_regs *regs = args->regs;
445
446 switch (cmd) {
447 case DIE_NMI:
448 if (atomic_read(&kgdb_active) != -1) {
449 /* KGDB CPU roundup */
450 kgdb_nmicallback(raw_smp_processor_id(), regs);
451 was_in_debug_nmi[raw_smp_processor_id()] = 1;
452 touch_nmi_watchdog();
453 return NOTIFY_STOP;
454 }
455 return NOTIFY_DONE;
456
457 case DIE_NMI_IPI:
458 /* Just ignore, we will handle the roundup on DIE_NMI. */
459 return NOTIFY_DONE;
注释也说了,主cpu发过来的中断由DIE_NMI来处理。在2.6.26上面DIE_NMI_IPI和DIE_NMI的处理是一样的.kgdb_nmicallback()会被调用.究竟kgdb用什么方法让从cpu停下来的呢?
(kernel/kgdb.c)
1536 int kgdb_nmicallback(int cpu, void *regs)
1537 {
1538 #ifdef CONFIG_SMP
1539 if (!atomic_read(&cpu_in_kgdb[cpu]) &&
1540 atomic_read(&kgdb_active) != cpu &&
1541 atomic_read(&cpu_in_kgdb[atomic_read(&kgdb_active)])) {
1542 kgdb_wait((struct pt_regs *)regs);
1543 return 0;
1544 }
1545 #endif
1546 return 1;
1547 }
看到名字我迫不及待地想看看kgdb_wait
(kernel/kgdb.c)
564 static void kgdb_wait(struct pt_regs *regs)
565 {
566 unsigned long flags;
567 int cpu;
568
569 local_irq_save(flags);
570 cpu = raw_smp_processor_id();
571 kgdb_info[cpu].debuggerinfo = regs;
572 kgdb_info[cpu].task = current;
573 /*
574 * Make sure the above info reaches the primary CPU before
575 * our cpu_in_kgdb[] flag setting does:
576 */
577 smp_wmb();
578 atomic_set(&cpu_in_kgdb[cpu], 1);
579
580 /* Wait till primary CPU is done with debugging */
581 while (atomic_read(&passive_cpu_wait[cpu]))
582 cpu_relax();
583
584 kgdb_info[cpu].debuggerinfo = NULL;
585 kgdb_info[cpu].task = NULL;
586
587 /* fix up hardware debug registers on local cpu */
588 if (arch_kgdb_ops.correct_hw_break)
589 arch_kgdb_ops.correct_hw_break();
590
591 /* Signal the primary CPU that we are done: */
592 atomic_set(&cpu_in_kgdb[cpu], 0);
593 touch_softlockup_watchdog();
594 clocksource_touch_watchdog();
595 local_irq_restore(flags);
596 }
代码都列出来了,估计大家也都看到这个锁是怎样让从cpu停止活动的,cpu_relax()前面已经提到过了,它可以转换成一条pause汇编.在这里小弟再总结一下:主cpu在kgdb的主函数中(kgdb_handle_exception)设置数组passive_cpu_wait[]中所有的元素(包括描述自己的那个元素),然后向除了自己以外的所有cpu发NMI IPI中断,因此所有的从cpu都会进入kgdb_wait(),进来后从cpu先设置cpu_in_kgdb[]中自己的那个元素,以通知主cpu,自己已经准备好了,然后反复对自己的那个passive_cpu_wait元素进行判断然后relax,而另外一方面,主cpu也正反复检查cpu_in_kgdb[]的元素,等待所有从cpu都进入等待状态.这样passive_cpu_wait让从cpu等待,cpu_in_kgdb让主cpu保证所有从cpu都进入了等待状态.
当然,有上锁就有解锁,在主函数中,当处理完远端gdb请求后退出时就会解锁:
(kernel/kgdb.c)
1513 if (!kgdb_single_step) {
1514 for (i = NR_CPUS-1; i >= 0; i--)
1515 atomic_set(&passive_cpu_wait[i], 0);
1516 /*
1517 * Wait till all the CPUs have quit
1518 * from the debugger.
1519 */
1520 for_each_online_cpu(i) {
1521 while (atomic_read(&cpu_in_kgdb[i]))
1522 cpu_relax();
1523 }
1524 }
一样的做法,只不过这次是解锁和等待所有从cpu离开kgdb_wait()回到日常生活.
好了,大家应该不会忘记前面我们提的一个问题,就是为什么要有kgdb_single_step这么一个判断,它在我们前面的代码中连续出现了3次.kgdb_single_step,就正如大家想的那样,当gdb向kgdb发送一个's'单步执行命令时这个变量的值就会变成1.那为什么在kgdb处于单步调试的情况下反而不去停止其它cpu的活动呢?
在邮件列表里面,一位仁兄04年写的一封信里给出了答案,道理很简单.如果想看原文,可以在邮件列表里面搜debugger_step这个关键字(在进内核树之前,kgdb_single_step就叫这个名字),在最老的几封信里面. 在2.6.27内核x86平台上,当gdb要求kgdb进行单步调试时,kgdb是会让所有从cpu都处于忙等待,只让被单步调试的那个cpu继续运行,一直到gdb过来一个'c'(continue)为止,才让从cpu们继续.这样我们再回头看看前面的代码,然后考虑这样一个情况:主cpu由于一个断点进入kgdb的代码,一开始,它并不是处于单步调试的状态,所以kgdb_single_step为0,这样kgdb把其它所有cpu都锁住,让它们等待,然后和远程的gdb通信,假如这时候gdb要求单步调试,kgdb就把kgdb_single_step设成1,然后让程序继续执行,在1513行,也是判断kgdb_single_step(用于解锁),这时候,kgdb_single_step已经不是0了,所以主cpu并没有帮其它从cpu解锁,所以主cpu在下一条指令完成后再次进入kgdb主函数,这时候,所有从cpu还在忙等待,所有没有必要在设置passive_cpu_wait[]和向它们发IPI NMI中断,不然从cpu为了响应这个NMI会在kgdb_wait里面再调用一次kgdb_wait,这正是之前kgdb解决过的一个bug.
主函数还剩下一点点了,我们把他看完:
(kernel/kgdb.c)
1492 /*
1493 * At this point the primary processor is completely
1494 * in the debugger and all secondary CPUs are quiescent
1495 */
1496 kgdb_post_primary_code(ks->linux_regs, ks->ex_vector, ks->err_code);
1497 kgdb_deactivate_sw_breakpoints();
1498 kgdb_single_step = 0;
1499 kgdb_contthread = current;
1500 exception_level = 0;
这些是在进入和远程gdb通信之前kgdb所做的最后的初始化,kgdb_post_primary_code的意思就是在主cpu在控制整个机器后,它还有什么事情需要做的,这是体系结构相关的,x86对kgdb_post_primary_code实现非常简单,就是保存了一下中断向量号和错误代码。在开始前我们先禁止所有断点,把内存还原成原来的样子。主函数在最后就是还原环境,中间夹着的是最重要的一行代码了,和gdb通信,执行他发来的命令。
(kernel/kgdb.c)
1502 /* Talk to debugger with gdbserial protocol */
1503 error = gdb_serial_stub(ks);
主函数这样就差不多了,有些同志可能注意到了kgdb_cpu_doing_single_step和前面passive_cpu_wait[]/cpu_in_kgdb[]形成的锁在功能上有些重叠了,前者是当有一个cpu在单步调试时,其它cpu不能进kgdb的代码.而后者则是当kgdb在处在单步调试时让其它cpu处在忙等待中.忙等待有怎么会进kgdb的代码呢?对于这个问题,我没有很确切的回答,如果大家知道就请指点指点小弟.这两段代码是一位大侠在一次提交中放到库里的。但是它们的目标都是一样,就是保护单步调试的cpu.
在离开之前,主函数在前面究竟做了什么事情?其实没干多少活,大部分时间都在控制竞态,包括单步调试,停止从cpu的活动等。
>>>>>>>>>3.gdb远程串行协议(GDB remote serial protocol)>>>>>>>>>
在研究kgdb命令具体执行过程之前,我们先来看看gdb和远程的kgdb是通过一种什么的方式通信的。在这里我们只做简略的介绍,详细说明可以在这里找到:http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/gdb/remote-protocol.html
这个现实世界中充满了各种各样的协议,很多事情都是遵循协议来做事,网络有TCP/IP,SPX/IPX, 电信有ISUP,PRI,SIP,就业,租房,离婚都有协议.gdb也不例外,gdb和远程目标之间用gdb远程串行协议(下面简称为RSP)来通信。
RSP是一种基于ASCII码的协议,以gdb发送命令,目标端(在这里是kgdb)返回执行结果或信息的方式来进行的,而命令和回复在传输过程中又是封装在一个包(packet)里面的.每个包以一个'$'开始,接着是实际数据,数据以一个'#'结束,后面跟着两位十六进制数字用作校验和($packet-data#checksum).校验和需要对256取模,因为只有两位。
当收到一个数据包,(无论是命令还是回复,无论是gdb端还是目标端)收到数据包的一方应该根据收到的数据和校验和检查这个包的合法性,如果是一个正常的包,应该返回一个'+',如果是一个损坏的包,则应该返回一个'-'。在包里面的数据,可以用',',';',':'三个符号来分隔成不同的段,他们的用法根据不同的命令不同而不同.
gdb提供了一个很方便的方式让我们把通信过程记录下来,在gdb里面使用"set remotelogfile [filepath]"指定你要记录的文件,但是这个文件一定要在开始记录前手动创建,下面一段是在sys_mount设置一个断点的log:
c info source
c b sys_mount
w $mc018ae77,1#fa
r +$55#6a
w +$mc018ae77,1#fa
r +$55#6a
w +$mc018ae77,1#fa
r +$55#6a
w +$mc018ae77,a#2a
r +$5589e5565383ec148d55#e6
w +$mc018ae77,1#fa
r +$55#6a
w +$mc018ae78,1#fb
r +$89#71
w +$mc018ae79,5#00
r +$e5565383ec#a0
w +$mc018ae79,5#00
r +$e5565383ec#a0
w +$mc018ae78,1#fb
r +$89#71
w +$mc018ae79,1#fc
r +$e5#9a
w +$mc018ae7a,1#24
r +$56#6b
w +$mc018ae7a,1#24
r +$56#6b
w +$mc018ae7b,1#25
r +$53#68
w +$mc018ae7c,1#26
r +$83#6b
w +$mc018ae7c,1#26
r +$83#6b
w +$mc018ae77,1#fa
r +$55#6a
w +
c info breakpoints
c c
w $Z0,c018ae7f,1#72
gdb规定目标端起码应该实现:'g' 'G' 'm' 'M' 'c' 's',kgdb当然也有实现了,而且还不止这些,有一些是和体系结构没有太紧密的关系,比如断点的设置和删除,kgdb只是记录起这个地址,和设置这个断点的状态;内存的读取和修改,也许这个和体系结构有关,但是具体实现内核已经搞定,kgdb不用关心。另外一方面一些比如's'单步执行这种命令就和体系结构关系十分密切,x86上是通过设置标志寄存器的Trap标记,让cpu运行完下一条指令后触发一个debug中断,其他的体系结构的做法都有所不同。
下面具体看看一些具体的命令:
'g'
格式:$g#67
描述:gdb向目标端发送这个命令来获取目标机当前寄存器的值
回复:+ $123456789abcdef0...#xx
'+'用来应答'g'这个命令,表明目标端正确地收到这个命令,然后就是目标端的回复包,gdb规定用8十六进制个数字来表示一个寄存器的值,所以第一个寄存器的值为12345678,第二个为9abcdef0,依此类推,而具体每个寄存器的含义和寄存器个数又体系结构决定,定义在gdb的代码中. 当然这里8个数字是对32位系统来说的,为什么是8位?限于我们这个协议是基于ASCII的,一个十六进制数只能标记4位,那32位自然是8个十六进制数了。
'G'
格式:$GXXXXXXXXXXX...#xx
描述:和g相反,这个命令用来设置目标机当前寄存器的值
回复: + $OK#9a
OK表示设置成功,后面我们会讲到不成功的情况.
'm'
格式:$m6a1bbb,2#b9
描述:读取一段内存的值,这里是读取以6a1bbb位起始地址的两个字节
回复: + $f488#0a 目标端把值返回.
'M'
格式:$Mccc5cc,2:a340#01
描述:设置一段内存的值,这里是把以ccc5cc位开始地址的两个字节设成a340
回复: + $OK#9a
's'
格式:$sADDR#xx
描述:用户进行单步调试时用到,ADDR指明了程序将从那个地址恢复运行,如果忽略ADDR,程序就从断点处继续运行.
回复:+ 目标端会马上返回数据正确或错误接收,但不会马上返回信息,具体信息要到下一次断点被触发时才会返回.下面会提到.
'c'
格式:$cADDR#xx
描述:让程序恢复正常运行
回复:和's'一样.
'Z'
格式: $ZTADDR,LENGTH#xx
'Z'命令用来设置断点或watch点,用过gdb的同志应该不会陌生了
'T'字段定义了这个命令的对象,0:软件断点,1:硬件断点,2:写watch点,3:读watch点,4:访问watch点.
'ADDR'就是我们所关心的内存地址,'LENGTH',对于软件中断它指明被断点指令覆盖的内存长度,kgdb对于软件断点忽略掉它,因为触发kgdb的指令与体系结构相关,已经定义在kgdb这边,就如x86的int3在内存里面的二进制指令为"0xcc";对于硬件断点和watch点,'LENGTH'指明gdb关注的内存长度.
'z'
格式: $zTADDR,LENGTH#xx
各项与'Z'相同.用来取消断点。
回复:
错误回复:
格式:+ $E01#a6
描述:如果目标端在执行gdb的命令时出错时返回错误回复,比如访问内存时出错.E后面根两位的错误码,错误码在gdb里面没有定义,没有定义其实更加方便,可以让开发端和目标端对错误码的使用带来灵活.
空回复:
格式:+ $#00
描述:当目标端不认识gdb发来的命令时,返回空回复表示自己不支持这个命令.
对'c','s'的回复:
有好几种对'c','s'的回复,其中比较常见的是'S'和'T'
'S'
格式: $SAA#b8
作用: AA表明触发这次通信的那个异常相关的信号,这个信号就是posix标准中的信号.
'T'
格式: $TAAN..:R..;N..:R..#xx
作用: AA同样是信号号, N..:R.. 这表明一个寄存器和它的值,N标记寄存器号,R是它对应的值,其中,如果N不是一个16进制数而是"thread",那么后面R的值就指明当前的进程号.如果是其它的字符串gdb会省略.
Kgdb在回复's','c'时选用了'T'的方式,不过在'T'消息里面只有thread一个字段,没有给gdb传更多的寄存器信息.
协议就在这里打住,有兴趣的同学可以继续深入研究各种高级用法.
顺便把gdb_serial_stub()开头的代码列出来:
(kernel/kgdb.c)
1215 /*
1216 * This function performs all gdbserial command procesing
1217 */
1218 static int gdb_serial_stub(struct kgdb_state *ks)
1219 {
1220 int error = 0;
1221 int tmp;
1222
1223 /* Clear the out buffer. */
1224 memset(remcom_out_buffer, 0, sizeof(remcom_out_buffer));
1225
1226 if (kgdb_connected) {
1227 unsigned char thref[8];
1228 char *ptr;
1229
1230 /* Reply to host that an exception has occurred */
1231 ptr = remcom_out_buffer;
1232 *ptr++ = 'T';
1233 ptr = pack_hex_byte(ptr, ks->signo);
1234 ptr += strlen(strcpy(ptr, "thread:"));
1235 int_to_threadref(thref, shadow_pid(current->pid));
1236 ptr = pack_threadid(ptr, thref);
1237 *ptr++ = ';';
1238 put_packet(remcom_out_buffer);
1239 }
1240
1241 kgdb_usethread = kgdb_info[ks->cpu].task;
1242 ks->kgdb_usethreadid = shadow_pid(kgdb_info[ks->cpu].task->pid);
1243 ks->pass_exception = 0;
实现传输的两个函数为put_packet()和get_packet()它们的实现十分简单直接,而传输时使用的是底层驱动的读写操作,这部分的实现和kgdb没有直接的关系,kgdb只是使用了现成的东西而已,这里就不罗嗦了.前面这段就是'T'回复.
>>>>>>>>>>4. 命令实现:>>>>>>>>>>
kgdb实现了众多的命令,这里不一一进行说明,有些实现十分简单,有些就需要仔细看看,不过其实很多工作gdb端已经完成了,到kgdb这边的都是很直接的,比如用户试图在gdb中打印一个局部变量的值,gdb通过和kgdb得到当前进程寄存器的信息,最终计算出要读的内存地址和长度,kgdb只要应gdb的要求提供相应的信息和设置相应的地址就够了,所以gdb是调试里面的主角.不过我们这里关心的还是kgdb,gdb的工作就忽略了.
我们不妨用一个gdb用户的角度来看看,大多数情况下我们只要:设置断点,让程序继续,单步调试,打印内存这些功能.对应gdb发给kgdb的命令并不就是'Z','c','s','m'命令,一般一个gdb的用户的命令都对上十几二十条kgdb命令的发送和接收.
4.1 断点
先从数据结构入手,kgdb用一个数组kgdb_break[]来描述软件断点(我们这里暂且只讨论软断点):
(kernel/kgdb.c)
107 /*
108 * Holds information about breakpoints in a kernel. These breakpoints are
109 * added and removed by gdb.
110 */
111 static struct kgdb_bkpt kgdb_break[KGDB_MAX_BREAKPOINTS] = {
112 [0 ... KGDB_MAX_BREAKPOINTS-1] = { .state = BP_UNDEFINED }
113 };
每个元素为kgdb_bkpt定义在
(include/linux/kgdb.h)
98 struct kgdb_bkpt {
99 unsigned long bpt_addr;
100 unsigned char saved_instr[BREAK_INSTR_SIZE];
101 enum kgdb_bptype type;
102 enum kgdb_bpstate state;
103 };
结构体里面的每个成员都很好理解,bpt_addr就是断点设置的地址,saved_instr[]放的是被断点指令所替换的真实指令.大家也知道一般调试器的实质就是在断点对于的内存地址中把原来的指令或部分指令替换成一条断点指令,对于x86就是"int 3",这条指令要能让系统进入被gdb控制的状态.type对应上面所说的'Z'命令的type字段,state标志每个断点的状态4中状态:
(include/linux/kgdb.h)
83 enum kgdb_bptype {
84 BP_BREAKPOINT = 0,
85 BP_HARDWARE_BREAKPOINT,
86 BP_WRITE_WATCHPOINT,
87 BP_READ_WATCHPOINT,
88 BP_ACCESS_WATCHPOINT
89 };
90
91 enum kgdb_bpstate {
92 BP_UNDEFINED = 0,
93 BP_REMOVED,
94 BP_SET,
95 BP_ACTIVE
96 };
在kgdb_break[]被初始化时state成员都被设置成BP_UNDEFINED,表明这个数组元素可用,当gdb设置一个断点时,一个数组元素会被设成BP_SET,相反取消断点时则把相应的数组项设成BP_REMOVED.当断点被激活时元素被设置为BP_ACTIVE.
几个断点状态的重要切换点:
1. BP_UNDEFINED/BP_REMOTED -> DB_SET 和 BP_SET -> BP_REMOVED分别发生在收到gdb的'Z'和'z'指令时.由函数kgdb_set_sw_break()和kgdb_remove_sw_break()实现
2. BP_SET -> DB_ACTIVE 一个断点处于BP_SET状态并没有生效,只是说明这个断点是有效的.当gdb发来's'或'c'命令恢复原来程序执行时才会让断点变成BP_ACTIVE.函数kgdb_activate_sw_breakpoints()实现了这个状态转换.
3. BP_ACTIVE -> BP_SET 发生在进入和gdb_serial_stub()之前,先把断点都失效在与远端的gdb通信,在讲主函数的时候几经提过.
4. 其它状态 -> DB_UNDEFINED 函数remove_all_break()完成这个功能,它主要是在远程gdb与kgdb断开链接时,即进行'D','k','C15'等命令时kgdb对所有断点进行重置.
下面具体看看kgdb是怎样设置断点的:
(kernel/kgdb.c)
616 /*
617 * SW breakpoint management:
618 */
619 static int kgdb_activate_sw_breakpoints(void)
620 {
621 unsigned long addr;
622 int error = 0;
623 int i;
624
625 for (i = 0; i < KGDB_MAX_BREAKPOINTS; i++) {
626 if (kgdb_break[i].state != BP_SET)
627 continue;
628
629 addr = kgdb_break[i].bpt_addr;
630 error = kgdb_arch_set_breakpoint(addr,
631 kgdb_break[i].saved_instr);
632 if (error)
633 return error;
634
635 kgdb_flush_swbreak_addr(addr);
636 kgdb_break[i].state = BP_ACTIVE;
637 }
638 return 0;
639 }
kgdb_arch_set_breakpoint()是一个和体系结构相关的函数,但是暂时在内核代码里面只有一个地方实现了,这里所做的就是读取断点地址对应的指令并保存起来,然后用一条可以触发断点的指令替换这段内存:
(kernel/kgdb.c)
167 /*
168 * Weak aliases for breakpoint management,
169 * can be overriden by architectures when needed:
170 */
171 int __weak kgdb_arch_set_breakpoint(unsigned long addr, char *saved_instr)
172 {
173 int err;
174
175 err = probe_kernel_read(saved_instr, (char *)addr, BREAK_INSTR_SIZE);
176 if (err)
177 return err;
178
179 return probe_kernel_write((char *)addr, arch_kgdb_ops.gdb_bpt_instr,
180 BREAK_INSTR_SIZE);
181 }
这里出来一个有趣的单词__weak,它是一个宏,和__packed是同一种东西都是gcc的扩展属性:
#define __packed __attribute__((packed))
#define __weak __attribute__((weak))
如果这个关键字用在函数定义上面,一般情况下和一般函数没有两样。但是当有一个同名函数但是不带__weak被定义时,所有对这个函数的调用都是指向后者(不带__weak那个),如果有两个一样的函数都用了__weak,那么真正调用那个,就要看连接器了。
设置断点的过程其实就是像大家想像的那样,把相应地址的指令读上来并保存起来,把一条断点指令写到这个地址上去.
关于断点的函数像kgdb_set_sw_break(),kgdb_remove_sw_break()这些函数的实现都是直接了当的,这里就不一一说明了.
4.2 continue 和 step
'c'(continue)和's'(step)命令就稍微复杂一点。
(kernel/kgdb.c) gdb_serial_stub()
1303 case 'c': /* Continue packet */
1304 case 's': /* Single step packet */
1305 if (kgdb_contthread && kgdb_contthread != current) {
1306 /* Can't switch threads in kgdb */
1307 error_packet(remcom_out_buffer, -EINVAL);
1308 break;
1309 }
1310 kgdb_activate_sw_breakpoints();
1311 /* Fall through to default processing */
1312 default:
1313 default_handle:
1314 error = kgdb_arch_handle_exception(ks->ex_vector,
1315 ks->signo,
1316 ks->err_code,
1317 remcom_in_buffer,
1318 remcom_out_buffer,
1319 ks->linux_regs);
1305行对变量kgdb_contthread进行判断,kgdb_contthread在正常情况下就是等于current,它是为gdb的'Hc'命令服务的,为了更好地了解这个变量,我们再看看这个命令在gdb在远程协议中的意义:
$HCT...#xx (set thread)
Set thread for subsequent operations (`m', `M', `g', `G', et.al.). C = `c' for thread used in step and continue; T... can be -1 for all threads. C = `g' for thread used in other operations. If zero, pick a thread, any thread.
我们这里只关心'Hc'命令,因为kgdb_contthread和'Hc'相关.上面的文档也说了,'H'命令是位下面的操作选择线程,而'Hc'这是针对step和continue这两个动作的.再看看邮件列表上面的同志是怎样说的:
> Before s command gdb sets thread to be stepped using Hc. An Hc0 packet
> indicates that all threads are to be resumed and current thread is to be
> single stepped. An Hc<thread id> indicates that only current thread is to be
> single stepped while holding other threads where they are.
在gdb发起单步调试命令之前都会先使用'Hc'命令对线程进行一些指定,'Hc0'代表让所有的线程都恢复工作,只有当前的这个线程继续被单步调试,如果是'Hc<thread id>'则代表只有当前线程可以跑,其它的都得等等.对于单步调试'Hc<thread id>'是最常见的.那问一个问题,gdb是怎样知道这个pid的?大家应该不会忘了上面在讲远程协议时关于's','c'的返回值吧,当后面一个断点被触发时它们的返回值才发回给gdb,而返回的内容就包含了这个pid('T'回复),告诉gdb现在断点是从那个线程里面触发的.kgdb在处理'Hc'命令时只是对这个pid进行合法检查,然后就把这个pid对应的task_struct找出来赋给了kgdb_contthread. 第1305行的这个检查是在看gdb要求这个pid是不是当前进程(也是kgdb报告上去的),如果不是就代表进程发生过切换,而在kgdb中这是不应该发生的,向gdb报告这个错误,然后等待下一个指令.
1310行激活所有断点,这是因为,'c','s'指令其实就意味着程序要往下走了.断点要归位,准备好下次的触发.
接着进入体系结构相关的'c','s'处理:
(arch/x86/kernel/kgdb.c)
354 /**
355 * kgdb_arch_handle_exception - Handle architecture specific GDB packets.
356 * @vector: The error vector of the exception that happened.
357 * @signo: The signal number of the exception that happened.
358 * @err_code: The error code of the exception that happened.
359 * @remcom_in_buffer: The buffer of the packet we have read.
360 * @remcom_out_buffer: The buffer of %BUFMAX bytes to write a packet into.
361 * @regs: The &struct pt_regs of the current process.
362 *
363 * This function MUST handle the 'c' and 's' command packets,
364 * as well packets to set / remove a hardware breakpoint, if used.
365 * If there are additional packets which the hardware needs to handle,
366 * they are handled here. The code should return -1 if it wants to
367 * process more packets, and a %0 or %1 if it wants to exit from the
368 * kgdb callback.
369 */
370 int kgdb_arch_handle_exception(int e_vector, int signo, int err_code,
371 char *remcomInBuffer, char *remcomOutBuffer,
372 struct pt_regs *linux_regs)
373 {
374 unsigned long addr;
375 unsigned long dr6;
376 char *ptr;
377 int newPC;
378
379 switch (remcomInBuffer[0]) {
380 case 'c':
381 case 's':
382 /* try to read optional parameter, pc unchanged if no parm */
383 ptr = &remcomInBuffer[1];
384 if (kgdb_hex2long(&ptr, &addr))
385 linux_regs->ip = addr;
386 case 'D':
387 case 'k':
388 newPC = linux_regs->ip;
389
390 /* clear the trace bit */
391 linux_regs->flags &= ~X86_EFLAGS_TF;
392 atomic_set(&kgdb_cpu_doing_single_step, -1);
393
394 /* set the trace bit if we're stepping */
395 if (remcomInBuffer[0] == 's') {
396 linux_regs->flags |= X86_EFLAGS_TF;
397 kgdb_single_step = 1;
398 atomic_set(&kgdb_cpu_doing_single_step,
399 raw_smp_processor_id());
400 }
401
402 get_debugreg(dr6, 6);
403 if (!(dr6 & 0x4000)) {
404 int breakno;
405
406 for (breakno = 0; breakno < 4; breakno++) {
407 if (dr6 & (1 << breakno) &&
408 breakinfo[breakno].type == 0) {
409 /* Set restore flag: */
410 linux_regs->flags |= X86_EFLAGS_RF;
411 break;
412 }
413 }
414 }
415 set_debugreg(0UL, 6);
416 kgdb_correct_hw_break();
417
418 return 0;
419 }
420
421 /* this means that we do not want to exit from the handler: */
422 return -1;
423 }
对应'c','s'命令的处理其实十分相近,都是先看看命令里面带没带地址参数,如果有就改变pc寄存器的值.x86需要清除标志寄存器里面的X86_EFLAGS_TF位和kgdb_cpu_doing_single_step,这两个东东前面都提到过了,X86_EFLAGS_TF是用来触发debug异常的,而kgdb_cpu_doing_single_step则是防止竞态(kgdb_handle_exception中).对应'c'命令这些清理是必要的.如果是's'命令396~399行则重新对这些变量和寄存器赋值,我们也终于看到老朋友kgdb_single_step和kgdb_cpu_doing_single_step被赋值了,而这个地方也是它们唯一一处被设置的地方(不包括清0操作).
下面又是一个调试寄存器,dr6.前面我们已经看到过dr7.dr6是什么意思呢?它是一个调试状态寄存器,当调试事件被触发时,它会告诉我们触发的原因. dr6里面只有7位被用到了(第0,1,2,3和13,14,15),其余的都保留.403行判断条件关心的是第14位,第14位用来表示调试事件是否因为单步调试即控制寄存器里面的TF位(X86_EFLAGS_TF)被值位而造成的.如果是就正常,把寄存器归0,以便下一次检测.如果不是单步调试造成的,再检查低4位,低4位分别代表4个硬件调试断点(x86的4个硬件断点),谁触发谁的那一位就被置成1,而breakinfo[]也是关于硬件断点的数据结构,这里我们只关心软件断点,所以就不多说了.对硬件断点有兴趣的同学可以在网上找找相关资料.
当一切正常,0被返回,然后kgdb对这个异常处理也结束了,所有东西又回到想以前一样.
到这里大家可能已经发现了一个问题,对于这几个关于's','c'命令的处理代码里面,并没有看到对pc寄存器(指令寄存器)的修改。如果这是真的话,被断点指令替换了的那部分指令不就没有执行而直接到下一条指令了?这绝对是影响运行结果的,大家都不会相信这是事实,那真相是怎样的呢?光在kgdb这边已经没有什么线索了,让我们换个角度看看gdb在处理用户的s和c(注意,前面也看到平均每条用户的指令就对应十多条gdb发给kgdb的指令).用上面提到的gdb提供的log来看这个问题不太好,因为gdb似乎对发给kgdb和从kgdb收到的命令和回复在log里面进行了修改,把它们的顺序重新排了一下,隐藏了真相。也没太好的办法,只好在gdb_serial_stub()里面加了三条printk语句,分别是kgdb收到的消息和kgdb发出去的消息,还有当kgdb处理完一次和gdb通信后打出一条消息表示表示。
下面一段是这样一种情况:在系统起来后用gdb在sys_mount(),sys_mknod()上面设置两个断点,然后让目标己继续。在目标机上mount一个设备,随便比如sda1,不一定要有效的命令只要能触发sys_mount()就够了,当然目标机这时也就停下来了。在开发机上的gdb执行's'(用户的)我们可以看到下面的输出:
Nov 21 16:45:41 localhost kernel: kgdb exit
Nov 21 16:46:09 localhost kernel: kgdb in: g
Nov 21 16:46:09 localhost kernel: kgdb out: 1500000058297c2300000000e887050898cff7c1b4cff7c10000edc00000000080ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb in: P8=7fae18c0
Nov 21 16:46:09 localhost kernel: kgdb out:
Nov 21 16:46:09 localhost kernel: kgdb in: G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000007fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000007fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: z0,c018ae7f,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: z0,c017dbaf,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfbc,4
Nov 21 16:46:09 localhost kernel: kgdb out: e8870508
Nov 21 16:46:09 localhost kernel: kgdb in: m80587e8,8
Nov 21 16:46:09 localhost kernel: kgdb out: 2f6465762f736461
Nov 21 16:46:09 localhost kernel: kgdb in: m80587f0,8
Nov 21 16:46:09 localhost kernel: kgdb out: 3100000011000000
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc0,4
Nov 21 16:46:09 localhost kernel: kgdb out: f8870508
Nov 21 16:46:09 localhost kernel: kgdb in: m80587f8,8
Nov 21 16:46:09 localhost kernel: kgdb out: 2f6d6e742f736461
Nov 21 16:46:09 localhost kernel: kgdb in: m8058800,8
Nov 21 16:46:09 localhost kernel: kgdb out: 312f000041000000
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc4,4
Nov 21 16:46:09 localhost kernel: kgdb out: 008a0508
Nov 21 16:46:09 localhost kernel: kgdb in: m8058a00,8
Nov 21 16:46:09 localhost kernel: kgdb out: 686673706c757300
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc8,4
Nov 21 16:46:09 localhost kernel: kgdb out: 0000edc0
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfcc,4
Nov 21 16:46:09 localhost kernel: kgdb out: 00000000
Nov 21 16:46:09 localhost kernel: kgdb in: Hc982
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: s
Nov 21 16:46:09 localhost kernel: kgdb exit
Nov 21 16:46:09 localhost kernel: kgdb in: g
Nov 21 16:46:09 localhost kernel: kgdb out: 1500000058297c23a4cff7c1e887050898cff7c1b4cff7c10000edc00000000082ae18c08203200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb in: Z0,c018ae7f,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: Z0,c017dbaf,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: s
Nov 21 16:46:09 localhost kernel: kgdb exit
这是's'(用户)命令的一部分,不过这部分已经足够说明问题了。"kgdb in"就是gdb发过来的命令,"kgdb out"自然就是回复,"kgdb exit"代表离开一次kgdb的代码,也就是完成一次和gdb的通信。(一次通信可以完成很多条命令)
我们把关注的指令按顺序摘出来:
kgdb in: g
kgdb out: 1500000058297c2300000000e887050898cff7c1b4cff7c10000edc00000000080ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
kgdb in: G
15000000 58297c23 00000000 e8870508 98cff7c1 b4cff7c1 0000edc0 00000000 7fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
kgdb out: OK
kgdb in: z0,c018ae7f,1
kgdb out: OK
kgdb in: z0,c017dbaf,1
kgdb out: OK
kgdb in: s
kgdb exit
kgdb in: Z0,c018ae7f,1
kgdb out: OK
kgdb in: Z0,c017dbaf,1
kgdb out: OK
kgdb in: s
kgdb exit
先轻轻回顾一下协议命令,'g',gdb向kgdb要寄存器的值;'G',gdb设置目标机的寄存器;'z'取消一个断点;'Z'设置一个断点;'s'单步执行.这里先指明地址c018ae7f是sys_mount的地址,c017dbaf是sys_mknod的地址。
故事一开始是sys_mount断点被触发,gdb先读取目标机的寄存器'g',然后gdb对这些值进行修改,和上面'g'的值对比,只修改了第65,66个16进制数,我们在讲协议的时候讲过gdb把每8个16进制数变成一个寄存器值,那我们按8拆分,发现只有第9个寄存器被修改了它的值从80ae18c0改为7fae18c0,7fae18c0这个数看上去很眼熟,是的它正是我们的sys_mount,只不过数字有点错位了.那我们一定会想这个寄存器是不是pc寄存器呢?没错就是它.在include/asm-x86/kgdb.h定义了每个寄存器的含义.
24 enum regnames {
25 GDB_AX, /* 0 */
26 GDB_CX, /* 1 */
27 GDB_DX, /* 2 */
28 GDB_BX, /* 3 */
29 GDB_SP, /* 4 */
30 GDB_BP, /* 5 */
31 GDB_SI, /* 6 */
32 GDB_DI, /* 7 */
33 GDB_PC, /* 8 also known as eip */
34 GDB_PS, /* 9 also known as eflags */
35 GDB_CS, /* 10 */
36 GDB_SS, /* 11 */
37 GDB_DS, /* 12 */
38 GDB_ES, /* 13 */
39 GDB_FS, /* 14 */
40 GDB_GS, /* 15 */
41 };
pc寄存器正是第9个.寄存器赋值函数在gdb_cmd_setregs()中实现,结构十分简单,这里就不罗嗦了.这里gdb把pc指针减了一,让cpu再次运行断点所在位置的指令.这里又会有另外一个问题,刚刚在gdb_serial_stub()中我们也看到了,如果是's'或'c'命令,kgdb会在恢复运行之前激活所有断点.这样断点不就又被触发吗?gdb肯定没那么傻B,在发's'命令之前,先发两个'z'命令把所有断点都清了,再单步调试,这样,kgdb上面都没有断点了,随便让它激活多少遍断点都没所谓了.而且's'单步调试马上又会把控制权交给kgdb.在执行完断点原来位置的那条指令后,马上又用两条'Z'把原来两个断点加上.然后继续单步调试.
总结一下这个过程:
1.gdb获得当前pc寄存器值,并把它减一.
2.取消所有断点.
3.单步调试原来断点位置的指令.
4.单步调试引起的debug中断让kgdb在次拿到控制权,kgdb再把所有断点加回去.
5.后面继续单步调试.
'c'命令的过程和's'的过程差不多,只是在第5步不会继续单步而是'c'继续执行.
一句话,gdb控制了整个过程.
查看变量的gdb操作其实也没太多好说的了,无非就变成一个或多个gdb的'm'命令,还是那句,gdb才是调试的主角,它做了大部分的事情.
>>>>>>>>>>5 初始化时机.>>>>>>>>>>
看完上面这些内容,难免有人会问,那kgdb在什么时候起来的呀?假如我们用"kgdboc=ttyS0,115200 kgdbwait"作为启动参数.就在 drivers/serial/kgdboc.c 中的module_init(init_kgdboc)里面.就这样完了吗?那内核究竟在启动的那个阶段对kgdb有反应?这里就扯远一点,暂时离开一下kgdb.我们从module_init开始,它的定义在include/linux/init.h
259 #define module_init(x) __initcall(x);
204 #define __initcall(fn) device_initcall(fn)
199 #define device_initcall(fn) __define_initcall("6",fn,6)
159 /* initcalls are now grouped by functionality into separate
160 * subsections. Ordering inside the subsections is determined
161 * by link order.
162 * For backwards compatibility, initcall() puts the call in
163 * the device init subsection.
164 *
165 * The `id' arg to __define_initcall() is needed so that multiple initcalls
166 * can point at the same handler without causing duplicate-symbol build errors.
167 */
168
169 #define __define_initcall(level,fn,id) \
170 static initcall_t __initcall_##fn##id __used \
171 __attribute__((__section__(".initcall" level ".init"))) = fn
(include/linux/init.h)
135 typedef int (*initcall_t)(void);
一个程序编译好之后有代码段,只读数据段,数据段等一些段,而__attribute__((__section__(...)))这个gcc的编译扩展属性,它能够把函数或数据放入指定名字的段中。
那__define_initcall这一团是什么意思呢?首先这是一个定义和初始化的语句,initcall_t是一个函数指针类型,所以这里定义一个静态的函数指针,它的名字是__initcall_##fn##id,"##"在宏里面是连接两个字符串,所以这个变量名是根据进来的参数名字不一样而不同.并且把传进来fn(我们这里就是init_kgdboc())赋给这个变量。最重要的是把这个变量放在".initcall" level ".init"段中,我们的level是6,所以定义的这个函数指针放在.initcall6.init这个段中。
连接器允许我们通过连接脚本修改默认段的起始位置,段的内容等很多东西,先来看看x86的脚本arch/x86/kernel/vmlinux_32.lds.S。从名字我们大概可以猜出我们上面的那些段对应于这个文件中的:
133 .initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {
134 __initcall_start = .;
135 INITCALLS
136 __initcall_end = .;
137 }
连接脚本里面,'.'代表当前位置,所以__initcall_start和__initcall_end分别代表.initcall.init段的开始位置和结束位置,这种东西可以理解为一个常量,后者我们下面马上会用到。
而INITCALLS则在include/asm-generic/vmlinux.lds.h里面定义:
363 #define INITCALLS \
364 *(.initcallearly.init) \
365 VMLINUX_SYMBOL(__early_initcall_end) = .; \
366 *(.initcall0.init) \
367 *(.initcall0s.init) \
368 *(.initcall1.init) \
369 *(.initcall1s.init) \
370 *(.initcall2.init) \
371 *(.initcall2s.init) \
372 *(.initcall3.init) \
373 *(.initcall3s.init) \
374 *(.initcall4.init) \
375 *(.initcall4s.init) \
376 *(.initcall5.init) \
377 *(.initcall5s.init) \
378 *(.initcallrootfs.init) \
379 *(.initcall6.init) \
380 *(.initcall6s.init) \
381 *(.initcall7.init) \
382 *(.initcall7s.init)
像__initcall_start和__initcall_end一样,__early_initcall_end的值也是段里面的一个位置。
上面两段话合起来就是说在连接内核时,把所有放在中间文件(输入文件)中的.initcallxx.init段里面的东西都连接到输出文件的.initcall.init段中。
所以我们刚才的那个函数指针最后会在目标文件的.initcall.init段中.那现在问题是谁会去管这个段? 它就是do_initcalls
(init/main.c)
749 static void __init do_initcalls(void)
750 {
751 initcall_t *call;
752
753 for (call = __early_initcall_end; call < __initcall_end; call++)
754 do_one_initcall(*call);
755
756 /* Make sure there is no pending stuff from the initcall sequence
*/
757 flush_scheduled_work();
758 }
do_initcalls()遍历一次放在__early_initcall_end和__initcall_end之间的函数指针,并依次调用它们指向的函数,当然包括我们的init_kgdboc()了。那do_initcalls()又是在那里被调用了呢?
start_kernel()
'->rest_init()
'->kernel_init()
'->do_basic_setup()
'->do_initcalls()
这样清楚了吧,do_initcalls是在init进程里面才被调用的。init_kgdboc()所做的事情由于本人知识有限,暂时说不出来,不过它所干的最重要的两件事情是:根据内核启动参数寻找对应的驱动和在初始化完成后直接执行一条asm("int $3")汇编手动触发一个中断,把控制权交给kgdb。
kgdb调试linux内核(针对2.6内核)
http://kgdb.linsyssoft.com/downloads.htm
下载相应的KGDB内核补丁。
从linux内核官方网站上下载对应的版本内核。对内核打补丁,打补丁时要根据KGDB的README和series文件说明,按顺序打。
软硬件准备
环境:
一台开发机developer(192.168.0.1com1),一台测试机target(192.168.0.2 com1),都预装redhat 9;一根串口线
测试串口线
物理连接好串口线后,使用一下命令进行测试,stty可以对串口参数进行设置
在developer上执行:
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
echo hello > /dev/ttyS0
在target上执行:
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
cat /dev/ttyS1
串口线没问题的话在target的屏幕上显示hello
=========== 通过串口 =============
在Kernel hacking配置项中将以下三项编译进内核
KGDB: Remote (serial) kernel debugging with gdb
KGDB: Thread analysis
KGDB: Console messages through gdb
*注意(1) Serial port number for KGDB 这个选项表示选择哪个串口,1表示ttyS1,0表示ttyS0。
使用scp进行将相关文件拷贝到target上(当然也可以使用其它的网络工具)
#scp arch/i386/boot/bzImage root@192.168.0.2:/boot/vmlinuz-2.4.23-kgdb
*在target机器上
编辑/etc/grub.conf文件,加入以下行:
#vi /etc/grub.conf
title Red Hat Linux (2.4.23-kgdb)
root (hd0,0)
kernel /boot/vmlinuz-2.4.23-kgdb ro root=/dev/hda1
#
重起机器
在developer上的刚编译的内核源代码目录下,使用gdb命令调试新的内核。
# gdb vmlinux
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0
如果成功的话,下面会出现进入断点的信息。
============ 通过网络 ============
变异内核的时候选择通过网络调试。
Method for KGDB communication (KGDB: On ethernet - in kernel)
grub的设置
kernel vmlinuz ro root=/dev/hda1 kgdboe=@192.168.0.2/,◎192.168.0.1/
可以参考README文件。
在developer上的刚编译的内核源代码目录下,使用gdb命令调试新的内核。
# gdb vmlinux
(gdb) target remote udp:192.168.0.2:6443
如果成功的话,下面会出现进入断点的信息。
后记:
如果只vmlinux文件拷贝到其他机器上,也可以调试,但看不到断点处的代码,因为vmlinux所在的目录下面没有源代码。而且它使用的是绝对路径,vmlinux所在的路径改变了,也会看不到源代码
4.6. 调试器和相关工具
调试模块的最后手段是使用调试器来单步调试代码, 查看变量值和机器寄存器. 这个方法费时, 应当尽量避免. 但是, 通过调试器获得的代码的细粒度视角有时是很有价值的.
在内核上使用一个交互式调试器是一个挑战. 内核代表系统中的所有进程运行在自己的地址空间. 结果, 用户空间调试器所提供的一些普通功能, 例如断点和单步, 在内核中更难得到. 本节中, 我们看一下几个调试内核的方法; 每个都有缺点和优点.
gdb 对于看系统内部是非常有用. 在这个级别精通调试器的使用要求对 gdb 命令有信心, 需要理解目标平台的汇编代码, 以及对应源码和优化的汇编码的能力.
调试器必须把内核作为一个应用程序来调用. 除了指定内核映象的文件名之外, 你需要在命令行提供一个核心文件的名子. 对于一个运行的内核, 核心文件是内核核心映象, /proc/kcore. 一个典型的 gdb 调用看来如下:
gdb /usr/src/linux/vmlinux /proc/kcore
第一个参数是非压缩的 ELF 内核可执行文件的名子, 不是 zImage 或者 bzImage 或者给启动环境特别编译的任何东东.
gdb 命令行的第二个参数是核心文件的名子. 如同任何 /proc 中的文件, /proc/kcore 是在被读的时候产生的. 当 read 系统调用在 /proc 文件系统中执行时, 它映射到一个数据产生函数,而不是一个数据获取函数; 我们已经在本章"使用 /proc 文件系统"一节中利用了这个特点. kcore 用来代表内核"可执行文件", 以一个核心文件的形式; 它是一个巨大的文件, 因为他代表整个的内核地址空间, 对应于所有的物理内存. 从 gdb 中, 你可查看内核变量,通过发出标准 gdb 命令. 例如, p jiffies 打印时钟的从启动到当前时间的嘀哒数.
当你从gdb打印数据, 内核仍然在运行, 各种数据项在不同时间有不同的值; 然而, gdb 通过缓存已经读取的数据来优化对核心文件的存取. 如果你试图再次查看 jiffies 变量, 你会得到和以前相同的答案. 缓存值来避免额外的磁盘存取对传统核心文件是正确的做法, 但是在使用一个"动态"核心映象时就不方便. 解决方法是任何时候你需要刷新 gdb 缓存时发出命令 core-file /proc/kcore; 调试器准备好使用新的核心文件并且丢弃任何旧信息. 然而, 你不会一直需要发出 core-file 在读取一个新数据时; gdb 读取核心以多个几KB的块的方式, 并且只缓存它已经引用的块.
gdb 通常提供的不少功能在你使用内核时不可用. 例如, gdb 不能修改内核数据; 它希望在操作内存前在它自己的控制下运行一个被调试的程序. 也不可能设置断点或观察点, 或者单步过内核函数.
注意, 为了给 gdb 符号信息, 你必须设置 CONFIG_DEBUG_INFO 来编译你的内核. 结果是一个很大的内核映象在磁盘上, 但是, 没有这个信息, 深入内核变量几乎不可能.
有了调试信息, 你可以知道很多内核内部的事情. gdb 愉快地打印出结构, 跟随指针, 等等. 而有一个事情比较难, 然而, 是检查 modules. 因为模块不是传递给gdb 的 vmlinux 映象, 调试器对它们一无所知. 幸运的是, 作为 2.6.7 内核, 有可能教给 gdb 需要如何检查可加载模块.
Linux 可加载模块是 ELF 格式的可执行映象; 这样, 它们被分成几个节. 一个典型的模块可能包含一打或更多节, 但是有 3 个典型的与一次调试会话相关:
.text
这个节包含有模块的可执行代码. 调试器必须知道在哪里以便能够给出回溯或者设置断点.( 这些操作都不相关, 当运行一个调试器在 /proc/kcore 上, 但是它们在使用 kgdb 时可能有用, 下面描述).
.bss
.data
这 2 个节持有模块的变量. 在编译时不初始化的任何变量在 .bss 中, 而那些要初始化的在 .data 里.
使 gdb 能够处理可加载模块需要通知调试器一个给定模块的节加载在哪里. 这个信息在 sysfs 中, 在 /sys/module 下. 例如, 在加载 scull 模块后, 目录 /sys/module/scull/sections 包含名子为 .text 的文件; 每个文件的内容是那个节的基地址.
我们现在该发出一个 gdb 命令来告诉它关于我们的模块. 我们需要的命令是 add-symble-flile; 这个命令使用模块目标文件名, .text 基地址作为参数, 以及一系列描述任何其他感兴趣的节安放在哪里的参数. 在深入位于 sysfs 的模块节数据后, 我们可以构建这样一个命令:
(gdb) add-symbol-file .../scull.ko 0xd0832000 -s .bss 0xd0837100 -s .data 0xd0836be0
我们已经包含了一个小脚本在例子代码里( gdbline ), 它为给定的模块可以创建这个命令.
我们现在使用 gdb 检查我们的可加载模块中的变量. 这是一个取自 scull 调试会话的快速例子:
(gdb) add-symbol-file scull.ko 0xd0832000 -s .bss 0xd0837100 -s .data 0xd0836be0
add symbol table from file "scull.ko" at
.text_addr = 0xd0832000
.bss_addr = 0xd0837100
.data_addr = 0xd0836be0
(y or n) y
Reading symbols from scull.ko...done.
(gdb) p scull_devices[0]
$1 = {data = 0xcfd66c50,
quantum = 4000,
qset = 1000,
size = 20881,
access_key = 0,
...}
这里我们看到第一个 scull 设备当前持有 20881 字节. 如果我们想, 我们可以跟随数据链, 或者查看其他任何感兴趣的模块中的东东.
这是另一个值得知道的有用技巧:
(gdb) print *(address)
这里, 填充 address 指向的一个 16 进制地址; 输出是对应那个地址的代码的文件和行号. 这个技术可能有用, 例如, 来找出一个函数指针真正指向哪里.
我们仍然不能进行典型的调试任务, 如设置断点或者修改数据; 为进行这些操作, 我们需要使用象 kdb( 下面描述 ) 或者 kgdb ( 我们马上就到 )这样的工具.
许多读者可能奇怪为什么内核没有建立更多高级的调试特性在里面.答案, 非常简单, 是 Linus 不相信交互式的调试器. 他担心它们会导致不好的修改, 这些修改给问题打了补丁而不是找到问题的真正原因. 因此, 没有内嵌的调试器.
其他内核开发者, 但是, 见到了交互式调试工具的一个临时使用. 一个这样的工具是 kdb 内嵌式内核调试器, 作为来自 oss.sgi.com 的一个非官方补丁. 要使用 kdb, 你必须获得这个补丁(确认获得一个匹配你的内核版本的版本), 应用它, 重建并重安装内核. 注意, 直到本书编写时, kdb 只在IA-32(x86)系统中运行(尽管一个给 IA-64 的版本在主线内核版本存在了一阵子, 在被去除之前.)
一旦你运行一个使能了kdb的内核, 有几个方法进入调试器. 在控制台上按下 Pause(或者 Break) 键启动调试器. kdb 在一个内核 oops 发生时或者命中一个断点时也启动, 在任何一种情况下, 你看到象这样的一个消息:
Entering kdb (0xc0347b80) on processor 0 due to Keyboard Entry
[0]kdb>
注意, 在kdb运行时内核停止任何东西. 在你调用 kdb 的系统中不应当运行其他东西; 特别, 你不应当打开网络 -- 除非, 当然, 你在调试一个网络驱动. 一般地以单用户模式启动系统是一个好主意, 如果你将使用 kdb.
作为一个例子, 考虑一个快速 scull 调试会话. 假设驱动已经加载, 我们可以这样告诉 kdb 在 sucll_read 中设置一个断点:
[0]kdb> bp scull_read
Instruction(i) BP #0 at 0xcd087c5dc (scull_read)
is enabled globally adjust 1
[0]kdb> go
bp 命令告诉 kdb 在下一次内核进入 scull_read 时停止. 你接着键入 go 来继续执行. 在将一些东西放入一个 scull 设备后, 我们可以试着通过在另一个终端的外壳下运行 cat 命令来读取它, 产生下面:
Instruction(i) breakpoint #0 at 0xd087c5dc (adjusted)
0xd087c5dc scull_read: int3
Entering kdb (current=0xcf09f890, pid 1575) on processor 0 due to
Breakpoint @ 0xd087c5dc
[0]kdb>
我们现在位于 scull_read 的开始. 为看到我们任何到那里的, 我们可以获得一个堆栈回溯:
[0]kdb> bt
ESP EIP Function (args)
0xcdbddf74 0xd087c5dc [scull]scull_read
0xcdbddf78 0xc0150718 vfs_read+0xb8
0xcdbddfa4 0xc01509c2 sys_read+0x42
0xcdbddfc4 0xc0103fcf syscall_call+0x7
[0]kdb>
kdb 试图打印出调用回溯中每个函数的参数. 然而, 它被编译器的优化技巧搞糊涂了. 因此, 它无法打印 scull_read 的参数.
到时候查看一些数据了. mds 命令操作数据; 我们可以查询 schull_devices 指针的值, 使用这样一个命令:
[0]kdb> mds scull_devices 1
0xd0880de8 cf36ac00 ....
这里我们要求一个(4字节)字, 起始于 scull_devices 的位置; 答案告诉我们的设备数组在地址 0xd0880de8; 第一个设备结构自己在 0xcf36ac00. 为查看那个设备结构, 我们需要使用这个地址:
[0]kdb> mds cf36ac00
0xcf36ac00 ce137dbc ....
0xcf36ac04 00000fa0 ....
0xcf36ac08 000003e8 ....
0xcf36ac0c 0000009b ....
0xcf36ac10 00000000 ....
0xcf36ac14 00000001 ....
0xcf36ac18 00000000 ....
0xcf36ac1c 00000001 ....
这里的 8 行对应于 scull_dev 结构的开始部分. 因此, 我们看到第一个设备的内存位于 0xce137dbc, quantum 是 4000 (16进制 fa0), 量子集大小是 1000 (16进制 3e8 ), 当前有 155( 16进制 9b) 字节存于设备中.
kdb 也可以改变数据. 假想我们要截短一些数据从设备中:
[0]kdb> mm cf26ac0c 0x50
0xcf26ac0c = 0x50
在设备上一个后续的 cat 会返回比之前少的数据.
kdb 有不少其他功能, 包括单步(指令, 不是 C 源码的一行), 在数据存取上设置断点, 反汇编代码, 步入链表, 存取寄存器数据, 还有更多. 在你应用了 kdb 补丁后, 一个完整的手册页集能够在你的源码树的 documentation/kdb 下发现.
4.6.3. kgdb 补丁
目前为止我们看到的 2 个交互式调试方法( 使用 gdb 于 /proc/kcore 和 kdb) 都缺乏应用程序开发者已经熟悉的那种环境. 如果有一个真正的内核调试器支持改变变量, 断点等特色, 不是很好?
确实, 有这样一个解决方案. 在本书编写时, 2 个分开的补丁在流通中, 它允许 gdb, 具备完全功能, 针对内核运行. 这 2 个补丁都称为 kgdb. 它们通过分开运行测试内核的系统和运行调试器的系统来工作; 这 2 个系统典型地是通过一个串口线连接起来. 因此, 开发者可以在稳定地桌面系统上运行 gdb, 而操作一个运行在专门测试的盒子中的内核. 这种方式建立 gdb 开始需要一些时间, 但是很快会得到回报,当一个难问题出现时.
这些补丁目前处于健壮的状态, 在某些点上可能被合并, 因此我们避免说太多, 除了它们在哪里以及它们的基本特色. 鼓励感兴趣的读者去看这些的当前状态.
第一个 kgdb 补丁当前在 -mm 内核树里 -- 补丁进入 2.6 主线的集结场. 补丁的这个版本支持 x86, SuperH, ia64, x86_64, 和 32位 PPC 体系. 除了通过串口操作的常用模式, 这个版本的 kgdb 可以通过一个局域网通讯. 使能以太网模式并且使用 kgdboe参数指定发出调试命令的 IP 地址来启动内核. 在 Documentation/i386/kgdb 下的文档描述了如何建立.[16]
作为一个选择, 你可使用位于 http://kgdb.sf.net 的kgdb补丁. 这个调试器的版本不支持网络通讯模式(尽管据说在开发中), 但是它确实有内嵌的使用可加载模块的支持. 它支持 x86, x86_64, PowerPC, 和 S/390 体系.