debug 是定位并解决代码开发过程中问题的最好方法,本篇文章讲解如何利用 QEMU 和 GDB 来搭建 linux kernel 内核及内核模块的调试环境,以及如何结合 VSCode 实现带 UI 的调试。

⚙️ 以下是本篇文章所使用的环境:

  • Host:Ubuntu 24.04
  • QEMU: 8.2.2
  • GDB: 15.0.50
  • Linux kernel 6.8.0

# 编译内核

在网上大多数教程中都只说了要配置 CONFIG_DEBUG_INFO=y ,但实际较新版本的 linux 内核的配置文件中引入了 CONFIG_DEBUG_INFO_NONE 字段,默认为 CONFIG_DEBUG_INFO_NONE=y ,这种情况 make 会覆盖 CONFIG_DEBUG_INFO=y 的选项,导致无法生成带调试的信息的内核,所以请务必按照如下步骤进行配置。

首先在 linux 源代码目录中执行 make defconfig 获取一个默认的 .config 配置,然后用 vim .config 打开 .config 并找到 CONFIG_DEBUG_INFO_NONE ,然后进行如下修改:

  • CONFIG_DEBUG_INFO_NONE=y ➡️ CONFIG_DEBUG_INFO_NONE=n
  • # CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT is not set ➡️ CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y

然后保存,并执行 make ,会提示一些配置问题,一般直接回车即可,但是启用 GDB_SCRIPTS 要手动设置下:

make
  SYNC    include/config/auto.conf.cmd
*
* Restart config...
*
*
* Compile-time checks and compiler options
*
Debug information
  1. Disable debug information (DEBUG_INFO_NONE)
> 2. Rely on the toolchain's implicit default DWARF version (DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT)
  3. Generate DWARF Version 4 debuginfo (DEBUG_INFO_DWARF4)
  4. Generate DWARF Version 5 debuginfo (DEBUG_INFO_DWARF5)
choice[1-4?]: 2
Reduce debugging information (DEBUG_INFO_REDUCED) [N/y/?] (NEW)
Compressed Debug information
> 1. Don't compress debug information (DEBUG_INFO_COMPRESSED_NONE) (NEW)
  2. Compress debugging information with zlib (DEBUG_INFO_COMPRESSED_ZLIB) (NEW)
  3. Compress debugging information with zstd (DEBUG_INFO_COMPRESSED_ZSTD) (NEW)
choice[1-3?]:
Produce split debuginfo in .dwo files (DEBUG_INFO_SPLIT) [N/y/?] (NEW)
Provide GDB scripts for kernel debugging (GDB_SCRIPTS) [N/y/?] (NEW) y # 其余都是默认,只有这个要手动开启
Warn for stack frames larger than (FRAME_WARN) [2048] 2048
Strip assembler-generated symbols during link (STRIP_ASM_SYMS) [N/y/?] n
Generate readable assembler code (READABLE_ASM) [N/y/?] n
Install uapi headers to usr/include (HEADERS_INSTALL) [N/y/?] n
Enable full Section mismatch analysis (DEBUG_SECTION_MISMATCH) [N/y/?] n
Make section mismatch errors non-fatal (SECTION_MISMATCH_WARN_ONLY) [Y/n/?] y
Force weak per-cpu definitions (DEBUG_FORCE_WEAK_PER_CPU) [N/y/?] n

当配置完成并开始出现编译时,我们先 ctrl+c 停止一下,然后再次 vim .config 打开 .config ,并作如下修改:

  • # CONFIG_KGDB is not set ➡️ CONFIG_KGDB=y
  • CONFIG_RANDOMIZE_BASE=y ➡️ CONFIG_RANDOMIZE_BASE=n
  • # CONFIG_DEBUG_INFO_REDUCED is not set ➡️ CONFIG_DEBUG_INFO_REDUCED=n
  • 如果架构支持 CONFIG_FRAME_POINTER,要保持开启

然后保存 .config 并再次 make ,此时又会提示一些配置问题,全部回车即可,待内核编译完成,目录下会生成以下两个文件

  • ./vmLinux
  • ./arch/x86/boot/bzImage

其中 vmLinux 为 GDB 所需的调试 Map 文件,bzImage 为大内核文件,至此,该步骤完成。

# 制作 qemu 启动盘

# 基本操作系统

由于不同的发行版本其代码实现差异巨大,所以建议在开发和调试内核的场景下,在确定了 Linux 版本后,建议优先选择使用该内核版本的操作系统版本,例如 ubuntu 24.04 LTS 默认使用 linux 6.8 的内核,那当我想调试 6.8 的内核时,就优先选择 ubuntu 24.04 系统作为 qemu 的基础系统,这样可以减少很多不必要的麻烦。

我们使用 debootstrap 来为 qemu 制作一个最基本的启动盘。

