King3399(ubuntu文件系统)KGDB配置与功能测试

0 引言

上一篇文章提到了GDB/GDBServer的调试方法,这种方法无法对驱动模块.ko文件进行调试,对于驱动模块的开发我们需要用到KGDB,本人在配置及使用KGDB时遇到很多问题,虽然KGDB对于驱动模块的开发很重要,但网上的资料大都不涉及具体步骤,本文将仔细记录该过程以供大家参考,内容虽以king3399为目标板进行配置及测试,但流程适用于rk/rp官方SDK

1 内核(kernel)配置KGDB

这里先梳理KGDB配置及调试的大致流程以便有一个宏观的了解:

  1. 在内核(kernel)中开启KGDB相应配置开关
  2. 编译内核(kernel)
  3. 在设备树中传入KGDB相应参数
  4. 重新编译内核(kernel),并将生成的boot.img文件烧录
  5. 交叉编译带有调试信息的驱动模块文件,并将驱动模块文件传至板子
  6. 板子上电,加载模块触发调试等待主机连接
  7. 主机通过调试串口连接板子并开始调试

rk/rp关于KGDB这部分的官方资料几乎没有,如果还在试图寻找的不用再浪费时间了!!!,下边是具体过程,郑重提示:保险起见,任何文件修改前请做好备份,做好修改记录以及虚拟机的快照备份!!!

需要注意的是,下边提到的文件及路径应根据手中的板子决定,不可照抄照搬,即使使用的是同一颗芯片(rk3399),如果板子的型号不同(king3399)或者系统不同,那么涉及的文件也大概率不同!

  1. 查看板子所用配置文件

    在...sdk/下执行 ./build.sh kernel

    如果之前没对kernel做其他的修改该编译过程将持续大约2分钟,从打印日志可提取如下信息:

    1. 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/

    2. K_KERNEL_CFG=rockchip_linux_defconfig

      该文件为板子的内核配置文件

      绝对路径为 /home/username/sdk/kernel/arch/arm64/configs/

    这两个文件是主要配置文件,找不到这两个文件将无法完成本文后续操作

  2. 修改kernel配置

    1. 备份需要修改的文件

      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分钟!!!,另外,编译前的部分文件我们在后边可能还会重新用到

    2. 利用make menuconfig修改kernel配置

      这部分内容参考脚注【a】

      bash 复制代码
      cd ~/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

    3. 编译修改后的kernel,该过程持续约60分钟!!!

      bash 复制代码
      cd ~/sdk
      
      ./build.sh kernel

    完成后会在 ~/sdk/kernel下生成新的boot.img,但这并不是最终烧录的boot.img文件,到这,已经完成kernel的相关配置工作

  3. 修改设备树,传入KGDB启动参数

    1. 指定系统的标准输出(stdout)设备路径

      bash 复制代码
      cd ~/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属性,这里需要核对一下,看是否是115200

      stdout-path:告诉内核应该将控制台输出(如printk、早期启动信息等)发送到哪个设备,在内核完全初始化之前,就确定一个可用的输出设备用于调试信息,当系统有多个可能的输出设备(如UART、LCD、VGA等)时,明确指定使用哪一个[摘自 deepseek]

    2. 输入KGDB启动参数

      这部分内容参考脚注【b】

      bash 复制代码
      cd ~/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",一系列操作下来比较麻烦

    3. 添加调试驱动模块子节点(kgdb_test)

      为更加符合外设的整体驱动配置结构,这里设置了一个kgdb_test子结点,该节点不与任何硬件属性绑定(例如引脚、电平之类的),子节点除了 status 与 compatible 是必要的,其他的属性可有可无,time属性是随意添加的,无实际意义,仅仅是想在该节点中添加一个非字符串属性,在后续调试时,如果有必要可以进行相关操作

      bash 复制代码
      cd ~/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>;
      };
    4. 重新编译内核并核对设备树参数

      完成设备树修改后还需要重新编译内核kernel,本次编译大约持续2分钟

      bash 复制代码
      cd ~/sdk
      
      ./build.sh kernel

      为确保成功修改设备树中的启动参数,强烈建议完成下述核对操作,本人一开始配置KGDB就是过于自信,没有进行反编译处理,导致没有注意到stdout-path这个属性(这个属性在本人所查配置KGDB资料中根本没人提到!!!)

      bash 复制代码
      cd ~/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>;
      };
    5. 烧录内核镜像boot.img

      完成上述操作并核对没有问题后便可将生成的boot.img文件烧录到板子中,boot.img路径为

      bash 复制代码
      cd ~/sdk/kernel/
  4. 编译具有调试信息的驱动模块.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_*字段)

    bash 复制代码
    readelf -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目录下

    bash 复制代码
    scp kgdb_test.ko kgdb_app [email protected]:/home/username/xxx
    
    # username:板子的用户
    # 192.168.aaa.bbb:板子的ip
    # /home/username/xxx:home目录下用户username的任意路径
2 调试前最后的准备

