这里摘抄了网上一篇博客的一段话。
kgdb 实现了在开发机上使用 gdb 远程调试目标机的功能,包括命令处理、陷阱处理以及串口通信3个主要部分。
kgdb 会在 linux 内核中添加一个调试 stub ,调试 stub 是 linux 内核中的一小段代码,是运行 gdb 的开发机和目标机内核之间的媒介。
gdb 和调试 stub 之间通过 gdb 串行协议进行通信。 gdb 串行协议是一种基于消息的 ASCII 码协议,包含了各种调试命令。
当设置断点时, kgdb 将断点的指令替换为一条 trap 指令,当执行到断点时,控制权就转移到调试 stub 中去。
此时,调试 stub 的任务就是使用远程串行通信协议将当前内核环境传送给 gdb ,然后从 gdb 处接收命令。
gdb 命令告诉调试 stub 下一步该做什么,当调试 stub 收到继续执行的命令时,将恢复内核的运行环境,
把对 cpu 的控制权重新交还给内核。
总结几点:
先在 PC 上测试 kgdb 的搭建,了解 kgdb 相关原理。我们 PC 版本的 tcpnc 也可以利用 kgdb 进行源码层级的调试。
环境:开发机,Ubuntu 14.04 LTS;目标机,VMware上搭建的虚拟机 Ubuntu Server 14.04.05,内核版本为 4.4.24。
我们的 tcpnc Linux版是跑在 CentOS上的,不过大致调试流程也是一样。
图1是我的目标机上通过 uname -r
得到的内核版本号(你也意识到了这和我前面讲的 4.4.24 不同,4.4.24是我换过内核之后的版本号,上图是我目标机一开始的版本号,开机时可以选择加载哪个内核)。
图2是我在路径 /usr/src/linux-headers-4.4.0-31
下的Makefile中看到的内核版本号,与通过 uname -r
查到的不同,貌似这个才是对应 kernel.org
上的内核版本。Ubuntu会出现这种不一致的问题,需要小心。对于内核源码,建议通过源码顶层目录的Makefile确认一下。
不过,如果你编译过内核后,给目标机换了内核,就不存在上面的担忧了。通过换内核,确保了机器上面跑的内核和我们的源码一致,强烈建议这样做。
apt-get install build-essential
apt-get install libncurses5-dev
apt-get install dpkg-dev
apt-get source linux-image-$(uname -r)
下载完成后自动解压到当前目录。可以在源码顶层目录的Makefile看一下内核版本号。
配置内核主要是为了开启 kgdb 功能。进入下载好的内核源码目录。
make menuconfig
图3即为内核配置主界面。
以下建议勾选。
KGDB: kernel debugger配置完后如图4:
建议关闭的选项如下:
保存,退出。更改被写入.config文件。
修改内核顶层目录的Makefile文件,找到
ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
KBUILD_CFLAGS += -Os
else
ifdef CONFIG_PROFILE_ALL_BRANCHES
KBUILD_CFLAGS += -O2
else
KBUILD_CFLAGS += -O2
修改为
ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
KBUILD_CFLAGS += -Os
else
ifdef CONFIG_PROFILE_ALL_BRANCHES
KBUILD_CFLAGS += -O1
else
KBUILD_CFLAGS += -O1
-O2的优化会让我们的调试很麻烦。
在内核源码目录下。
make ##编译内核
make modules_install ##安装内核模块
make install ##安装内核二进制映像文件
以上步骤完成后,会在 /boot
目录下有如下文件。
- config-4.4.24
- initrd.img-4.4.24,根文件系统
- System.map-4.4.24,符号表
- vmlinuz-4.4.24,内核映像
同时,我们刚刚的动作还会修改 /boot/grub/grub.cfg
文件,grub.cfg文件和我们的开机启动项有关。打开grub.cfg
文件,我们可以看到,里面多了关于新内核 4.4.24 的开机启动项。如图5:
可以看到,默认启动项已经变为 4.4.24 内核了。
reboot
重启,登陆系统后, uname -r
看内核是否更换成功。
kgdb 的配置涉及到目标机和开发机。
我们这里涉及到串口通信,在VMware中,我们需要设置虚拟机(作为目标机)如何和宿主机(作为开发机)通过串口通信的。后文中,虚拟机和目标机是等价的,宿主机和开发机是等价的。注意操作是在开发机还是在目标机上做的。
打开虚拟机的设置页面,如图6。
点击最下面的 Add
按钮,添加串口设备,如图7。
选中 Serial Port
选项,然后 next
,选中 Output to socket
,如图8。
继续 next
,如图9。
在 Socket(named pipe)
处,你可以任意填一个文件,你需要有读写权限。图 9上面填的是 test.socket ,后面我们谈到的 Serial.socket 和它是一回事,注意分别。
这里我们也发现,虚拟机和宿主机是通过命名管道通信的,经典的 IPC 通信啊!
填的文件不需要事先创建,就算你创建了,以后开启虚拟机的时候也会提示你覆盖。
From: Server To: An Application
,这里也记得更改一下。
Application就是我们宿主机的 gdb 了,目标机作为通信的 Server 一方。点击 Finish,VMware端的设置完成。
为了后续的方便,我们还需要安装 minicom ,minicom是一个简单的串口通信的程序,类似我们 windows 的串口助手,我们这里使用minicom测试虚拟机的哪个 ttySN (N=0,1,2,…)和主机通信 。
sudo apt-get install minicom
然后在目录 /etc/minicom
下,创建 minicom 的配置文件 minirc.dfl 。添加如下内容:
pu port unix#/home/victor/vmware/serial.socket
你也发现了, #
号后面跟的是我们前面在VMware中创建的 Socket,所以此处你需要修改 #
号后面的内容,改为你自己创建的命名管道文件的路径。
在宿主机中的terminal中,开启 minicom 。如图10:
开启虚拟机,在虚拟机的terminal中测试到底是哪一个 ttySN (N=0,1,2,…)与宿主机通信。
echo 1 > /dev/ttyS0
echo 1 > /dev/ttyS1
echo 1 > /dev/ttyS2
...
如果在目标机执行 echo 1 > /dev/ttyS1
的时候,宿主机的 minicom 那边输出1,那么说明 ttyS1 是与宿主机通信的设备文件,以此类推,找到我们需要的那个 ttySN 。
测试完成后记得关闭minicom,切记!不然就一直占用文件,后面的步骤就会失败。
内核更换成功后,此新内核开启了 kgdb 的功能。剩下的就是如何配置 kgdb ,让我们可以在开发机上调试目标机的内核模块。
进入虚拟机,编辑启动项文件。
vi /boot/grub/grub.cfg
图 11是我已经改好的grub.cfg。
找到类似下面这行代码的内容:
linux /vmlinuz-4.4.24 root=/dev/mapper/ubuntu--vg-root ro quiet
更改为:
linux /vmlinuz-4.4.24 root=/dev/mapper/ubuntu--vg-root ro
quiet kgdboc=ttyS1,115200 kgdbwait
我们只添加了 quiet kgdboc=ttyS1,115200 kgdbwait
。
注意,你可能会在grub.cfg中发现有好多行代码和上面谈到的类似,但你只需要修改 menuentry
部分的那行代码,
后面的都是 submenuentry
的启动项代码。仔细观察一下,你会发现特征的。
上面修改的目的是让目标机在以4.4.24内核启动的时候,进入 kgdb 调试界面,等待开发机和目标机连接。连接的方式是串口,
波特率是115200,与串口相关的是目标机的 ttyS1 设备文件。 ttyS1 设备文件在目录 /dev
下。我们在 /dev
下可以看到很多
类似 ttyS1 这样的文件,如 ttyS0 、 ttyS2 、…。 我们在目标机中的 kgdb 就是通过这个设备文件来和开发机通信的。
你可能会问,为什么一定就是 ttyS1 呢?对,还真不一定是 ttyS1 ,不同人会不同,这里一定要注意!具体是 ttyS0 、 ttyS1 、或者是其他,通过我们前面讲 minicom 的部分确定。
还有, 记得把在虚拟机上编译好的内核源码拷贝到宿主机上 ,后面调试的时候会用到。
好了,现在你可以重启虚拟机了。关于 tty、pts、pty的相关概念看 这里。
重启虚拟机后,你会卡在如 图12 :
虚拟机这时候等待开发机和它连接。
我们还需要用到 socat,socat把虚拟机的串口输出传送到 pts 上。
sudo apt-get install socat
sudo socat -d -d /home/victor/vmware/serial.socket &
注意 socat 的参数,serial.socket 是我们前面在vmware中创建的命名管道文件。在命令后面添加 &
,是为了让 socat 进程一直在后台运行。如 图13 :
很重要! 关注 PTY is /dev/pts/14
这一句,记住 pts14
。
进入前面从虚拟机拷到宿主机的内核源码目录。
gdb ./vmlinux
进入 gdb 调试环境后
set serial baud 115200 ## 115200 是前面在设置目标机的时候确定的
target remote /dev/pts/14 ## pts 14是socat确定的
如 图14 :
好了,现在目标机已经在我们掌握了。试着在 gdb 调试环境中,敲 c
,发现虚拟机正常进入开机了吧。如 图15 :
总结一下:
利用 kgdb 调试的大概原理如 图16 。
我们给虚拟机添加了一个串口硬件设备,那么在虚拟机的 /dev/
目录中,自然就会有一个设备文件与之对应。这个设备文件一般以 ttySN (N=0,1,2,…)的形式存在。但我们知道这个串口是虚拟的,那么虚拟机的串口和谁通信呢?通过命名管道的方式和主机通信,也即文中提到的 Serial.socket 。
minicom 可以直接读写 Serial.socket 文件,所以我们可以通过 minicom 来测试我们给虚拟机添加的串口到底对应了虚拟机的哪个 ttySN ,这也可以通过我们配置 minicom 的过程了解大概。
minicom 的作用仅仅如此,测试完毕后,记得关闭 minicom,否则占用 Serial.socket 会导致后面的 socat 无法正常工作。
socat 的作用就是承担宿主机的某个 pts 和 虚拟机的某个 ttySN 的通信,然后我们的 gdb 通过 pts 和虚拟机进行串口通信。
回顾一下我们的配置过程,想想每步的目的。
其实做到前面部分已经可以调试内核代码了。图 15表示,目标机已经正常启动。
如果某时刻目标机内核崩溃,那么开发机中的 gdb 就会出现图 17中的结果。
在虚拟机中敲入下面两行代码,可以使虚拟机崩溃。
echo 1 > /proc/sys/kernel/sysrq
echo c > /proc/sysrq-trigger
在开发机的 gdb 中,你可以查看内核崩溃的点的相关信息。
但我们如何调试内核模块呢?以一个简单的内核模块 代码 为例。
在开发机中编译内核模块,将其拷入目标机中;如果是在目标机中编译的模块,则需要将模块代码及编好的模块拷到开发机中;在目标机中上模块,通过 cat /sys/module/main/sections/.text
获得模块在内核中的加载地址,暂定为 address 。图 18是我加载的模块 main.ko 在内核中的地址。
在虚拟机的终端中键入 echo g > /proc/sysrq-trigger
可以使虚拟机进入中断状态,我们才能在开发机上进行下一步操作。
在开发机中,如果你已经进入图 14的界面,在 gdb 中加载我们模块符号文件:
add-symbol-file /home/victor/raspberry/test-netfilter/main.ko address
address 为我们的模块在内核中的地址。如图 19。
现在就可以像 gdb 调试应用程序一样调试内核模块了,如设断点、查看变量值。
如图 20一样,我们在开发机的 gdb 中键入 b my_hookfn
,可以看到我们在模块代码函数 my_hookfn
的地方设置了一个断点。
然后我们继续键入 c
,让虚拟机内核跑起来。在虚拟机中 ping 一下主机,就会发现虚拟机代码进入了我们的模块,主机的 gdb 上显示我们设置的断点生效了。
图 20上也有显示。接下来就是 gdb 的常规调试命令了。如 21。
安装镜像详细步骤参见 这里。 考虑到内存卡只有 8G ,建议下载不带图形界面的 RASPIAN JESSIE LITE 版本镜像,解压得到我们需要的 .img 文件,然后给树莓派安装好系统,看是否能正常启动。默认登陆账户为 pi ,密码 raspberry。
由于 Jessie 版本镜像内核版本太高,我们需要替换掉 Jessie 的内核。Raspbian pi 2 B 所支持的最早的内核版本号为 3.18.16。
去树莓派官方 Github 下载 3.18.16 的内核。建议在Raspberry pi 上直接用 git 下载。但似乎需要自己更新一下 Raspberry pi 的源,编辑一下 /etc/apt/sources.list
,添加国内阿里源。sources.list 文件内容如下:
deb http://mirrors.aliyun.com/raspbian/raspbian/ jessie main non-free contrib
deb-src http://mirrors.aliyun.com/raspbian/raspbian/ jessie main non-free contrib
更新保存完毕后, update
一下,然后安装 git。
sudo apt-get update
sudo apt-get install git
安装完后,下载 3.18.16 的代码
git clone -b rpi-3.18.y --depth 1 https://github.com/raspberrypi/linux.git
不建议全部 git 下来,太大。也不建议下载 zip 文件解压,有时候会报错。
进入raspberry pi 源码目录,在顶层目录的Makefile中,找到如下:
ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
KBUILD_CFLAGS += -Os $(call cc-disable-warning,maybe-uninitialized,)
else
KBUILD_CFLAGS += -O2
endif
修改为
ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
KBUILD_CFLAGS += -Os $(call cc-disable-warning,maybe-uninitialized,)
else
KBUILD_CFLAGS += -O1
endif
目的是便于我们调试,-O2的优化,会让我们的调试很麻烦,而内核编译过程依赖于某些优化,-O0会导致编译失败。
编译替换内核的方法与先前在 PC 上的步骤类似,可以看这里的 官方教程 。需要注意的是,官方教程提供了两种方法,一种是交叉编译,就是说在我们的PC机上编译,然后把编译好的内核复制到 SD 上去;另一种是直接在树莓派上编译。推荐直接在 raspberry pi 上编译内核,时间比较长,3小时左右。还有, 注意官网上对于 raspberr pi 1 和 raspberry pi 2/3 的步骤不一样 。
很重要! 在编译内核之前,我们还需要配置内核编译选项,开启树莓派的 kgdb 功能。
在 pi 上利用 make menuconfig
配置内核,还需要 libncurses5-dev 包。
sudo apt-get install libncurses5-dev
进入树莓派的源码目录,
cd linux
KERNEL=kernel7
make bcm2709_defconfig
此为官网步骤,做完后,会在源码顶层目录生成 .config 文件。 检查 .config 文件的内容。
CONFIG_DEBUG_SECTION_MISMATCH=y
CONFIG_DEBUG_INFO=y
CONFIG_MAGIC_SYSRQ=y
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y
CONFIG_KDB_KEYBOARD=y
确保上述内容出现在文件 .config 中。配置过程可以 make menuconfig
,或者直接写 .config 文件。配置完后,再按照官网步骤继续进行。
替换内核成功后,重启 raspberry pi。 显示器可能没有反应了! 但是我们通过 SSH 可以连上 pi 。
解决方法:通过 SSH 修改 /boot/config.txt
文件,找到 hdmi_safe
那行,可以看到是被注释了的,在其下面添加如下内容
hdmi_safe=1
overscan_left=-30
overscan_right=-30
overscan_top=-30
overscan_bottom=-30
hdmi_group=2
hdmi_mode=4
hdmi_drive=2
config_hdmi_boost=4
存盘,退出,重启,显示器应该可以用了。 uname -r
查看内核是否替换成功。替换成功后,可以试着在 raspberry pi 编译我们的 test-filter 模块,看是否正常运行。
先去树莓派的官方 Github 上下载相关工具。此操作在开发机上做。
git clone https://github.com/raspberrypi/tools.git
在目录 arm-bcm2708/arm-bcm2708-linux-gnueabi/bin/
下,我们可以看到有很多以 arm-bcm2708-linux-gnueabi-
开头的文件,如图 22。
我们调试 X86 平台的代码用的是 gdb ,那我们调试 arm 平台的代码就得用 arm 平台的相关工具。与 gdb 对应的就是 arm-bcm2708-linux-gnueabi-gdb
。我们看到的 arm-bcm2708-linux-gnueabi-gcc
等其他的二进制程序就是用于在 X86 平台上编译出可以在 arm 平台上运行的程序,这在交叉编译中会用得到。
需要注意的是,这一整套交叉编译的工具都是 32 位程序。我们可以通过 file
命令查看,如图 23。
很重要! 必须在32位系统下运行 arm-bcm2708-linux-gnueabi-gdb ,否则很大概率会报错。你可以通过如下 command 来验证一下是否可以运行 arm-*-gdb。
ldd ./arm-bcm2708-linux-gnueabi-gdb
./arm-bcm2708-linux-gnueabi-gdb -v
ldd
那条指令用于查看 arm-*-gdb 的依赖库,你看是否有找不到的库,我当初就是 libpython2.7.so.1.0
死活找不到,但是我通过 locate libpython2.7.so.1.0
明明可以找到 libpython2.7.so.1.0
的位置,但还是报错。后来用 file 指令看了一下,才发现可能是 32 位的程序,无法用 64 位的库。图 24是我通过 ldd
查看的结果,可以发现,依赖的库都满足了。
图 25也显示我的 arm-*-gdb
正常运行了。
但是现在一般电脑都是64位吧,只能找变通方法了。
安装了一个 32 位的Ubuntu 虚拟机,在虚拟机上下载 python2.7 的源码,重新编译得到 32 位的库。然后把 32 位的库拷到开发机上。ldconfig 一下,然后你再看 arm-*-gdb是否正常运行。还是提示不行的话,请自行百度如何在 64 位Linux机器上运行 32 位程序,把相关的步骤照做一下。我当初就是安装了杂七杂八的一些东西,然后就好了。
编译 python 的时候,config 的时候,注意添加 –enable-shared 选项,如下:
./configure --enable-shared --prefix=/usr/local/python27
make
make install
添加了 –enable-shared 选项,才会编译出我们需要的 libpython2.7.so.1.0。
将 libpython2.7.so.1.0 拷入我们的主机的 /usr/local/lib 目录下。
cd /usr/local/lib
ln -s libpython2.7.so.1.0 libpython2.7.so
/sbin/ldconfig
/sbin/ldconfig -v
弄完后,看看 tools 目录下的 arm-*-gdb 是否可以正常运行,记得切换为 root 用户。
我的主机是 Ubuntu,如果是Windows的话,可以试着在虚拟机上调试 raspberry pi。安装一个 32 位的虚拟机,让虚拟机识别出串口就行。
进入 raspberry pi,修改 raspberry pi 的开机启动项。
sudo vi /boot/cmdline.txt
修改为如下内容:
dwc_otg.lpm_enable=0 dwc_otg.speed=1 debug console=ttyAMA0,115200
kgdboc=ttyAMA0,115200 kgdbwait console=tty1 root=/dev/mmcblk0p2
rootfstype=ext4 elevator=deadline rootwait
如果这时候你重启树莓派,那么它会卡在 gdb 界面,类似图 12,表示raspberry pi 等待 gdb 串口连接。
我们使用串口来调试 raspberry,那么如何将 raspberry 和 PC 连起来?
图 26为 raspberry pi 2 B的 GPIO 布局。注意 RXD
、 TXD
和 GND
那三个引脚,找到它们在板子上的位置。
图 27为一个USB转串口的模块。
将图 27的 RXD 和 raspberry 的 TXD连接,TXD和 raspberry 的 RXD连接,GND与raspberry 的GND连接。将USB2Serial模块插上笔记本,会发现在 /dev/
目录下出现了一个 ttyUSB0
的设备文件。
表示PC正确识别了USB2Serial模块。
后面的步骤与在PC上大致相同,就是图14处的步骤略有不同。
set serial baud 115200
target remote /dev/ttyUSB0
需要将 图14处的 target remote /dev/pts/14
修改为 target remote /dev/ttyUSB0
。
树莓派更换内核后,编译模块可能需要修改Makefile文件,如何修改,参看另一篇 module_makefile.pdf
。
测试模块 Makefile 如下:
### Makefile ---
## Author: victor@victor-hacks
## Version: $Id: Makefile,v 0.0 2016/11/18 14:27:02 victor Exp $
## Keywords:
## X-URL:
ifneq ($(KERNELRELEASE),)
obj-m += main.o
else
PWD := $(shell pwd)
# KVER := $(shell uname -r)
# KDIR := /lib/modules/$(KVER)/build
KDIR := /usr/src/linux-lts-xenial-4.4.0 ##换成你自己的内核源码目录
default:
$(MAKE) CFLAGS=-g-O1 -C $(KDIR) M=$(PWD) modules
all:
make CFLAGS=-g-O1 -C $(KDIR) M=$(PWD) modules
clean:
rm -rf *.o *.mod.c *.ko *.symvers *.order *.makers
endif
### Makefile ends here
测试模块代码如下:
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7
8 /**
9 * Hook function to be called.
10 * We modify the packet's src IP.
11 */
12 unsigned int my_hookfn(unsigned int hooknum,
13 struct sk_buff *skb,
14 const struct net_device *in,
15 const struct net_device *out,
16 int (*okfn)(struct sk_buff *))
17 {
18 struct iphdr *iph;
19 iph = ip_hdr(skb);
20
21 /* log the original src IP */
22 printk(KERN_INFO"src IP %pI4\n", &iph->saddr);
23
24 /* modify the packet's src IP */
25 /* iph->saddr = in_aton("8.8.8.8"); */
26
27 return NF_ACCEPT;
28 }
29
30 /* A netfilter instance to use */
31 static struct nf_hook_ops nfho = {
32 .list = {NULL,NULL},
33 .hook = my_hookfn,
34 .pf = PF_INET,
35 .hooknum = NF_INET_PRE_ROUTING,
36 .priority = NF_IP_PRI_FIRST,
37 /* .owner = THIS_MODULE, */
38 };
39
40 static int __init sknf_init(void)
41 {
42 if (nf_register_hook(&nfho)) {
43 printk(KERN_ERR"nf_register_hook() failed\n");
44 return -1;
45 }
46 return 0;
47 }
48
49 static void __exit sknf_exit(void)
50 {
51 nf_unregister_hook(&nfho);
52 }
53
54 module_init(sknf_init);
55 module_exit(sknf_exit);
56 MODULE_AUTHOR("zhangsk");
57 MODULE_LICENSE("GPL");