0 引言
上一篇文章提到了GDB/GDBServer的调试方法,这种方法无法对驱动模块.ko文件进行调试,对于驱动模块的开发我们需要用到KGDB,本人在配置及使用KGDB时遇到很多问题,虽然KGDB对于驱动模块的开发很重要,但网上的资料大都不涉及具体步骤,本文将仔细记录该过程以供大家参考,内容虽以king3399为目标板进行配置及测试,但流程适用于rk/rp官方SDK
1 内核(kernel)配置KGDB
这里先梳理KGDB配置及调试的大致流程以便有一个宏观的了解:
- 在内核(kernel)中开启KGDB相应配置开关
- 编译内核(kernel)
- 在设备树中传入KGDB相应参数
- 重新编译内核(kernel),并将生成的boot.img文件烧录
- 交叉编译带有调试信息的驱动模块文件,并将驱动模块文件传至板子
- 板子上电,加载模块触发调试等待主机连接
- 主机通过调试串口连接板子并开始调试
rk/rp关于KGDB这部分的官方资料几乎没有,如果还在试图寻找的不用再浪费时间了!!!
,下边是具体过程,郑重提示:保险起见,任何文件修改前请做好备份,做好修改记录以及虚拟机的快照备份!!!
需要注意的是,下边提到的文件及路径应根据手中的板子决定,不可照抄照搬,即使使用的是同一颗芯片(rk3399),如果板子的型号不同(king3399)或者系统不同,那么涉及的文件也大概率不同!
-
查看板子所用配置文件
在...sdk/下执行 ./build.sh kernel
如果之前没对kernel做其他的修改该编译过程将持续大约2分钟,从打印日志可提取如下信息:
-
RK_DEFCONFIG=/home/username/sdk/device/rockchip/.chips/rk3399/king-rk3399-ubuntu_defconfig
该文件提到了板子所用设备树的相对目录及文件名,
RK_KERNEL_DTS_NAME="rk3399/king-rk3399"
绝对路径为:/home/username/sdk/kernel/arch/arm64/boot/dts/rockchip/rk3399/
-
K_KERNEL_CFG=
rockchip_linux_defconfig
该文件为板子的内核配置文件
绝对路径为 /home/username/sdk/kernel/arch/arm64/configs/
这两个文件是主要配置文件,找不到这两个文件将无法完成本文后续操作
-
-
修改kernel配置
-
备份需要修改的文件
bash# 备份板子的默认配置文件 cd ~/sdk/kernel/arch/arm64/configs cp rockchip_linux_defconfig rockchip_linux_defconfig.20250421.00.bak # 备份kernel默认配置文件 cd ~/sdk/kernel cp .config .config.20250421.00.bak # 备份修改kernel前的boot.img文件 cd ~/sdk/kernel cp boot.img boot.img.20250421.00.bak # 备份板子设备树文件(king3399 底板)编译后的.dtb文件 cd ~/sdk/kernel/arch/arm64/boot/dts/rockchip/rk3399 cp king-rk3399.dtb king-rk3399.dtb.20250421.00.bak # 备份板子设备树文件(rp 层级) cd ~/sdk/kernel/arch/arm64/boot/dts/rockchip/rk3399 cp rp-rk3399-board.dtsi rp-rk3399-board.dtsi.20250421.00.bak # 备份板子设备树文件(rk 层级) cd ~/sdk/kernel/arch/arm64/boot/dts/rockchip cp rk3399-linux.dtsi rk3399-linux.dtsi.20250421.00.bak # 备份板子设备树文件(king 层级) cd ~/sdk/kernel/arch/arm64/boot/dts/rockchip/rk3399 cp king-rk3399.dts king-rk3399.dts.20250421.00.bak
备份主要是为了后边可能会进行对比/恢复,因为
修改kernel的配置后,编译一次大约需要花费60分钟!!!
,另外,编译前的部分文件我们在后边可能还会重新用到 -
利用make menuconfig修改kernel配置
这部分内容参考脚注【a】
bashcd ~/sdk/kernel make menuconfig KCONFIG_CONFIG=arch/arm64/configs/rockchip_linux_defconfig ARCH=arm64
通过界面进入到如下目录并做下述修改
Kernel hacking > Generic Kernel Debugging Instruments
Kernel hacking > Generic Kernel Debugging Instruments > KGDB: kernel debugger
Kernel hacking > Compile-time checks and compiler options
修改完成后,选择右下角 Save 进行保存,注意不要保存到原路径(这里的原路径指的是 rockchip_linux_defconfig 所在的路径),而是保存到 .config(~/sdk/kernel/ 下,这也就是前文为何将.config备份的原因)
使用下述命令来保存 defconfig 文件并覆盖原来的配置文件
bash# 保存 defconfig 文件 make savedefconfig ARCH=arm64 # 覆盖原来的配置文件(这也就是前文为何将rockchip_linux_defconfig备份的原因) cp defconfig arch/arm64/configs/rockchip_linux_defconfig
可利用对比软件对比修改前后的rockchip_linux_defconfig,通过对比会发现部分配置位置与备份不同,这并不影响,重要的是多了如下两个配置
CONFIG_KGDB=y
CONFIG_KGDB_KDB=y
-
编译修改后的kernel,该过程持续约60分钟!!!
bashcd ~/sdk ./build.sh kernel
完成后会在 ~/sdk/kernel下生成新的boot.img,但这并不是最终烧录的boot.img文件,到这,已经完成kernel的相关配置工作
-
-
修改设备树,传入KGDB启动参数
-
指定系统的标准输出(stdout)设备路径
bashcd ~/sdk/kernel/arch/arm64/boot/dts/rockchip/rk3399 vim rp-rk3399-board.dtsi # 修改如下 chosen { // stdout-path = "serial2:1500000n8"; // <--- 修改项 stdout-path = "serial2:115200n8"; }; &fiq_debugger { rockchip,serial-id = <2>; rockchip,wake-irq = <0>; rockchip,irq-mode-enable = <1>; /* If enable uart uses irq instead of fiq */ rockchip,baudrate = <115200>; /* Only 115200 and 1500000 */ <--- 核对项 interrupts = <GIC_SPI 150 IRQ_TYPE_LEVEL_HIGH 0>; pinctrl-names = "default"; pinctrl-0 = <&uart2c_xfer>; };
这个文件中只修改了
stdout-path
属性,另外还需要注意rockchip,baudrate
属性,这里需要核对一下,看是否是115200stdout-path:告诉内核应该将控制台输出(如printk、早期启动信息等)发送到哪个设备,在内核完全初始化之前,就确定一个可用的输出设备用于调试信息,当系统有多个可能的输出设备(如UART、LCD、VGA等)时,明确指定使用哪一个[摘自 deepseek]
-
输入KGDB启动参数
这部分内容参考脚注【b】
bashcd ~/sdk/kernel/arch/arm64/boot/dts/rockchip vim rk3399-linux.dtsi # 修改如下 chosen { bootargs = "loglevel=7 earlycon=uart8250,mmio32,0xff1a0000 console=ttyFIQ0 initcall_debug=1 kgdboc=ttyFIQ0,115200 kgdbcon root=PARTUUID=614e0000-0000 rootfstype=ext4 rw rootwait snd_aloop.index=7 kgdbwait"; // <--- 修改项 }; fiq_debugger: fiq-debugger { compatible = "rockchip,fiq-debugger"; rockchip,serial-id = <2>; rockchip,wake-irq = <0>; rockchip,irq-mode-enable = <1>; /* If enable uart uses irq instead of fiq */ // rockchip,baudrate = <1500000>; /* Only 115200 and 1500000 */ rockchip,baudrate = <115200>; /* Only 115200 and 1500000 */ <--- 修改项 interrupts = <GIC_SPI 150 IRQ_TYPE_LEVEL_HIGH 0>; pinctrl-names = "default"; pinctrl-0 = <&uart2c_xfer>; };
这个文件中只修改了
bootargs
属性,另外还需要注意rockchip,baudrate
属性,这里需要核对一下,看是否是115200(这个文件中的 rockchip,baudrate 属性会被rp-rk3399-board.dtsi 覆盖,1500000这个波特率也大概率用不到,可直接修改为115200)对于bootargs中的各个参数具体的意义这里不过多解释,但有以下几个需要注意
kgdbcon:(KGDB Console),允许通过同一串口(或终端)同时进行内核调试和输出控制台信息,如果之前没有接触过kgdb应该不清楚这是什么意思,想要搞明白得大致了解内核调试的过程,正常情况下当加载符号文件并设置断点后我们就会continue让程序继续运行,在程序运行的过程中可能会通过串口A输出一些日志,当程序运行到断点处会停止运行,此时我们将由串口B输入指令(比如在当下断点我们想查看某个变量的状态),那这种情况下就会涉及到两个串口,不管是从硬件还是软件层面来说,这无疑会增加某项成本,那如何解决这一问题?这里我了解到的有两个方案,其一就是这里提到的"kgdbcon",该参数将允许运行日志与调试指令共用一个串口(分时复用),另一个就是"agent-proxy",后者是在软件层面将一个串口分成两个端口,一个用于调试,一个用于日志打印,关于后者的配置及使用可以参考【c】
kgdbwait:在内核启动早期暂停系统并等待调试器(GDB)连接,这里建议不添加这个参数,因为对于驱动开发来说,我们一般会动态加载模块(.ko),等到没有问题了才会合并,动态加载需要系统完全启动后才操作,添加该参数后,每次都卡在启动过程,然后连接串口,再输入"go",一系列操作下来比较麻烦
-
添加调试驱动模块子节点(kgdb_test)
为更加符合外设的整体驱动配置结构,这里设置了一个kgdb_test子结点,该节点不与任何硬件属性绑定(例如引脚、电平之类的),子节点除了 status 与 compatible 是必要的,其他的属性可有可无,time属性是随意添加的,无实际意义,仅仅是想在该节点中添加一个非字符串属性,在后续调试时,如果有必要可以进行相关操作
bashcd ~/sdk/kernel/arch/arm64/boot/dts/rockchip/rk3399 vim king-rk3399.dts # 添加kgdb_test子节点如下 kgdb_test: kgdb_test { status = "okay"; compatible="king3399,kgdb-test"; time = <1000>; };
-
重新编译内核并核对设备树参数
完成设备树修改后还需要重新编译内核kernel,本次编译大约持续2分钟
bashcd ~/sdk ./build.sh kernel
为确保成功修改设备树中的启动参数,强烈建议完成下述核对操作
,本人一开始配置KGDB就是过于自信,没有进行反编译处理,导致没有注意到stdout-path这个属性(这个属性在本人所查配置KGDB资料中根本没人提到!!!)bashcd ~/sdk/kernel/arch/arm64/boot/dts/rockchip/rk3399 # 将更新的dtb文件反编译为dts # 该过程可能会产生一些警告,可不处理 dtc -I dtb -O dts king-rk3399.dtb -o king-rk3399-dtc.dts # 打开反编译后的文件 vim king-rk3399-dtc.dts # 查看chosen以及fiq-debugger节点 chosen { bootargs = "loglevel=7 earlycon=uart8250,mmio32,0xff1a0000 console=ttyFIQ0 initcall_debug=1 kgdboc=ttyFIQ0,115200 kgdbcon root=PARTUUID=614e0000-0000 rootfstype=ext4 rw rootwait snd_aloop.index=7 kgdbwait"; // <--- 核对项 stdout-path = "serial2:115200n8"; // <--- 核对项 }; fiq-debugger { compatible = "rockchip,fiq-debugger"; rockchip,serial-id = <0x2>; rockchip,wake-irq = <0x0>; rockchip,irq-mode-enable = <0x1>; rockchip,baudrate = <0x1c200>; // <--- 核对项 0x1c200 = 115200 interrupts = <0x0 0x96 0x4 0x0>; pinctrl-names = "default"; pinctrl-0 = <0x4e>; phandle = <0x1a7>; };
-
烧录内核镜像boot.img
完成上述操作并核对没有问题后便可将生成的boot.img文件烧录到板子中,boot.img路径为
bashcd ~/sdk/kernel/
-
-
编译具有调试信息的驱动模块.ko文件
在生成带调试信息的.ko文件前,需要知道如何判断.ko文件是否带有调试信息,如果完成前面的内核(kernel)配置并生成新的内核,此时再去编译.ko模块通常就会带有调试信息
bash# 这里以之前文章风扇驱动模块举例, # 对fan_test.ko文件直接执行如下操作(不重新编译) file fan_test.ko # 返回 # fan_test.ko: ELF 64-bit LSB relocatable, # ARM aarch64, version 1 (SYSV), BuildID # [sha1]=2a59c...xxx...94c, with debug_info, not stripped # 为方便对比这里再创建一个新的文件夹, # 将 Makefile fan_test.c fan_app.c复制到该文件夹, # 并在该文件夹下执行 make # 此时生成的.ko文件是配置了kgdb的内核所编译的, # 对刚生成的.ko文件执行如下操作 file fan_test.ko # 返回 # fan_test.ko: ELF 64-bit LSB relocatable, # ARM aarch64, version 1 (SYSV), BuildID # [sha1]=2a59c...xxx...94c, with debug_info, not stripped
通过对比发现好像没啥区别(本人的确实没有区别,正常来说应该是不一样的,如果返回的信息中存在 "with debug_info, not stripped",则说明.ko文件包含调试信息),后来我查了一下,如果内核配置了"CONFIG_DEBUG_INFO=y",生成的 .ko 文件通常会包含调试信息,而这个选项(CONFIG_DEBUG_INFO)本人的rockchip_linux_defconfig中一直为开启的(CONFIG_DEBUG_INFO=y),如果配置kgdb后的内核编译的.ko没有"with debug_info, not stripped",可以在模块生成脚本Makefile中强制开启调试信息,具体配置如下
bash# 模块同级目录下的Makefile脚本中添加如下两条配置 EXTRA_CFLAGS += -g # 强制启用调试信息 STRIP = : # 禁用 strip 操作
此外,还可以通过如下指令确认模块是否带有调试信息(若包含调试信息则通常存在.debug_*字段)
bashreadelf -S fan_test.ko | grep debug # 返回类似如下字段 # [31] .debug_info PROGBITS 0000000000000000 00000dfc # [32] .rela.debug_info RELA 0000000000000000 0002fa38 # [33] .debug_abbrev PROGBITS 0000000000000000 0001b267 # [34] .debug_loc PROGBITS 0000000000000000 0001c066 # ......
然而就算满足上述两个也不能顺利调试,编译时会对程序进行优化,导致断点无法定位到指定的行,因此还需要在Makefile中添加如下配置
bash# 参数 作用 必要性 # -g3 生成最大调试信息(包括宏定义) 必需 # -O0 完全禁用优化 必需 # -Wall 启用所有警告 推荐 # -Wextra 启用额外警告 推荐 # -fno-omit-frame-pointer 保留帧指针(便于回溯) 内核调试需要 EXTRA_CFLAGS = -O0 KBUILD_CFLAGS+="-fno-gnu-unique"
为突出本文的重点及适用范围,文末仓库将会给出一个简单的驱动模块示例kgdb_module(示例包含kgdb_test.c,kgdb_app.c,Makefile),本文后续调试过程将以此模块进行,该模块不依赖于硬件,且模块逻辑简单明了
将文末仓库中的kgdb_module文件夹复制到虚拟机的home目录下,修改Makefile内的
KERNEL_DIR 与 CROSS_COMPILE
路径,这两个路径本系列博文之前有提到,可自行查看,修改完后在kgdb_module文件夹内执行 make,稍等片刻后将生成的kgdb_test.ko与kgdb_app传到板子的home目录下bashscp kgdb_test.ko kgdb_app [email protected]:/home/username/xxx # username:板子的用户 # 192.168.aaa.bbb:板子的ip # /home/username/xxx:home目录下用户username的任意路径
2 调试前最后的准备
为了使本文描述更加准备易懂,这里做如下几个定义:
- 虚拟机:代指调试机,也就是前文用来编译boot.img、.ko文件的电脑
- 虚拟机-终端a:虚拟机中开启的一个终端,以此类推-终端b、c...x
- 板子:代指目标机,也就是开发板king3399
- 板子-终端1:板子中开启的一个终端,以此类推-终端2、3...n
将usb转串口模块插入电脑,另一端接入板子调试串口的接口(板子引出的接口上有标注丝印DEBUG,具体型号的板子可查看原理图,交叉连接TX-RX,RX-TX
,GND-GND)
在虚拟机中打开虚拟机-终端a,查看usb转串口模块所在路径(文件夹),若存在多个则可将usb转串口拔掉后再执行该指令以确定具体的路径,这里假设所用路径为/dev/ttyUSB0
bash
ls /dev/ttyUSB*
在虚拟机-终端a中打开串口并进行相应配置(minicom的安装与使用可自行网上查找)
bash
sudo minicom -s
# 在弹出的一级界面中通过上下键选择 Serial port setup,回车打开串口配置界面
# 在弹出的二级界面中通过键盘字母A切到第一行,并修改为相应路径,回车保存
# 在弹出的二级界面中通过键盘字母E切到第五行,并修改为前文设备树中设定的波特率,回车保存
# 在弹出的二级界面中通过键盘字母F切到第六行,直接回车可切换状态为 No
# 完成上述修改后双击回车退出二级界面
# 在弹出的一级界面中通过上下键选择 Save Setup as dfl,并回车,保存此次修改
# 在弹出的一级界面中通过上下键选择 Exit,并回车,退出一级界面并打开串口
# 以上便是minicom最基本也是最常用的指令/配置,对于其他配置可通过 Ctrl + A 再按 Z 调出配置快捷键界面(帮助界面)