debootstrap 是 debian/ubuntu 下的一个工具,用来构建一套基本的系统 (根文件系统)。生成的目录符合 Linux 文件系统标准 (FHS),即包含了 /boot、/etc、/bin、/usr 等等目录,但它比发行版本的 Linux 体积小很多,当然功能也没那么强大,因此,只能说是 “基本的系统”,但也足够我们使用了。 debootstrap 的使用命令为:

sudo debootstrap --arch [平台] [发行版本代号] [目录] [镜像源地址]

不同 ubuntu 发行版本代号可以在这里查到,也可以通过 lsb_release -a 查看自己本机的系统代码:

❯ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 24.04.1 LTS
Release:	24.04
Codename:	noble

首先安装下 debootstrap

sudo apt update
sudo apt install debootstrap

这里我选择 ubuntu 24.04 版本作为基础系统,代码 Noble Numbat (取第一个单词),国内用户可以使用清华源加速安装:

sudo debootstrap --arch=amd64 noble rootfs http://mirrors.ustc.edu.cn/ubuntu/

完成后使用

sudo chroot rootfs

进入 rootfs 系统中,以下操作直到 exit 前,都是在该操作系统中进行的。

# 基本工具及配置

安装 busybox

busybox 是一个集成了一百多个最常用 Linux 命令和工具(如 cat、echo、grep、mount、telnet 等)的精简工具箱,它只需要几 MB 的大小,很方便进行各种快速验证,被誉为 “Linux 系统的瑞士军刀”。

apt update
apt install -y busybox init

然后编辑 /init (如果没有则手动创建)

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
exec /bin/sh

然后赋予可执行权限:

chmod +x /init

设置 root 用户密码:

root@kernel-fuzz:/# passwd
New password:
Retype new password:
passwd: password updated successfully

然后就可以使用 exit 退出当前系统了。

# 制作 qemu 镜像

# 打包 rootfs
sudo tar -czvf rootfs.tar.gz -C rootfs .
# 创建硬盘
dd if=/dev/zero of=rootfs.img bs=1M count=512
mkfs.ext4 rootfs.img
mkdir mnt
sudo mount rootfs.img mnt
# 复制
sudo tar -xzf rootfs.tar.gz -C mnt
sudo umount mnt
# 转换成 qcow2 镜像
qemu-img convert -f raw -O qcow2 rootfs.img rootfs.qcow2

至此,准备工作都已经完成了,接下来就可以调试内核了。

# 调试内核

# 启动 QEMU 并启用 GDB 远程调试

使用如下命令启动 qemu

sudo qemu-system-x86_64 -m 2G \
	 	        -smp 2 \
 		        -kernel /path/to/linux-6.8/arch/x86/boot/bzImage \
		        -drive file=/path/to/rootfs.qcow2,format=qcow2  \
		        -append "console=ttyS0 nokaslr root=/dev/sda rw"  \
 		        -nographic \
		        -enable-kvm \
		        -s -S
  • -s :相当于 -gdb tcp::1234 ,在端口 1234 上开启 GDB 服务器
  • -S :启动后暂停 CPU,等待 GDB 连接

通过以上命令启动 qemu 后会卡住,这是因为 -S 参数会暂停 kernel 的执行,等待 GDB 的连接。

# 自动加载调试脚本

由于某些发行版可能会将 gdb 脚本的自动加载限制在已知的安全目录中。 如果 gdb 输出拒绝加载 vmlinux-gdb.py(相关命令找不到),请将:

add-auto-load-safe-path /path/to/linux-src

添加到 ~/.gdbinit ,这个 py 脚本就是实现了一些内核调试命令,他们都以 lx- 开头,下面会展示。

# 连接 GDB 到 QEMU

在宿主机(host)上,打开 gdb 并加载调试符号:

gdb vmlinux

然后在 GDB 中连接到 QEMU:

target remote :1234

此时 GDB 会连接到 QEMU,CPU 仍然暂停。这时就可以任意在内核中打断点进行调试了,例如:

