ARM64 的 Linux 内核 kgdb/kdb 调试

2018年6月25日 19.27k 次阅读 1 条评论 10 人点赞

目录

什么是 gdb、kdb、kgdb?

GDB 是 GNU 开源组织发布的一个强大的 UNIX 下的程序调试工具,功能强大,支持远程调试、交叉调试等。被广泛应用于 UNIX 环境下的应用程序调试。kgdb 是仅用于调试内核的一种源码级调试器,kgdb 运行在核内,自身不能单独用来调试内核,需要远端的 gdb 通过串口或网络远程连接上来进行调试,连上之后就可以像调试核外应用程序一样调试内核,如设置断点、查看数据结构,打印调用栈等。kgdb 在内核 2.6.26 之前是作为一个独立于内核的社区项目,之后合并进内核,成为内核自带的一种调试工具。kdb 是 linux 内核的另一个调试器,在 kgdb 合进内核之前作为 SGI 的一个子项目独立运行。2009 年开始合并进 linux 2.6.35,与 kgdb 在代码上合二为一,共用一个 debug core,功能上又保持独立。kdb 提供一个 shell 形式的调试控制台,可让 kgb 和 kgbd 互相切换。kgdb 和 kdb 的关系可以参考下图:

kdb 只支持汇编级的调试,不像 kgdb 支持c语言级别的调试,只能通过裸内存地址查看变量、设置断点。其内建的命令也可以用来查看 cpu 调用栈,进程信息等。kdb 可单独使用,不需要远程机器的辅助。值得一提的是 gdb 连上 kgdb 之后可以使用 kdb 的全部功能,所以从开发者的角度来说 kgdb 更具有亲和性。

ARM64 平台下的 kgdb 完善

Gdb 调试器会访问系统的寄存器信息、处理器的状态等。因此,kgdb 的实现除了通用部分还需要各体系架构的特定支持。arm64 对 kgdb 的支持始于 linux 3.14-rc4,这个版本只提供基本的 demo,后来陆续增加断点功能、单步调试功能。基于4.4 内核下的 arm64 kgdb 并不完全成熟,在使用 gdb 远程调试 arm64 平台下 linux 内核时会存在一些问题,包括客户端和 target 端。为了使 kgdb 在 arm64 平台下达到基本可用的状态,需要做相应修复。

Target 端的 kgdb 完善

远端的 gdb 连上 linux 的 kgdb 之后,在断点处执行单步调式(step/next)的时候,调式器并不是执行断点处的语句,而是每次都陷入到下面的代码段:

arch/arm64/kernel/entry.S:356
el1_irq:
        kernel_entry 1
        enable_dbg

社区也有人碰到这个问题,并提交了如下 patch 来修复这个问题。这个问题并不是在所有的 arm64 平台上都会碰到,patch 还在讨论并没有合进 upstream,Patch 如下:

Subject: [PATCH] KERNEL: arm64, debug: disable interrupts while a software
 step is enabled

This patch enforce interrupts to be masked while single stepping. Without
this patch, we will alway fall into arm64/kernel/entry.S while issue step
or next operate.
---
 arch/arm64/kernel/kgdb.c | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/arch/arm64/kernel/kgdb.c b/arch/arm64/kernel/kgdb.c
index bcac81e..e83b960 100644
--- a/arch/arm64/kernel/kgdb.c
+++ b/arch/arm64/kernel/kgdb.c
@@ -23,6 +23,10 @@
 #include <linux/kdebug.h>
 #include <linux/kgdb.h>
 #include <asm/traps.h>
