# 编译内核
在网上大多数教程中都只说了要配置 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: UbuntuDescription: 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_RWX
和 CONFIG_STRICT_MODULE_RWX
来配置,一般是默认开启的。当这两个选项开启时, kgdb
添加断点时,默认是通过将指定内核代码改写为断点指令,从而在执行到指定位置时触发软件断点的,而该选项的存在会导致断点指令无法写入,所以才会出现如下错误:
(gdb) c | |
Continuing. | |
Warning: | |
Cannot insert breakpoint 1. | |
Cannot access memory at address 0xffffffff83220950 |
解决方法有两种:
- 在
gdb
中使用hb
替代b
来下硬件断点 - 在
.config
中关闭CONFIG_STRICT_KERNEL_RWX
和CONFIG_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 | |
}, | |
] | |
} | |
] | |
} |