为了使本文描述更加准备易懂,这里做如下几个定义:

  1. 虚拟机:代指调试机,也就是前文用来编译boot.img、.ko文件的电脑
  2. 虚拟机-终端a:虚拟机中开启的一个终端,以此类推-终端b、c...x
  3. 板子:代指目标机,也就是开发板king3399
  4. 板子-终端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 最后的调试

为了避免有遗漏的环节,这里对虚拟机与板子的配置与状态做一个总结:

  1. 虚拟机

    1. kernel配置了kgdb并编译通过
    2. 设备树配置了kgdbcon、波特率、添加子节点等并编译kernel通过
    3. 安装了minicom并配置且打开
    4. 虚拟机编译生成带调试信息的.ko文件
  2. 板子

    1. 烧录了上述配置了kgdb的boot.img
    2. 加载了编译后的.ko文件并查询相关地址,
    3. 启用 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 一些小问题
  1. 执行 vmlinux 的工具

    1. 如果使用自己安装的,可在"/usr/bin/"目录下查找

    2. 如果使用SDK自带的(如本文),则可在下述类似路径查找

      bash 复制代码
      /home/username/sdk/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin
    3. 如果使用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.
  2. 远程连接时

    1. 返回 '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"
    2. 返回 Input/output error.

      bash 复制代码
         (gdb) target remote /dev/ttyUSB0
         # /dev/ttyUSB0: Input/output error.
         # 本人多次执行 target remote /dev/ttyUSB0 后便可正常连接(仅供参考)
  3. 无法触发断点

    现象是一切正常,但运行应用程序时(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属性有了具体的地址
       # 此时可以再次尝试执行应用程序
  4. 调试卡在 printk

    这个问题网上有提到,就是如果我们在kgdb_test.c中有使用printk,程序运行时会一直卡在(陷入)printk处(即使没有在该行打断点),这个问题本人能力有限,没有解决,在kgdb_test.c中我将所有的printk都注释掉了,如果想复现的可以释放编译再尝试调试,我的想法是既然都使用kgdb了,对于中间变量为啥不用 print(p value)

  5. 无法Continue

    现象就是当执行c时,程序一直卡在当前断点,这个问题本人能力有限,没有解决,在本文上边的例子中,用了一个很笨的办法,那就是执行到某个断点后如果要继续向下运行,那就将当前断点禁用disable(disable num),对于如何查看断点是否被禁用可在执行(i b)后查看断点的Enb列(y:使能,n:失能),程序运行完后再统一使能(enable),类似的操作还有禁用所有断点(disable),删除所有断点(delete)

  6. 其他工具

    这里提两个本人感觉很实用的工具,但安装与使用方法这里不赘述,自行网上查找

    1. agent-proxy :将一个串口变成两个,一个调试,一个打印日志
    2. CGDB :将终端窗口分为上下两部分,上面是代码窗口,下面是调试窗口
5 总结

这篇博文前后花了一个多月的时间,基本把所有步骤与问题都记录了下来,花这么多时间一方面是本人菜,另一方面是官方参考资料就像是@¥%.,关于板子配置kgdb的部分我几乎把所有能查到的都查了,rk的芯片在市场上的保有量应该不算小,这也是当初选择这个板子的主要原因,驱动开发这个方向也并不那么小众,但是相关的资料都很零碎很乱,本人并不是这方面的砖家,以前主要接触的芯片都是ST、Ti、Analog以及Xilinx等,但东西是否做的用心稍微对比一下就能看出,芯片你玩不过人家,你#@把资料与生态先做起来啊,活该%¥!

【a】 [[野火]《嵌入式Linux镜像构建与部署---基于LubanCat-RK系列板卡》_20240615.pdf------6.4 内核配置选项]

【b】 Rv1109内核调试

【c】 使用KGDB调试Linux驱动(以imx6ull开发板为例)

【d】 项目相关资料 pwd : 1nca

相关推荐
simple_whu2 小时前
Ubuntu24.04编译ORB_SLAM的一系列报错解决
ubuntu·slam
我不是秃头sheep3 小时前
Ubuntu 安装 Docker(镜像加速)完整教程
linux·ubuntu·docker
小猪写代码4 小时前
Ubuntu 系统默认已安装 python,此处只需添加一个超链接即可
linux·python·ubuntu
有谁看见我的剑了?5 小时前
ubuntu 22.04 wifi网卡配置地址上网
linux·运维·ubuntu
码农新猿类5 小时前
Ubuntu摄像头打开失败
linux·运维·ubuntu
PWRJOY6 小时前
Ubuntu磁盘空间分析:du命令及常用组合
linux·运维·ubuntu
小灰兔的小白兔8 小时前
【Ubuntu】扩充磁盘大小
ubuntu
chennalC#c.h.JA Ptho12 小时前
ubuntu studio 系统详解
linux·运维·服务器·经验分享·ubuntu·系统安全
ChironW20 小时前
Ubuntu 24.04 LTS系统上配置国内时间同步
linux·运维·服务器·ubuntu