+#include <asm/ptrace.h>
+
+
+static DEFINE_PER_CPU(unsigned int, kgdb_pstate);

 struct dbg_reg_def_t dbg_reg_def[DBG_MAX_REG_NUM] = {
        { "x0", 8, offsetof(struct pt_regs, regs[0])},
@@ -188,6 +192,9 @@ int kgdb_arch_handle_exception(int exception_vector, int signo,
                err = 0;
                break;
        case 's':
+
+               __this_cpu_write(kgdb_pstate, linux_regs->pstate);
+               linux_regs->pstate |= PSR_I_BIT;
                /*
                 * Update step address value with address passed
                 * with step packet.
@@ -229,6 +236,17 @@ static int kgdb_compiled_brk_fn(struct pt_regs *regs, unsigned int esr)

 static int kgdb_step_brk_fn(struct pt_regs *regs, unsigned int esr)
 {
+       unsigned int pstate;
+
+       if (!kgdb_single_step)
+               return DBG_HOOK_ERROR;
+       kernel_disable_single_step();
+
+       pstate = __this_cpu_read(kgdb_pstate);
+       if (pstate & PSR_I_BIT)
+               regs->pstate |= PSR_I_BIT;
+       else
+               regs->pstate &= ~PSR_I_BIT;
        kgdb_handle_exception(1, SIGTRAP, 0, regs);
        return 0;
 }
--
2.7.4

问题的原因在于 kgdb 在断点处没有关闭处理器的中断能力,在单步执行的时候处理器会接收中断信号,比如时钟中断,从而转向中断处理,正如上面第一个表格呈现的那样。所以修复方法就是单步执行的时候关闭 cpu 的中断处理机制,continue 的时候恢复中断处理。

Host 端的 gdb 更改

基于 ubuntu 16.04 的 gdb-7.11 去远程连接 linux 内核的 kgdb 的时候协议会报错,提示:Remote 'g' packet reply is too long,然后连接被中断。需打上如下补丁:

--- gdb-7.11.1.orig/gdb/remote.c
+++ gdb-7.11.1/gdb/remote.c
@@ -7208,8 +7208,19 @@ process_g_packet (struct regcache *regca
   buf_len = strlen (rs->buf);

   /* Further sanity checks, with knowledge of the architecture.  */
-  if (buf_len > 2 * rsa->sizeof_g_packet)
-    error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
+
+if (buf_len > 2 * rsa->sizeof_g_packet) {
+    rsa->sizeof_g_packet = buf_len ;
+    for (i = 0; i < gdbarch_num_regs (gdbarch); i++) {
+        if (rsa->regs->pnum == -1)
+            continue;
+        if (rsa->regs->offset >= rsa->sizeof_g_packet)
+            rsa->regs->in_g_packet = 0;
+        else
+            rsa->regs->in_g_packet = 1;
+    }
+}

gdb 支持交叉调试,例如可以使用 x86 上的 gdb 程序远程连接 arm64 平台上的 kgdb。如需在x86 上远程调试 linux 内核也需要打上上面的 patch,并使用 gdb 专用的交叉调试程序:gdb-multiarch。

ARM64 平台下 kgdb 远程调试示例

示例演示在 x86 平台上使用 gdb-multiarch 远程交叉调试 Linux 内核。kgdb 远程调试支持两种连接方式,一种是使用串口通信,另一种是使用网络。linux 内核已集成对串口通信的支持,网络通信需要下载第三方模块,且社区已经没人维护网络通信的代码,因此基本上都会选择串口作为调试的通信接口。本示例就是使用 kgdboc(kgdb over console) 作为 kgdb 调试的通信载体。

target 端和 host 端的准备

对于 target 端,即被调试的 klinux,需要打开以下或者关闭某些内核选项。如下表所示:

配置选项
备注
CONFIG_DEBUG_INFO=y 使内核包含调试信息
# CONFIG_DEBUG_RODATA is not set 该选项会使内核代码段变的只读,从而不可设置断点
CONFIG_FRAME_POINTER=y 使调试器能打印更精细的栈信息
CONFIG_KGDB=y 这两个选项支持 kdb & kgdb
CONFIG_KGDB_KDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y kgdb通过串口通信
# CONFIG_DEBUG_SET_MODULE_RONX is not set 关闭该选项,否则模块调试时不可设断点。

使用带 kgdb 调试选项的内核配置文件重新编译完内核之后,还需编译一个带调试信息的 vmlinux 供远程的 gdb 使用。可通过如下方式编译:

[root@Kylin ~]# make vmlinux

编译完了之后,在 uboot 启动内核的时候需添加 kgdboc=ttyS0,115200 的启动参数来激活 kgdb。ttyS0 是使用的串口设备,115200 是串口使用的 baudrate。如下所示:

uboot# setenv bootargs console=ttyS0,115200 earlyprintk=uart8250-32bit,0x28001000
                root=/dev/sda2 rootwait rw rootdelay=10 kgdboc=ttyS0,115200

进入系统之后,使用 magic key 触发kdb,暂停系统的运行。

[root@Kylin ~]# echo g  >  /proc/sysrq-trigger

进入 kdb 交互式 shell 之后,可以使用 help 命令查看 kdb 支持的调试功能,并尝试使用之。前面说过 kdb 和 kgdb 可通过交互 shell 自己切换,在交互式 shell 下输入 kgdb 即可使内核陷入 kgdb模式,等待远程 gdb 的连接。

[1] kdb> kgdb

陷入 kgdb 之后想再次切换回 kdb 可按顺序键入字符: $3#33。整个操作过程如下图所示:

对于 host 端,即运行 gdb-multiarch 的一端,使用带 patch 的 gdb-multiarch 替换系统现有的 gdb-multiarch,将上面准备好的 vmlinux(最好是整个编译目录)拷贝到 host 上。对于连接到 x86 上的串口设备,如果既想做控制台使用又想同时作为 gdb 通信的载体,两者之间会存在干扰,可以从以下 site 下载一个串口多路复用的软件 agent-proxy,编译,执行。该软件单独使用也可替代 Minicom 的功能。解压之后可按如下方式运行该软件:

[root@Kylin ~]# ./agent-proxy  5550^5551 0 /dev/ttyUSB0,115200 -s003

关于该软件的原理和各个参数的意义可查看里面的 README 文件。开启一个终端执行以下命令:

[root@Kylin ~]# telnet localhost 5550

连上之后即可当作串口控制台使用。再起一个终端,通过以下操作连接远端的 kgdb:

[root@Kylin ~]# gdb-multiarch vmlinux
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
Reading symbols from ./vmlinux...done.
warning: File "/root/klinux-x86/klinux/scripts/gdb/vmlinux-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
    add-auto-load-safe-path /root/klinux-x86/klinux/scripts/gdb/vmlinux-gdb.py
line to your configuration file "/root/.gdbinit".
To completely disable this security protection add
    set auto-load safe-path /
line to your configuration file "/root/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
    info "(gdb)Auto-loading safe path"

(gdb) set architecture aarch64
The target architecture is assumed to be aarch64

(gdb) target remote localhost:5551
Remote debugging using localhost:5551
arch_kgdb_breakpoint () at ./arch/arm64/include/asm/kgdb.h:32
32      asm ("brk %0" : : "I" (KGDB_COMPILED_DBG_BRK_IMM));

KGDB 的基本使用

连上 kgdb 之后,即可以使用像使用 gdb 调试本地应用程序一样调试内核代码的运行,下面展示以下基本的使用:

(gdb) thread
[Current thread is 232 (Thread 902)]

(gdb) bt
#0  arch_kgdb_breakpoint () at ./arch/arm64/include/asm/kgdb.h:32
#1  kgdb_breakpoint () at kernel/debug/debug_core.c:1071
#2  0xffffffc000146890 in sysrq_handle_dbg (key=<optimized out>) at kernel/debug/debug_core.c:825
#3  0xffffffc0004458c8 in __handle_sysrq (key=103, check_mask=false) at drivers/tty/sysrq.c:546
#4  0xffffffc000445d54 in write_sysrq_trigger (file=<optimized out>,
    buf=0x3dbbd408 "g\nt-file 是 /usr/bin/apt-file\n", '\337' <repeats 168 times>, <incomplete sequence \337>..., count=2, ppos=<optimized out>)
    at drivers/tty/sysrq.c:1099
#5  0xffffffc0002825dc in proc_reg_write (file=0xffffffc8dabe1600, buf=<optimized out>, count=<optimized out>, ppos=<optimized out>)
    at fs/proc/inode.c:216
#6  0xffffffc0002148b0 in __vfs_write (file=0xffffffc8dabe1600, p=<optimized out>, count=<optimized out>, pos=<optimized out>) at fs/read_write.c:489
#7  0xffffffc0002151f8 in vfs_write (file=0xffffffc8dabe1600,
    buf=0x3dbbd408 "g\nt-file 是 /usr/bin/apt-file\n", '\337' <repeats 168 times>, <incomplete sequence \337>..., count=<optimized out>,
    pos=0xffffffc8d62dfec8) at fs/read_write.c:562