如果在bootargs中添加kgdbwait,此时将板子上电,可以看到大量系统启动日志在虚拟机-终端a(串口)中输出,片刻后系统启动过程会卡住,此时按下虚拟机键盘的上键,可调出[kdb]输入提示符,注意,这里是 kdb 不是 kgdb
,出现这种情况的原因是因为我们在前文配置内核(kernel)时勾选了 "KGDB_KDB:include kdb frontend for kgdb",这个选项会让系统先进入kdb调试状态(关于kdb与kgdb的区别自行网上查看),由于我们不用kdb这种调试方法,因此现在有两条路:其一,在[kdb]输入提示符下输入"go"让系统继续启动,其二,在[kdb]输入提示符下输入"kgdb"进入kgdb调试模式,由于本人没有添加kgdbwait参数,因此这两条路不细说,此外,本人在配置内核(kernel)没有勾选KGDB_KDB时系统启动过程好像会出问题,不知是不是个人原因,由于kernel的编译时间太长了,因此没有深究
如果在bootargs中没有添加kgdbwait,此时将板子上电,可以看到大量系统启动日志在虚拟机-终端a(串口)中输出,系统应该会正常完全启动,板子上的风扇会呼呼转或者板子上接的屏幕会显示系统桌面
假设到这一步系统已经完全启动,那么关于虚拟机部分的准备工作已经完成,剩下的就是板子的准备工作
bash
# 打开板子-终端1,将路径切到kgdb_module目录下,加载模块
sudo /sbin/insmod kgdb_test.ko
# 为确定该模块已经加载还可以查看加载模块列表(板子-终端1)
lsmod
# 板子-终端1打印信息如下
# Module Size User by
# kgdb_test 16384 0
# 查看设备列表,后续会对该文件写入得确保已经存在(板子-终端2)
ls /dev/kgdb*
# 板子-终端2打印信息如下
# /dev/kgdb_test
# 查看已分配的主设备号(板子-终端2)
cat /proc/devices
# 板子-终端2打印信息如下
# 505 kgdb_test (可能不同)
# 打开板子-终端2,查询驱动模块加载地址,可进入如下路径查看
cd /sys/module/kgdb_test/sections
sudo cat .text
# 返回如下地址,这需要记录,后边在虚拟机中调试时需要输入
# 0xffffffc001775000 (可能不同)
# 打开板子-终端1,运行应用程序(kgdb_app),检测是否正常
sudo ./kgdb_test 0
# 板子-终端1打印信息如下
# kgdb test begin
# 打开板子-终端3,最后一步准备工作,
# 启用 Linux 系统的 SysRq(Magic SysRq)功能
sudo sh -c "echo 1 > /proc/sys/kernel/sysrq"
# 执行完该指令板子-终端3应该没有任何日志输出,
# 为验证是否生效可执行如下指令
sudo cat /proc/sys/kernel/sysrq
# 若输出 1,表示已启用
# 上述配置在重新上电后将会失效,持久化配置(重启后生效)如下
# 下述操作本人没有做,仅供参考
sudo vim /etc/sysctl.conf
# 在其中添加一行 kernel.sysrq = 1
# 运行加载配置
sudo sysctl -p
# 关于这最后一步"启用 Linux 系统的 SysRq(Magic SysRq)功能",
# 很多博文中并没有提到,这里做一个小的扩展,
# 当时本人在执行sudo sh -c "echo g > /proc/sysrq-trigger"后
# 并没有触发调试,仅在虚拟机-终端a中打印几行提示日志
# 后来查找相关资料得知是当前用户权限不够,
# 在没有执行"sudo sh -c "echo 1 > /proc/sys/kernel/sysrq"前,
# 我们可以查询当前用户权限,在板子-终端3中输入如下指令
sudo cat /proc/sys/kernel/sysrq
# 本人的日志返回176
# SysRq 标志位解析
# /proc/sys/kernel/sysrq 的值是一个位掩码(bitmask)
# 各二进制位代表的权限如下:
# 位(二进制) 值(十进制) 功能描述
# 000000001 1 启用 0-9 控制台日志级别(0 紧急,9 调试)
# 000000010 2 允许键盘控制(调整键盘映射、切换终端等)
# 000000100 4 允许进程管理(终止任务、kill 信号等)
# 000001000 8 允许同步文件系统(sync 命令)
# 000010000 16 允许重新挂载文件系统为只读(remount-ro 命令)
# 000100000 32 允许发送信号(term/kill 进程)
# 001000000 64 允许重启/关机(reboot/poweroff 命令)
# 010000000 128 允许修改进程优先级(nice 调整)
# 100000000 256 允许所有功能(完全启用 SysRq)
# 将 176 转换为二进制并匹配标志位:176 (十进制) = 10110000 (二进制)
# 对应标志位组合:
# 128 (010000000):允许修改进程优先级(nice)。
# 32 (00100000):允许发送信号(term/kill 进程)。
# 16 (00010000):允许重新挂载文件系统为只读(remount-ro)。
# 176 所拥有权限(当前配置下,SysRq 仅支持以下操作)
# 重新挂载文件系统为只读(Alt+SysRq+u 或 echo u > /proc/sysrq-trigger)。
# 终止进程(如 Alt+SysRq+e 终止所有进程)。
# 调整进程优先级(较少使用)。
完成上述准备工作后终于进入真正的调试部分了
3 最后的调试
为了避免有遗漏的环节,这里对虚拟机与板子的配置与状态做一个总结:
-
虚拟机
- kernel配置了kgdb并编译通过
- 设备树配置了kgdbcon、波特率、添加子节点等并编译kernel通过
- 安装了minicom并配置且打开
- 虚拟机编译生成带调试信息的.ko文件
-
板子
- 烧录了上述配置了kgdb的boot.img
- 加载了编译后的.ko文件并查询相关地址,
- 启用 Linux 系统的 SysRq(Magic SysRq)功能
bash
# 打开板子-终端3,触发 Linux 内核的 SysRq (Magic System Request) 功能
sudo sh -c "echo g > /proc/sysrq-trigger"
# 该指令强制内核生成一个崩溃转储,用于 内核调试 或 故障分析
# 此时板子的卡住,板子上连接的外设像键盘、鼠标一类的都不可用(没有反应)
# 虚拟机-终端a 返回如下日志
# Entering kdb (current=0xffffff80386763c0, pid 2377) on processor 2 due to Keyby
# 在虚拟机-终端a 中按下键盘的方向键"上键"调出输入提示符
# [2]kdb>
# 在该输入提示符输入 kgdb 切换为 kgdb调试
[2]kdb> kgdb
# 虚拟机-终端a 返回如下日志
# Entering please attach debugger or use $D#44+ or $3#33
# 到此,虚拟机-终端a 的使命就算是完成了,可以直接关掉了(叉掉)
# 打开虚拟机-终端b,进入如下路径并打开vmlinux(Linux 内核 ELF 可执行文件,
# 该文件在修改配置文件后编译kernel时生成的)
cd ~/sdk/kernel
sudo /home/username/sdk/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gdb ./vmlinux
# 除了上述指令(sdk自带的)还可以使用gdb-multiarch工具
# gdb-multiarch工具需自行安装(网上自查)
# ~/sdk/kernel sudo gdb-multiarch ./vmlinux
# 不过gdb-multiarch有一些问题,文末会说明
# 虚拟机-终端b返回如下信息
# ...
# For help, type "help".
# Type "apropos word" to search for commands related to "word"...
# Reading symbols from ./vmlinux...
# 设置板子架构为aarch64(根据实际架构设置)
(gdb) set architecture aarch64
# The target architecture is set to "aarch64".
# 设置虚拟机端调试串口所用波特率为115200,这个需要与前文对应
(gdb) set serial baud 115200
# 通过虚拟机usb转串口连接板子
# 这里是软件层面的连接,前提是完成前文的硬件实物连接
(gdb) target remote /dev/ttyUSB0
# Remote debugging using /dev/ttyUSB0
# warning: multi-threaded target stopped without sending a thread-id, using first non-exited thread
# [Switching to Thread 4294967294]
# arch_kgdb_breakpoint () at ./arch/arm64/include/asm/kgdb.h:21
# 21 asm ("brk %0" : : "I" (KGDB_COMPILED_DBG_BRK_IMM));
# 注:这里可能会返回如下错误
# /dev/ttyUSB0: Input/output error.
# 本人多次执行 target remote /dev/ttyUSB0 后便可正常连接(仅供参考)
# 查看当前加载的文件
(gdb) l
# 16
# 17 #ifndef __ASSEMBLY__
# 18
# 19 static inline void arch_kgdb_breakpoint(void)
# 20 {
# 21 asm ("brk %0" : : "I" (KGDB_COMPILED_DBG_BRK_IMM));
# 22 }
# 23
# 24 extern void kgdb_handle_bus_error(void);
# 25 extern int kgdb_fault_expected;
# 从打印的代码可以看到这不是我们需要调试的代码
# 更换调试文件
(gdb) file
# A program is being debugged already.
# Are you sure you want to change the file? (y or n) y
# No executable file now.
# Discard symbol table from `/home/username/sdk/kernel/vmlinux'? (y or n) y
# No symbol file now.
# 确认当前已无调试文件
(gdb) l
# No symbol table is loaded. Use the "file" command.
# 加载kgdb_test符号文件
# /home/username/kgdb_module/kgdb_test.ko:虚拟机中kgdb_test.ko文件的路径
# 0xffffffc001775000:kgdb_test.ko在板子上加载后查询到的地址
(gdb) add-symbol-file /home/username/kgdb_module/kgdb_test.ko 0xffffffc001775000
# add symbol table from file "/home/username/kgdb_module/kgdb_test.ko" at
# .text_addr = 0xffffffc001775000
# (y or n) y
# Reading symbols from /home/username/kgdb_module/kgdb_test.ko...
# 查看当前加载的文件(此处返回我们想要调试的文件内容)
(gdb) l
# 1 #include <linux/init.h>
# 2 #include <linux/module.h>
# 3 #include <linux/fs.h>
# 4 #include <linux/cdev.h>
# 5 #include <linux/uaccess.h>
# 6 #include <linux/types.h>
# 7 #include <linux/kernel.h>
# 8 #include <linux/delay.h>
# 9 #include <linux/ide.h>
# 10 #include <linux/errno.h>
# 此时可以多次输入 l (list) 查看调试的代码,
# 或者直接 回车 ,代表重复上上次的指令
# 或者跳转到指定行 l num ,其中 num 代表行号
# l (list)打印的代码左侧是 行号,在需要的行设置断点
# 打断点 break num
(gdb) b 44
# Breakpoint 1 at 0xffffffc00177554c: file /home/xxx/kgdb_module/kgdb_test.c, line 44.
(gdb) b 65
# Breakpoint 2 at 0xffffffc0017756e4: file /home/xxx/kgdb_module/kgdb_test.c, line 65.
(gdb) b 77
# Breakpoint 3 at 0xffffffc001775710: file /home/xxx/kgdb_module/kgdb_test.c, line 77.
(gdb) b 81
# Breakpoint 4 at 0xffffffc001775720: file /home/xxx/kgdb_module/kgdb_test.c, line 81.
(gdb) b 86
# Breakpoint 5 at 0xffffffc00177572c: file /home/xxx/kgdb_module/kgdb_test.c, line 86.
# 查询断点 info break
(gdb) i b
# Num Type Disp Enb Address What
# 1 breakpoint keep y 0xffffffc00177554c in kgdb_chr_dev_fun
# at /home/xxx/kgdb_module/kgdb_test.c:44
# 2 breakpoint keep y 0xffffffc0017756e4 in kgdb_chr_dev_write
# at /home/xxx/kgdb_module/kgdb_test.c:65
# 3 breakpoint keep y 0xffffffc001775710 in kgdb_chr_dev_write
# at /home/xxx/kgdb_module/kgdb_test.c:77
# 4 breakpoint keep y 0xffffffc001775720 in kgdb_chr_dev_write
# at /home/xxx/kgdb_module/kgdb_test.c:81
# 5 breakpoint keep y 0xffffffc00177572c in kgdb_chr_dev_write
# at /home/xxx/kgdb_module/kgdb_test.c:86
# 设置完断点后让程序继续运行
(gdb) c
# Continuing.
# warning: Invalid remote reply: 3
# 此时板子的控制权释放,可以正常使用
# 在板子-终端1 执行应用程序,这里单走一个 6
sudo ./kgdb_app 6
# 板子执行上述指令后由于触发断点卡住
# 控制权又交到了 虚拟机-终端b
# 虚拟机-终端b 返回信息如下
# ...
# [New Thread 4872]
# [New Thread 4882]
# [Switching to Thread 4883]
# Thread 341 hit Breakpoint 2, kgdb_chr_dev_write (filp=0xffffff800dc74800,
# buf=0x7ff80df0a7 "\006", cnt=1, offt=0xffffffc00db1be20)
# at /home/sr/ws/mydriver/kgdb_module/kgdb_test.c:65
# 65 int error = copy_from_user(&write_data, buf, cnt);
(gdb) p error
# $1 = 127
(gdb) disable 2
(gdb) c
# Continuing.
# Thread 341 hit Breakpoint 3, kgdb_chr_dev_write (filp=0xffffff800dc74800,
# buf=0x7ff80df0a7 "\006", cnt=1, offt=0xffffffc00db1be20)
# at /home/sr/ws/mydriver/kgdb_module/kgdb_test.c:77
# 77 aaa = write_data - 2;
(gdb) p aaa
# $2 = 0
(gdb) disable 3
(gdb) c
# Continuing.
# Thread 341 hit Breakpoint 5, kgdb_chr_dev_write (filp=0xffffff800dc74800,
# buf=0x7ff80df0a7 "\006", cnt=1, offt=0xffffffc00db1be20)
# at /home/sr/ws/mydriver/kgdb_module/kgdb_test.c:86
# 86 ccc = kgdb_chr_dev_fun(aaa);
(gdb) p aaa
# $3 = 4
(gdb) p ccc
# Cannot access memory at address 0x98
(gdb) disable 5
(gdb) c
# Continuing.
# Thread 341 hit Breakpoint 1, kgdb_chr_dev_fun (n=4)
# at /home/sr/ws/mydriver/kgdb_module/kgdb_test.c:44
# 44 bbb = n * 2;
(gdb) p bbb
# $4 = 0
(gdb) p n
# $5 = 4
(gdb) disable 1
(gdb) c
# Continuing.
# 执行到这里由于应用程序结束了,控制权又交到了 板子-终端1
4 一些小问题
-
执行 vmlinux 的工具
-
如果使用自己安装的,可在"/usr/bin/"目录下查找
-
如果使用SDK自带的(如本文),则可在下述类似路径查找
bash/home/username/sdk/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin
-
如果使用gdb-multiarch,则需要自行安装(apt install ...),但本人在使用这个工具时遇到如下问题(能力有限,没有解决,不确定是工具的问题还是人的问题)
bash# 问题 1 (gdb) add-symbol-file ~/kgdb_module/kgdb_test.ko 0xffffffc001775000 # add symbol table from file "~/kgdb_module/kgdb_test.ko" at # .text_addr = 0xffffffc001775000 # (y or n) y # BFD: warning: ~/kgdb_module/kgdb_test.ko: unsupported GNU_PROPERTY_TYPE (5) type: 0xc0000000 # Reading symbols from ~/kgdb_module/kgdb_test.ko...done. # 问题 2 (gdb) target remote /dev/ttyUSB0 # Remote debugging using /dev/ttyUSB0 # arch_kgdb_breakpoint () at ./arch/arm64/include/asm/kgdb.h:21 # 21 asm ("brk %0" : : "I" (KGDB_COMPILED_DBG_BRK_IMM)); # Call Frame Instruction op 45 in vendor extension space is not handled on this architecture.
-
-
远程连接时
-
返回 'vMustReplyEmpty': vMustReplyEmpty
bash(gdb) target remote /dev/ttyUSB0 # Remote debugging using /dev/ttyUSB0 # Remote replied unexpectedly to 'vMustReplyEmpty': vMustReplyEmpty # 原因: # 板子没有触发远程调试,需要在板子上执行 # sudo sh -c "echo g > /proc/sysrq-trigger"
-
返回 Input/output error.
bash(gdb) target remote /dev/ttyUSB0 # /dev/ttyUSB0: Input/output error. # 本人多次执行 target remote /dev/ttyUSB0 后便可正常连接(仅供参考)
-
-
无法触发断点
现象是一切正常,但运行应用程序时(sudo ./kgdb_app 6),程序无法停留在断点处,从头运行到尾,这里给出一个可能的原因,检查断点信息是否有具体的地址,方法如下
bash(gdb) i b # Num Type Disp Enb Address What # 1 breakpoint keep y <PENDING> /home/xxx/kgdb_module/kgdb_test.c:44 # 2 breakpoint keep y <PENDING> /home/xxx/kgdb_module/kgdb_test.c:65 # 可以看到断点的Address属性为 <PENDING>, # 原因是模块尚未被加载到内核就设置了断点, # 这里是板子上的模块未被加载到内核 # 这种现象大概率是先设置的断点再在板子上加载模块 # 解决方法 # 删除断点 delete num (gdb) d 1 (gdb) d 2 # 在板子加载模块后重新设置断点 (gdb) b 44 (gdb) b 65 # 再次查看断点 (gdb) i b # Num Type Disp Enb Address What # 3 breakpoint keep y 0xffffffc00177554c in kgdb_chr_dev_fun # at /home/xxx/kgdb_module/kgdb_test.c:44 # 4 breakpoint keep y 0xffffffc0017756e4 in kgdb_chr_dev_write # at /home/xxx/kgdb_module/kgdb_test.c:65 # 与之前的对比发现 Address属性有了具体的地址 # 此时可以再次尝试执行应用程序
-
调试卡在 printk
这个问题网上有提到,就是如果我们在kgdb_test.c中有使用printk,程序运行时会一直卡在(陷入)printk处(即使没有在该行打断点),这个问题本人能力有限,没有解决,在kgdb_test.c中我将所有的printk都注释掉了,如果想复现的可以释放编译再尝试调试,我的想法是既然都使用kgdb了,对于中间变量为啥不用 print(p value)
-
无法Continue
现象就是当执行c时,程序一直卡在当前断点,这个问题本人能力有限,没有解决,在本文上边的例子中,用了一个很笨的办法,那就是执行到某个断点后如果要继续向下运行,那就将当前断点禁用disable(disable num),对于如何查看断点是否被禁用可在执行(i b)后查看断点的Enb列(y:使能,n:失能),程序运行完后再统一使能(enable),类似的操作还有禁用所有断点(disable),删除所有断点(delete)
-
其他工具
这里提两个本人感觉很实用的工具,但安装与使用方法这里不赘述,自行网上查找
- agent-proxy :将一个串口变成两个,一个调试,一个打印日志
- CGDB :将终端窗口分为上下两部分,上面是代码窗口,下面是调试窗口
5 总结
这篇博文前后花了一个多月的时间,基本把所有步骤与问题都记录了下来,花这么多时间一方面是本人菜,另一方面是官方参考资料就像是@¥%.,关于板子配置kgdb的部分我几乎把所有能查到的都查了,rk的芯片在市场上的保有量应该不算小,这也是当初选择这个板子的主要原因,驱动开发这个方向也并不那么小众,但是相关的资料都很零碎很乱,本人并不是这方面的砖家,以前主要接触的芯片都是ST、Ti、Analog以及Xilinx等,但东西是否做的用心稍微对比一下就能看出,芯片你玩不过人家,你#@把资料与生态先做起来啊,活该%¥!
【a】 [[野火]《嵌入式Linux镜像构建与部署---基于LubanCat-RK系列板卡》_20240615.pdf------6.4 内核配置选项]
【b】 Rv1109内核调试