引言:kgdb是Linux内核提供的用于调试内核的源码级调试工具,支持断点设置,单步调试等源码调试常用功能,类似于在用户空间用gdb调试应用程序。内核调试工具还有另一款kdb,kdb更像一个shell工具,可以打印一些变量及寄存器的值,本文章仅针对kgdb的使用进行讲解。中文网站上能找到一些kgdb的使用教程,但基本上都是针对于两台Linux(通常一台是虚拟机)进行讲解,主要目的用于调试内核,或者梳理内核源码之用。本文章针对于嵌入式linux调试驱动讲解,调试主机为ubuntu虚拟机,目标机为正点原子的imx6ull开发板,驱动源码以正点原子的imx6ull驱动源码为例进行讲解,梳理如何使用kgdb调试.ko驱动模块。
一、基础信息。
kgdb是Linux内核在2.6.26版本之后提供的内核调试工具,之前的内核版本如要使用,需要打补丁,比较繁琐,所以要使用kgdb,请确保你的内核版本高于该版本。有关kgdb的基础信息,Linux官网给了较为详尽的说明,有兴趣的可以自己看一下。https://www.kernel.org/doc/html/v4.15/dev-tools/kgdb.html
kgdb从形式上来说类似于gdb server,你需要两台设备,一台主机,用于运行普通的gdb程序,一台被调试设备,需要安装所需调试的内核或者驱动,同时运行kgdb。kgdb与主机通过串口通讯,所以要在内核的启动参数里指定kgdb所需使用的串口设备号。内核也支持使用网口进行调试,但是我没有看到过成功的案例,我自己也没试,Linux官方的文档也说大部分架构通过串口调试,估计是网口的兼容性做的不好。设置好内核和内核参数后,启动内核,通过特殊的字符串触发kgdb,接下来就可以在主机上进行内核调试,调试方法与GDB调试应用程序相似。下面带着大家走一遍流程,并告诉大家那些我踩过的坑。
二、设置内核。
调试内核,需要在内核设置中设置一些选型。主要有如下几点。
1、CONFIG_KGDB=y 该选项可以通过make menuconfig配置,在如下位置:Kernel hacking —>KGDB:kernel debugger
2、CONFIG_DEBUG_INFO = y 通过 menuconfig 的 Kernel hacking —>Compile-time checks and compiler options—>Compile the kernel with debug info 该选型用于生成内核调试信息。
3、CONFIG_FRAME_POINTER=y 该选项适用于适合堆栈回溯技术的架构,imx6ull并不支持,所以nxp的linux内核里找不到该选项。
4、 # CONFIG_STRICT_KERNEL_RWX is not set 屏蔽掉该选型(如果有),该选型用于设置内核部分内存只读状态,如果设置为y,会导致在调试时无法设置断点。imx6ull同样无该选型。
综上,需在menuconfig中设置选项或直接在内核源码根目录的.config文件中直接更改设置项。
设置完成后,重新编译内核。
三、设置kgdboc参数
kgdboc 是kgdb over consle的缩写,Linux内核使用它作为kgdb串口驱动,所以需要给它传递一些参数。我们的嵌入式Linux,通过uboot传递即可,开机启动,停在uboot界面,设置bootargs参数如下:
setenv bootargs 'console=ttymxc0,115200 kgdboc=ttymxc0,115200 kgdbwait root=/dev/nfs nfsroot=192.168.1.103:/home/henryzu/linux/nfs/rootfs,proto=tcp rw ip=192.168.1.251:192.168.1.103:192.168.1.1:255.255.255.0::eth0:off'
重点是 kgdboc=ttymxc0,115200 kgdbwait,这是kgdboc的启动参数,其他参数为我的Linux内核rootfs加载地址,大家根据自己情况设置。ttymxc0是所需使用的串口设备名,115200代表波特率,kgdbwait 是一个启动参数,如果添加了这个参数,内核将在启动的时候在某个地方停下来,其实可以理解为在内核起始处添加了一个断点,方便主机的gdb软件连接。也可以选择不添加该选项。
四、设置主机的串口
接下来需要设置主机的串口。我是使用虚拟机的Ubuntu系统作为调试主机,以该情况为例讲解。
首先下载minicom,打开终端,输入:
sudo apt-get install minicom
等待软件安装完成。点击虚拟机右下角,将连接在Windows的串口,连接到虚拟机上。
左键点击选择,连接到主机。然后在终端输入如下指令:
ls /dev/tty*
会发现,多了一个ttyusb设备
我的是ttyUSB0,这就是刚连接的串口设备。
打开minicom
sudo minicom -s
进入minicom设置选项
按方向键,选择 串口设置选项
如图,将设备设置为刚刚的ttyusb设备,其他选项常规设置。重启minicom,复位一下开发板,可以看到uboot的启动信息,串口就设置好了。因为刚刚设置了bootargs的参数,添加了kgdbwait 选项,所以内核会停下来,如图
这代表kgdb已经启动成功了,正在等待远端gdb连接。
五、调试内核
现在,强行关掉该终端,切换到Linux源码根目录下。输入如下指令:
arm-linux-gnueabihf-gdb ./vmlinux
解释一下 arm-linux-gnueabihf-gdb 是32位arm专用的gdb,跟随 arm-linux-gnueabihf-gcc一起打包下载的,详情可以查看正点原子《linux驱动指南》交叉编译器章节。vmlinux是编译内核后,生成的专用于调试的符号文件。
输入以上指令后,gdb启动,并切换到gdb命令行状态,如图
此时输入如下指令
set serial baud 115200
接着输入
target remote /dev/ttyUSB0
这时候板子给gdb回复信息,如图
可以看到,内核此时停在 arch_kgdb_breakpoint()这个位置。如此,我们的环境搭建成功了。
这里还有几个小问题。
1、一些教程设置gdb波特率的命令是 remotebaud ,但这个命令无法识别,因为GDB在7.2版本后已经更新了这个命令,新的命令为:set serial baud xxxxx
2、可能会出现无法打开/dev/ttyUSB0,这是因为权限问题,输入
sudo chmod 777 /dev/ttyUSB0
即可解决。
六、调试驱动。
内核可以调试了,但一般我们不会去调试内核,我们需要的是调试驱动,调试我们自己写的.ko模块,像调试单片机一样,可以在驱动运转的时候,停下来,查看变量信息,寄存器信息,这会给我们开发驱动debug带来很多方便。但相信大家应该已经发现了kgdb的一个问题。它和板子的Linux终端共用一个串口,所以刚刚在第五步刚开始时,需要强行关掉信息输出终端,因为kgdb还在等着用这个串口了。因为一般板子只有一个串口,这就给调试带来了很大的麻烦,因为一般驱动是要应用程序调用的,也就是说我在调试时可能需要不停的在kgdb和终端之间来回切换,因为经常需要运行程序,停下程序,甚至输入一些参数。而且在实际的过程当中,切换也会有问题,笔者就发现,当板子上程序继续运行,强行关掉gdb窗口后,即便打开minicom,也无法输入终端信息,要关掉重开好几次才可以。我在一开始使用的时候,也是百思不得其解,为什么内核开发者会做出这么反人类的设计。后来我再官网看到了这样的提示:
好吧,看来他们自己也意识到了这个问题。总而言之,需要一个工具。这个工具叫 agent-proxy
源码地址在这里https://git.kernel.org/cgit/utils/kernel/kgdb/agent-proxy.git/
在终端输入如下指令:
git clone https://git.kernel.org/cgit/utils/kernel/kgdb/agent-proxy.git/
下载源码后,在源码目录下打开终端,输入make,编译软件,将生成的程序文件拷贝到 /bin 目录下。
接下来开始调试驱动。
驱动我选择了正点原子教程里的蜂鸣器实验驱动,注意,如果之前编译过该驱动模块,重新编译内核后,驱动模块也需要重新编译,否则会出错!!!驱动的具体安装和编写可参看正点原子教程。
重启开发板,因为之前设置了kgdbwait,所以内核会停下,这里可以把kgdbwait取消,重新启动内核。
打开ubuntu终端,输入如下命令:
agent-proxy 5550^5551 0 /dev/ttyUSB0,115200
这里使用刚刚安装的软件将ttyUSB0这个端口复用为两个 telnet端口
可以看到,agent-proxy已经启动了,注意,这个终端窗口不能关。
打开新的终端窗口,输入:
telnet localhost 5550
如图,可以看到已经成功的连接到开发板的终端
接下来,打开驱动文件所在的rootfs文件夹,用insmod命令安装beep.ko驱动。然后输入如下指令
cat / sys/module/beep/sections/.text
这里要讲一下,为什么要获取这个.text信息。因为一开始的时候,笔者直接用beep.ko文件调试,发现无法设置断点,去搜索了半天,发现可能是nokaslr的问题,具体什么意思大家可以搜索下,所以需要给gdb指定文件的装载位置,要获取这个信息。
接着输入:
echo g > /proc/sysrq-trigger
这个指令是触发kgdb运行的,输入该指令后,内核就会停下,等待远端gdb连接。
接下来,在ubuntu打开新的终端,进入驱动源代码所在目录(之前的输出终端也不要关),输入如下指令:
arm-linux-gnueabihf-gdb
启动gdb。
进入gdb命令行,输入
add-symbol-file beep.ko 0x7f000000
用add-symbol-file这个命令指定在板子中beep.ko的装载位置。
输入y,确认
接着输入
tar rem localhost:5551
可以看到,这是刚刚从ttyUSB0上复用出来的另一个端口。
此时的gdb终端窗口
可以看到,已经与kgdb成功建立连接,此时再看一下刚刚的输出终端窗口
一片乱码,笔者猜想这是因为二者协议不同,总之,现在不用来回切换了。现在开始调试,在gdb窗口输入l按回车,显示beep.ko代码
我们在61行这里打个断点,这个函数是驱动的输入函数,向驱动写入值的时候会调用该函数,beepstat就是拷贝用户程序传过来的指定值,我们看一下,等下调用驱动时,程序会不会在这里停下,然后能不能查看beepstat这个变量的值。
输入
b 61
接着输入 c 让程序继续运行。
这时可以看到,刚刚的终端窗口已经恢复正常了
这时候我们在板子终端运行beep的应用程序,该程序会调用beep.ko,此时我们可以看到,gdb的终端窗口,程序已经停下来了,且就停在我们刚刚打断点的位置
我们来查看一下beepstat的值
p beepstat
beepstat 的值是1,正是刚刚在板子beep应用程序输入的值。
我们再尝试下修改它的值,让蜂鸣器不要响,输入:
set variable beepstat = 0
回车,然后输入 c 让程序继续运行,蜂鸣器没有响,证明这个变量的值已经在断点处被成功更改了。除此之外,还可以查看寄存器的值,单步执行等,基本实现了与JTAG调试类似的效果,在这里就不再演示了。
七、总结
内核或驱动调试的方法非常多,这里有一个很好的总结。
https://blog.csdn.net/u014379540/article/details/52692103
作者讲解了驱动调试的常用方法,可以参考。
还有这篇文章,也提供了很好的演示
https://www.daimajiaoliu.com/daima/4ed23055b9003ec
当然,官方的文档最为详细,文章开始处已经贴了地址。官方文档除了基本的设置常见的问题外,还提供了实现kgdb的重要API接口,给志在为其他架构开发kgdb底层的内核开发人员作为参考。
有一本书,叫《软件调试》,系统的讲解了软件调试的相关技术,可以作为深入研究。
另外,在youtube上,有一个谷歌技术人员的技术讲座,他详细的介绍了kgdb的常见使用方法。
https://www.youtube.com/watch?v=HBOwoSyRmys 他是chrome os的开发人员。
欢迎转载,转载请注明出处并联系作者,谢谢。另,能力有限,不足和错误之处还请高手指出。