#8  0xffffffc000215df8 in SYSC_write (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:609
#9  SyS_write (fd=<optimized out>, buf=<optimized out>, count=<optimized out>) at fs/read_write.c:601
#10 0xffffffc000085c70 in el0_svc () at arch/arm64/kernel/entry.S:685
Backtrace stopped: previous frame identical to this frame (corrupt stack?)

(gdb) b md_ioctl
Breakpoint 1 at 0xffffffc0007d2b88: file drivers/md/md.c, line 6697.

(gdb) c
Continuing.
[New Thread 990]
[New Thread 974]
[New Thread 981]
[New Thread 991]

此时,target 已恢复正常运行,为了触发断点,在 target 上创建一个 raid0。创建的过程会调用 md_ioctl 函数:

如果图示,raid0 还没创建完就触发了断点,系统被挂起。再看远端的 gdb 是否停留在断点处:

(gdb) b md_ioctl
Breakpoint 1 at 0xffffffc0007d2b88: file drivers/md/md.c, line 6697.
(gdb) c
Continuing.
[New Thread 990]
[New Thread 974]
[New Thread 981]
[New Thread 991]
[Switching to Thread 990]

Thread 238 hit Breakpoint 1, md_ioctl (bdev=0xffffffc8d8dd09c0, mode=393375, cmd=2148272400, arg=548991617032) at drivers/md/md.c:6697
6697    {
(gdb)

在 gdb 交互界面使用快捷键 Ctrl+x+a 切出源码调试界面:

现在可以使用 step/next 单步调试程序的运行,使用 p 命令打印数据结构。

该函数在 raid 的创建过程中会被多次调用,可以看到使用 c 命令之后会再次触发断点。多次 continue 之后,raid0 创建成功,系统继续正常运行。下面在 target 上人为的触发一次 panic,让系统崩溃:

回到远端的 gdb 上来,如下图所示,gdb catch 到了这个异常,并自动定位到了造成出错的代码行,还给出了出错的原因是段错误,在断点处可以打印函数栈以及数据结构来进一步调试出错的具体原因。

在开发环境下我们可以激活 kgdb,让它捕捉系统的异常和崩溃,从而快速定位到代码出错的地方,提升我们调试代码的效率。

调试 kernel 的启动过程

kgdb 也可以用来调试内核的启动过程,当然只能调试 kgdb 自身初始化之后的内核启动过程,kgdb 依赖于串口通信,所以会在串口初始化完成之后启动 kgdb。对于 kgdb 本身初始化完成之前的内核启动过程我们可以借助 qemu 等虚拟机来调试。要激活 kgdb 的内核启动过程调试,需要在内核启动的时候增加一个内核启动参数: kgdbwait。 如下所示,在 uboot 启动时可设置:

uboot# setenv bootargs console=ttyS0,115200 earlyprintk=uart8250-32bit,0x28001000 root=/dev/sda2
                rootwait rw rootdelay=10 kgdboc=ttyS0,115200 kgdbwait

target 在完成 kgdb 的初始化之后会暂停内核的执行,并等待远端的 gdb 连接,如下所示:

target 停住以后,在 x86 上使用 gdb 按照之前所述的方式连接 kgdb。连上之后如下所示:

(gdb) target remote localhost:5551
Remote debugging using localhost:5551
arch_kgdb_breakpoint () at ./arch/arm64/include/asm/kgdb.h:32
32      asm ("brk %0" : : "I" (KGDB_COMPILED_DBG_BRK_IMM));
(gdb) bt
#0  arch_kgdb_breakpoint () at ./arch/arm64/include/asm/kgdb.h:32
#1  kgdb_breakpoint () at kernel/debug/debug_core.c:1071
#2  0xffffffc000147c7c in kgdb_initial_breakpoint () at kernel/debug/debug_core.c:973
#3  kgdb_register_io_module (new_dbg_io_ops=0xffffffc001cd3b00 <kgdb_use_con>) at kernel/debug/debug_core.c:1013
#4  0xffffffc000467f6c in configure_kgdboc () at drivers/tty/serial/kgdboc.c:200
#5  0xffffffc001aea0d4 in init_kgdboc () at drivers/tty/serial/kgdboc.c:229
#6  0xffffffc000082b90 in do_one_initcall (fn=0xffffffc001aea0b0 <init_kgdboc>) at init/main.c:837
#7  0xffffffc001ab6c50 in do_initcall_level (level=<optimized out>) at init/main.c:902
#8  do_initcalls () at init/main.c:910
#9  do_basic_setup () at init/main.c:928
#10 kernel_init_freeable () at init/main.c:1051
#11 0xffffffc00094eb48 in kernel_init (unused=<optimized out>) at init/main.c:979
#12 0xffffffc000085c10 in ret_from_fork () at arch/arm64/kernel/entry.S:661
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb)