(gdb) b start_kernel
Breakpoint 1 at 0xffffffff83220950: file init/main.c, line 875.
(gdb) c
Continuing.
Thread 1 hit Breakpoint 1, start_kernel () at init/main.c:875
875	{
(gdb)
下断点后执行报错:Cannot access memory at address 0xffffxxxxxx

这是因为较新版本的 linux 内核实现了一个内核代码段的自我保护机制,该机制通过 CONFIG_STRICT_KERNEL_RWXCONFIG_STRICT_MODULE_RWX 来配置,一般是默认开启的。当这两个选项开启时, kgdb 添加断点时,默认是通过将指定内核代码改写为断点指令,从而在执行到指定位置时触发软件断点的,而该选项的存在会导致断点指令无法写入,所以才会出现如下错误:

(gdb) c
Continuing.
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0xffffffff83220950

解决方法有两种:

  1. gdb 中使用 hb 替代 b 来下硬件断点
  2. .config 中关闭 CONFIG_STRICT_KERNEL_RWXCONFIG_STRICT_MODULE_RWX 并重新编译内核。(需要注意的是,在大多数架构下,该选项已经不是一个可选项,而是一个默认开启的特性,所以可能无法关闭,参考:https://kgdb-bugreport.narkive.com/sK74UMC3/patch-documentation-docbook-kgdb-update-config-strict-kernel-rwx-info)

以上两种方法都可以解决该问题。

Linux 内核默认导出了一些工具用于调试内核,可以使用 apropos lx 查看他们:

(gdb) apropos lx
function lx_clk_core_lookup -- Find struct clk_core by name
function lx_current -- Return current task.
function lx_dentry_name -- Return string of the full path of a dentry.
function lx_device_find_by_bus_name -- Find struct device by bus and name (both strings)
function lx_device_find_by_class_name -- Find struct device by class and name (both strings)
function lx_i_dentry -- Return dentry pointer for inode.
function lx_module -- Find module by name and return the module variable.
function lx_per_cpu -- Return per-cpu variable.
function lx_radix_tree_lookup --  Lookup and return a node from a RadixTree.
function lx_rb_first -- Lookup and return a node from an RBTree
function lx_rb_last -- Lookup and return a node from an RBTree.
function lx_rb_next -- Lookup and return a node from an RBTree.
function lx_rb_prev -- Lookup and return a node from an RBTree.
function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable.
function lx_thread_info -- Calculate Linux thread_info from task variable.
function lx_thread_info_by_pid -- Calculate Linux thread_info from task variable found by pid
lx-clk-summary -- Print clk tree summary
lx-cmdline -- Report the Linux Commandline used in the current kernel.
lx-configdump -- Output kernel config to the filename specified as the command
lx-cpus -- List CPU status arrays
lx-device-list-bus -- Print devices on a bus (or all buses if not specified)
lx-device-list-class -- Print devices in a class (or all classes if not specified)
lx-device-list-tree -- Print a device and its children recursively
lx-dmesg -- Print Linux kernel log buffer.
lx-dump-page-owner -- Dump page owner
lx-fdtdump -- Output Flattened Device Tree header and dump FDT blob to the filename
--Type <RET> for more, q to quit, c to continue without paging--
lx-genpd-summary -- Print genpd summary
lx-getmod-by-textaddr -- Look up loaded kernel module by text address.
lx-interruptlist -- Print /proc/interrupts
lx-iomem -- Identify the IO memory resource locations defined by the kernel
lx-ioports -- Identify the IO port resource locations defined by the kernel
lx-list-check -- Verify a list consistency
lx-lsmod -- List currently loaded modules.
lx-mounts -- Report the VFS mounts of the current process namespace.
lx-page_address -- struct page to linear mapping address
lx-page_to_pfn -- struct page to PFN
lx-page_to_phys -- struct page to physical address
lx-pfn_to_kaddr -- PFN to kernel address
lx-pfn_to_page -- PFN to struct page
lx-ps -- Dump Linux tasks.
lx-slabinfo -- Show slabinfo
lx-slabtrace -- Show specific cache slabtrace
lx-sym_to_pfn -- symbol address to PFN
lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules.
lx-timerlist -- Print /proc/timer_list
lx-version -- Report the Linux Version of the current kernel.
lx-virt_to_page -- virtual address to struct page
lx-virt_to_phys -- virtual address to physical address
lx-vmallocinfo -- Show vmallocinfo

# 调试内核模块

# 生成符号信息

调试内核模块时还需要额外在编译内核模块时生成调试符号,即 make 时添加 -g 参数:

make CFLAGS="-g"

或者在 Makefile 中添加:

EXTRA_CFLAGS="-g -DDEBUG"

例如 make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules EXTRA_CFLAGS="-g -DDEBUG"

# 加载模块

直接在 qemu 中使用 insmod simplefs.ko 加载模块,然后正常下断点调试即可。值得一提的是,即使模块还没有加载,也可以先设置模块内的断点,此时 gdb 的提示为:

(gdb) b btrfs_init_sysfs
Function "btrfs_init_sysfs" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (btrfs_init_sysfs) pending.

# VSCode 调试内核

由于 CONFIG_STRICT_KERNEL_RWX 的问题只能使用硬件断点,VSCode 对于硬件断点支持不太好,等寻找到更好的方案会继续更新。

对于不受 CONFIG_STRICT_KERNEL_RWX 影响的版本来说,只需要按如下配置设置 launch.json 即可:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "kernel-debug",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/vmlinux",
      "args": [],
      "stopAtEntry": false,
      "cwd": "${workspaceFolder}",
      "environment": [],
      "externalConsole": false,
      "logging": {
        "engineLogging": false
      },
      "MIMode": "gdb",
      "miDebuggerServerAddress": "127.0.0.1:1234",
      "setupCommands": [
        {
          "text": "-enable-pretty-printing",
          "ignoreFailures": true
        },
      ]
    }
  ]
}

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Gality 微信支付

微信支付

Gality 支付宝

支付宝