之后我们就可以在内核初始化路径中设置断点,调试其运行过程。如下所示,我们在rtc的某个函数处设置断点:

(gdb) b rtc_hctosys
Breakpoint 1 at 0xffffffc001af1e60: file drivers/rtc/hctosys.c, line 28.
(gdb) c
Continuing.
[New Thread 149]
[New Thread 150]
...................
Thread 17 hit Breakpoint 1, rtc_hctosys () at drivers/rtc/hctosys.c:28
28  {
(gdb) list
23   * slow down the sync API. So here we have the truncated value and
24   * the best guess is to add 0.5s.
25   */
26
27  static int __init rtc_hctosys(void)
28  {
29      int err = -ENODEV;
30      struct rtc_time tm;
31      struct timespec64 tv64 = {
32          .tv_nsec = NSEC_PER_SEC >> 1,
(gdb)

内核在运行到该函数处的时候会触发断点,并暂停内核引导过程。如果想查看内核各个自模块初始化顺利和过程,可以在 do_one_initcall 函数处设置断点,该函数负责加载各个模块的初始化函数。

调试动态加载的模块

有时候我们需要调试一个内核模块,比如存储子系统某些 iscsi target 功能必须编译成内核模块,这个时候就可以使用 kgdb 的模块调试功能来定位代码问题。但是,模块调试也有其局限性,一个是不方面调试模块的初始化函数,另一个就是配置有点麻烦。所以,作为开发阶段,建议将模块直接编译进内核,方便调试。假设使用 raid0 作为我们模块调试用的演示模块。为了减少代码的优化,方便调试器查看变量信息,我们可以在模块编译的时候更改默认的 -O2 优化等级为 -O0 或者 -O1。如下:

--- a/drivers/md/Makefile
+++ b/drivers/md/Makefile
@@ -2,6 +2,8 @@
 # Makefile for the kernel software RAID and LVM drivers.
 #

+EXTRA_CFLAGS   += -O0
+
 dm-mod-y       += dm.o dm-table.o dm-target.o dm-linear.o dm-stripe.o \
                   dm-ioctl.o dm-io.o dm-kcopyd.o dm-sysfs.o dm-stats.o
 dm-multipath-y += dm-path-selector.o dm-mpath.o

有的模块不支持 -O0 编译,需做一些代码调整。raid0 编译好之后需要给 target 和 host 都准备一份。在 target 上先以 kgdboc 的参数启动内核,然后加载 raid0 模块,获取模块在内核中的代码段地址,该地址供远端 Host 上的 gdb 加载 raid0 符号表时使用:

[root@Kylin ~]# lsmod
Module                  Size  Used by
raid0                  12386  0
[root@Kylin ~]# cat /sys/module/raid0/sections/.text
0xffffffbffc000000

示例中 raid0 的代码段地址为:0xffffffbffc000000。获取到之后使用 magic key 触发 kgdb,等待远端 gdb 的连接。在 host 上,使用 gdb 的 add-symbol-file 命令加载 raid0 的符号表:

[root@Kylin ~]# gdb-multiarch ./vmlinux
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
................
(gdb) set architecture aarch64
The target architecture is assumed to be aarch64
(gdb) add-symbol-file drivers/md/raid0.ko 0xffffffbffc000000
add symbol table from file "drivers/md/raid0.ko" at
    .text_addr = 0xffffffbffc000000
(y or n) y
Reading symbols from drivers/md/raid0.ko...done.

注意,本示例是将整个源码编译目录拷贝到了 host 上,add-symbol-file 第二个参数,即模块位置,视具体模块而定。接下来连接处于等待状态的 target,连上之后设置模块内的断点函数,并恢复内核运行:

(gdb) target remote localhost:5551
Remote debugging using localhost:5551
arch_kgdb_breakpoint () at ./arch/arm64/include/asm/kgdb.h:32
32      asm ("brk %0" : : "I" (KGDB_COMPILED_DBG_BRK_IMM));
(gdb) b raid0_run
Breakpoint 1 at 0xffffffbffc001ab8: file drivers/md/raid0.c, line 356.
(gdb) b raid0_make_request
Breakpoint 2 at 0xffffffbffc001f70: file drivers/md/raid0.c, line 457.
(gdb) list raid0_make_request
452                         + bio_sectors(bio));
453     }
454 }
455
456 static void raid0_make_request(struct mddev *mddev, struct bio *bio)
457 {
458     struct strip_zone *zone;
459     struct md_rdev *tmp_dev;
460     struct bio *split;
461
(gdb) c
Continuing.

在 target 端我们使用两个 loop 设备来创建一个 raid0, raid0 创建完成的时候会调用 raid0_run 函数来驱动 raid0 的运行。

从图中可以看到,创建的过程触发了断点,使整个内核陷入到 kgdb 的挂起状态。回到 host,gdb 已处于模块内的断点函数 raid0_run:

(gdb) bt
#0  raid0_run (mddev=0xffffffc8db976000) at drivers/md/raid0.c:356
#1  0xffffffc0007cc9d8 in md_run (mddev=0xffffffc8db976000) at drivers/md/md.c:5257
#2  0xffffffc0007cd458 in do_md_run (mddev=0xffffffc8db976000) at drivers/md/md.c:5346
#3  0xffffffc0007d3df4 in md_ioctl (bdev=0xffffffc07b423a80, mode=393375, cmd=1074530608, arg=549590216496) at drivers/md/md.c:6973
#4  0xffffffc00037c110 in __blkdev_driver_ioctl (arg=<optimized out>, cmd=<optimized out>, mode=<optimized out>, bdev=<optimized out>)
    at block/ioctl.c:288
#5  blkdev_ioctl (bdev=0xffffffc07b423a80, mode=<optimized out>, cmd=<optimized out>, arg=549590216496) at block/ioctl.c:581
#6  0xffffffc000251a0c in block_ioctl (file=0xffffffc8d9685f00, cmd=1074530608, arg=<optimized out>) at fs/block_dev.c:1756
#7  0xffffffc000227d88 in vfs_ioctl (arg=<optimized out>, cmd=<optimized out>, filp=<optimized out>) at fs/ioctl.c:43
#8  do_vfs_ioctl (filp=0xffffffc8d9685f00, fd=<optimized out>, cmd=1074530608, arg=549590216496) at fs/ioctl.c:607
#9  0xffffffc000228070 in SYSC_ioctl (arg=<optimized out>, cmd=<optimized out>, fd=<optimized out>) at fs/ioctl.c:622
#10 SyS_ioctl (fd=4, cmd=1074530608, arg=549590216496) at fs/ioctl.c:613
#11 0xffffffc000085c70 in el0_svc () at arch/arm64/kernel/entry.S:685
Backtrace stopped: previous frame identical to this frame (corrupt stack?)

使用快捷键切到源码调试界面,然后使用章节《KGDB 的基本使用》中介绍的方法对模块运行逻辑进行调试。raid0_make_request 是 raid0 的请求函数,只要是 raid0 有 I/O 读写都会触发该断点,在此不做演示,有兴趣可以尝试。

利用 qemu 调试 Linux 内核

kgdb/kdb 拥有种种优点,但是还是拥有一些限制,比如无法调试内核启动函数 start_kernel,这个函数会在内核启动时注册中断,内存初始化以及等等其他的外设初始化,原因也很容易理解,那就是在这个阶段,kgdb/kdb 尚且没有启动,自然无法通过串口或者网络对他们进行调试。幸好,还有 qemu,当内核运转在 qemu 之上时,我们可以理解 linux 内核为一段软件,既然是软件,那么他就可以被断下来,自然也可以一行行进行调试,完成 kgdb 无法完成的工作。

那就开始,首先需要准备好 qemu-system-aarch64 二进制程序,它用来模拟整个系统硬件环境,qemu 包含两个大的部分,一个用来模拟各个架构的核外应用程序,另外一部分则可以用来模拟整个系统硬件,这可以从名称分得清楚。

安装 qemu-system-aarch64 与 aarch64-linux-gnu-gdb

安装 qemu-system-aarch64 很简单,我的本机是 MacOS,所以我直接使用的是

[Jackieliu@JackieLiu-MacBookPro.local ~]# brew install qemu

当然,在 Ubuntu/debian 系统上直接调用

[root@Kylin ~]# apt install qemu-system-aarch64

利用 qemu 进行 Linux 内核调试同样需要分为 target 端和 host 端,所以也是需要进行远程连接调试(当然不必真正的远程,一切都可以位于你的本机),那么仍然需要利用 gdb 连接到 target 进行调试,这里的 target 就是 qemu 虚拟机,首先我们安装 gdb,这个 gdb 需要特别注意的是,那是需要能够运行 arm64 二进制的 gdb,可以采用文前提供的 gdb-multiarch 且设置交叉编译架构,当然也可以重新编译 gdb 直接在参数中指定需要调试的目标为 aarch64 指令,对于 MacOS 直接安装即可

[Jackieliu@JackieLiu-MacBookPro.local ~]# brew install aarch64-linux-gnu-gdb

对于 Ubuntu/debian 系统,直接使用系统的即可,然后在 gdb 中设置set architecture aarch64完成架构指定,如果不支持,最好是利用 gdb 源码重新编译,如果你直接看的这一章节,那么不需要打上之前的 gdb 相关的 patch,对于 qemu 来讲,一切都是 ready 的。

开始调试

第一步准备需要调试的内核 vmlinux、Image,将他们拷贝到调试机(host 本机)的源码目录(你需要准备一份源码到本机),qemu-system-aarch64 需要运行 Image,aarch64-linux-gnu-gdb 需要加载 vmlinx。

运行如下命令:

[Jackieliu@MacBookPro ~]# qemu-system-aarch64 -machine virt -cpu cortex-a57 -machine type=virt
                -nographic -smp 2 -m 2048 -kernel Image -hda ./rootfs.img
                -append "earlyprintk console=ttyAMA0 root=/dev/vda rw" -s -S

其中 rootfs.img 就是核外文件系统,这个可有可无,没有的话就进不去核外系统而已,当然,如果需要,你可以自己 dd 一个 image,然后 mkfs 该 image,mount 之后将核外文件系统导入到这个 image 之中即可,很简单就不做演示。这些参数中尤其需要注意的就是最后的-s -S 参数,他们表示让 qemu 虚拟机暂停等待以及单步执行,这对于调试很关键。运行命令之后得到下图:

另外打开一个终端窗口,连接 qemu 进行调试输入 target remote localhost:1234,qemu 的默认端口就是 1234,一般不需要修改。

[Jackieliu@MacBookPro ~]# aarch64-linux-gnu-gdb vmlinux
GNU gdb (GDB) 8.0
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin16.6.0 --target=aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
/Users/jackieliu/.gdbinit:1: Error in sourced command file:
No symbol table is loaded.  Use the "file" command.
Reading symbols from vmlinux...done.
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000000040000000 in ?? ()
(gdb)

打上断点到 start_kernel,然后按 c 开始调试吧

(gdb) b start_kernel
Breakpoint 1 at 0xffffffc001b615c4: file init/main.c, line 511.
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, start_kernel () at init/main.c:511
warning: Source file is more recent than executable.
511 {
(gdb)

可以通过按键 Ctrl+x+a 切换到如图源码视图进行调试。

编译参数 -O0 的艺术

利用 qemu 进行调试的时候,可以将内核编译为 -O0 进行调试(通过实验,在实际物理机无法使用 -O0 进行调试,会出现无法预期的各种奔溃错误,但是在 qemu 环境可以正确运行)。在 Linux 内核中加入下面的 Patch,可以满足基本的 qemu 调试需求。

From 22819216f63904da19c3ffd65595560ecabe609e Mon Sep 17 00:00:00 2001
From: Ben Shushu <runninglinuxkernel@126.com>
Date: Sat, 27 May 2017 17:41:07 +0800
Subject: [PATCH] change the gcc compile option form "-02" to "-00" to impove
 the debug process

---
 Makefile                            | 4 ++--
 arch/arm64/include/asm/jump_label.h | 4 ++++
 include/linux/compiler.h            | 4 ++++
 3 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile
index 8c087c8..5407f85 100644
--- a/Makefile
+++ b/Makefile
@@ -665,9 +665,9 @@ KBUILD_CFLAGS   += $(call cc-disable-warning,maybe-uninitialized,)
 KBUILD_CFLAGS  += $(call cc-option,-fno-store-merging)
 else
 ifdef CONFIG_PROFILE_ALL_BRANCHES
-KBUILD_CFLAGS  += -O2
+KBUILD_CFLAGS  += -O0
 else
-KBUILD_CFLAGS   += -O2
+KBUILD_CFLAGS   += -O0
 KBUILD_CFLAGS   += $(call cc-option,-fno-store-merging)
 endif
 endif
diff --git a/arch/arm64/include/asm/jump_label.h b/arch/arm64/include/asm/jump_label.h
index 1b5e0e8..e937f62 100644
--- a/arch/arm64/include/asm/jump_label.h
+++ b/arch/arm64/include/asm/jump_label.h
@@ -28,6 +28,7 @@

 static __always_inline bool arch_static_branch(struct static_key *key, bool branch)
 {
+#if 0
    asm goto("1: nop\n\t"
         ".pushsection __jump_table,  \"aw\"\n\t"
         ".align 3\n\t"
@@ -37,11 +38,13 @@ static __always_inline bool arch_static_branch(struct static_key *key, bool bran

    return false;
 l_yes:
+#endif
    return true;
 }

 static __always_inline bool arch_static_branch_jump(struct static_key *key, bool branch)
 {
+#if 0
    asm goto("1: b %l[l_yes]\n\t"
         ".pushsection __jump_table,  \"aw\"\n\t"
         ".align 3\n\t"
@@ -51,6 +54,7 @@ static __always_inline bool arch_static_branch_jump(struct static_key *key, bool

    return false;
 l_yes:
+#endif
    return true;
 }

diff --git a/include/linux/compiler.h b/include/linux/compiler.h
index 6fc9a6d..ffb1b5e 100644
--- a/include/linux/compiler.h
+++ b/include/linux/compiler.h
@@ -459,6 +459,7 @@ static __always_inline void __write_once_size(volatile void *p, void *res, int s
 # define __compiletime_error_fallback(condition) do { } while (0)
 #endif

+#if 0
 #define __compiletime_assert(condition, msg, prefix, suffix)       \
    do {                                \
        bool __cond = !(condition);             \
@@ -482,6 +483,9 @@ static __always_inline void __write_once_size(volatile void *p, void *res, int s
  */
 #define compiletime_assert(condition, msg) \
    _compiletime_assert(condition, msg, __compiletime_assert_, __LINE__)
+#else
+#define compiletime_assert(condition, msg) do { } while (0)
+#endif

 #define compiletime_assert_atomic_type(t)              \
    compiletime_assert(__native_word(t),                \
diff --git a/arch/arm64/mm/Makefile b/arch/arm64/mm/Makefile
index 54bb209..54791f8 100644
--- a/arch/arm64/mm/Makefile
+++ b/arch/arm64/mm/Makefile
@@ -2,6 +2,7 @@ obj-y                           := dma-mapping.o extable.o fault.o init.o \
                                   cache.o copypage.o flush.o \
                                   ioremap.o mmap.o pgd.o mmu.o \
                                   context.o proc.o pageattr.o
+CFLAGS_init.o          += -O1
 obj-$(CONFIG_HUGETLB_PAGE)     += hugetlbpage.o
 obj-$(CONFIG_ARM64_PTDUMP)     += dump.o
 obj-$(CONFIG_NUMA)             += numa.o
--
2.7.4

编译过程中会出现某些模块要求的 symbol 不存在的错误,不过可以不必理他,直接关闭这些模块即可,暂时我们不需要调试这些模块,而且调试模块的话可以使用 kgdb/kdb 组合进行调试,没必要采用 qemu 进行调试。

配置选项
备注
# CONFIG_BCACHE is not set bcache 模块
# CONFIG_BTRFS_FS is not set btrfs 模块
# CONFIG_TEST_STATIC_KEYS is not set 静态 key 测试模块

假如遇到其他的模块无法编译通过,按照上面的思路,继续关闭即可。

本文由 Zhengyuan Liu 完成前面三章节,由我完成后面最后一章节,并由我上传至本站