---
# System prepended metadata

title: 利用 QEMU 與 GDB 追蹤 Linux 核心
tags: [Linux, OS]

---

---
title: 利用 QEMU 與 GDB 追蹤 Linux 核心
tags:
    - Linux
    - OS
description:
    GDB 全名為 GNU Debugger ，是 GNU 軟體系統中的標準除錯器。本篇文章主要是介紹如何編譯 Linux 核心並且在 QEMU 中運作，同時透過 GDB 追蹤 Linux 核心。
---

# 利用 QEMU 與 GDB 追蹤 Linux 核心

GDB 全名為 GNU Debugger ，是 GNU 軟體系統中的標準除錯器，在許多類 Unix 的作業系統中都能夠使用，而現有的 GDB 所能支援除錯的程式語言有 C 、 C++ 、 Pascal 以及 FORTRAN。本篇文章主要是介紹如何編譯 Linux 核心並且在 QEMU 中運作，同時透過 GDB 追蹤 Linux 核心。

:::warning
在本篇筆記的命令列範例中，若前綴為 $ 者，表示其執行在 host 端；前綴為 # 者，表示其須執行在 guest 端 (QEMU 內)；前綴為 (gdb) 者，表示其執行在 GDB 模式。
:::

測試環境如下：
```
OS: Ubuntu 22.04
ARCH: X86_64
Linux Kernel Source Version: 6.6
```

# Build Linux Kernel

首先需要安裝以下套件：

```shell
$ sudo apt update && sudo apt upgrade
$ sudo apt -y -q install  \
  bc                      \
  flex                    \
  bison                   \
  build-essential         \
  expect                  \
  git                     \
  libncurses-dev          \
  libssl-dev              \
  libelf-dev              \
  u-boot-tools            \
  wget                    \
  xz-utils                \
  qemu-kvm                \
  iproute2                \
  python3                 \
  python3-pip
```

新增一個專案資料夾並進入該資料夾。

```shell
$ mkdir -p linux-kernel && cd linux-kernel
```

下載 kernel 的 source code 並且 build 起來，可以使用 wget 下載核心壓縮檔或者使用 git clone 取得原始碼，這裡使用 wget 作為示範。
```shell
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz
$ tar -xvf linux-6.6.tar.xz
$ cd linux-6.6
$ make allnoconfig
```
> 使用 git clone:
> ```shell
> $ git clone --depth=5 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux
> $ cd linux
> $ git checkout -b linux-6.1.y origin/linux-6.1.y
> ```

接著設定 config。

```shell
$ make menuconfig
```

將下列所有選項都勾起來：
```shell
Linux/x86 6.6.0 Kernel Configuration

├─ General Setup
│   └─ Initial RAM filesystem and RAM disk (initramfs/initrd) support -> Enable
├─ 64-bit kernel -> Enable
├─ Process type and features 
│   └─ Linux guest support -> Enable
│       └─ Support for running PVH guests -> Enable
├─ Enable loadable module support -> Enable
├─ Executable file formats -> Enable all
├─ Device Drivers
│   └─ Character devices
│        └─ Serial drivers
│            ├─ 8250/16550 and compatible serial support -> Enable
│            └─ Console on 8250/16550 and compatible serial port -> Enable
└─ Kernel hacking
    ├─ Kernel debugging -> Enable
    └─ Compile-time checks and compiler options
        ├─ Debug information
        │   └─ Generate DWARF Version 4 debuginfo -> Enable
        └─ Provide GDB scripts for kernel debugging -> Enable
```

:::warning
這邊需要確保 `CONFIG_DEBUG_INFO` 以及 `CONFIG_GDB_SCRIPTS` 在組態中有被開啟，以利後續實驗使用。方法是在 `$ make` 命令前，執行：

```shell
$ grep CONFIG_DEBUG_INFO .config
$ grep CONFIG_GDB_SCRIPTS .config
```

預期要看到：

```shell
CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y
```
:::



編譯 kernel，參數 $(nproc) 代表系統最大核心數量。

```shell
$ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu- -j$(nproc)
```
> 針對 Arm64 處理器架構，將指令更改為：
>  ```shell
>  $ make ARCH=arm64 CORSS_COMPILE=aarch64-linux-gun- -j$(nproc)
>  ```

編譯結束後，預期會見到以下訊息：

```shell
Kernel: arch/x86/boot/bzImage is ready
```

在編譯完成後，額外執行以下命令：

```shell
$ make ARCH=x86 scripts_gdb
```
我們首先嘗試使用以下指令啟動核心。
```shell
$ qemu-system-x86_64 -kernel arch/x86/boot/bzImage -nographic -append "console=ttyS0"
```

基本的意思就是啟動一台 x86_64 架構處理器的 VM ，以 bzImage 映像檔作為核心 ，關閉圖形並將文字輸出到 ttyS0 裝置上。

沒有特別的狀況的話， 核心會開始啟動，直到處理器使用率被跑滿，畫面會停在這一句：

```shell
[    1.882581] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---
```

表示 kernel 找不到可以掛載的 rootfs (root filesystem) ，沒辦法啟動應該在裡面的 init process。這時候我們可以按下 Ctrl-A 放開再按下 X 按鍵來離開 QEMU 環境。

# Build Root FS

回到專案資料夾 `linux-kernel` 內下載 busybox 並編譯。

```shell
$ cd ..
$ wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
$ tar -xf busybox-1.36.1.tar.bz2
```

截至目前為止，專案資料夾結構應如下：

```shell
linux-kernel
├─ busybox-1.36.1
└─ linux-6.6
```

移動至 `busybox-1.36.1` 資料夾內並執行 `make menuconfig`。

```shell
$ cd busybox-1.36.1
$ make menuconfig 
```

選擇 `Settings ---> Build static binary` 並執行。

```shell
$ make install
```

接著要製作 mount 至 kernel 的資料夾。

```shell
$ cd _install
$ mkdir -p lib lib64 proc sys etc etc/init.d
```

寫入開機之後需要的腳本，首先利用 vim 建立 rcS 檔案。

```shell
$ vim ./etc/init.d/rcS
```

將以下腳本寫入至 rcS 並儲存 (# 為註解符號，並非執行於 QEMU 內)：
```shell
#!/bin/sh
# Mount the /proc and /sys filesystems
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
# Populate /dev
/sbin/mdev -s
```

:::warning
並非所有的檔案都需要 mount ，可視實際需求而定
:::

設定 rcS 腳本的權限並且建立 rootfs 的 image。
```shell
$ chmod +x etc/init.d/rcS
$ find . | cpio -o --format=newc | gzip > ../../linux-6.6/rootfs.img.gz
```

測試 kernel 是否能順利運作，首先移動至 linux-6.6 資料夾並啟動 QEMU 。
```shell
$ cd ../../linux-6.6
$ qemu-system-x86_64 -kernel vmlinux -nographic -initrd rootfs.img.gz -append "root=/dev/ram rdinit=/sbin/init console=ttyS0"
```

當看到以下畫面，代表順利進入 QEMU 執行環境，可以輸入 `ls` 指令做確認：
![image](https://hackmd.io/_uploads/H1rh_i_fke.png)

若要離開測試環境，可以按下 Ctrl-A 放開再按下 X 按鍵。


# 啟動 GDB

linux kernel 有提供一些 linux debug 用的 GDB 指令，可以修改 GDB 的設定檔讓 GDB 啟動時自動載入加入這些指令的 script

```shell
$ echo "add-auto-load-safe-path `pwd`/scripts/gdb/vmlinux-gdb.py" >> ~/.gdbinit
```

輸入以下指令以再次啟動 Linux 核心。

```shell
$ qemu-system-x86_64 -kernel vmlinux -nographic -initrd rootfs.img.gz -append "root=/dev/ram rdinit=/sbin/init console=ttyS0" -S -s
```
`-S` 參數是讓 QEMU 將 VM 啟動時就將 VM 停住等待 GDB 的指令， `-s` 參數則是讓 QEMU 會監聽 `port:1234` 的連線。nokaslr 的核心參數是停用隨機分配 kernel 運作位址的功能。

接著開啟新的終端機移動到 `linux/linux-6.6` 並且啟動 GDB 。

```shell
$ cd linux/linux-6.6
$ gdb vmlinux -tui
```

參數 `-tui` 將以 TUI (Text User Interface) 模式啟動 GDB ，這個功能能夠在 GDB 命令列頂部顯示一個包含原始程式碼的視窗。

進入到 GDB 模式後我們首先需要設定監聽的埠號

```shell
(gdb) target remote:1234
```

## 設定中斷點

接下來可以設定中斷點來觀察核心的運作情況，可以利用 GDB 將中斷點設定在以下函式，並查看其作用。

> 不同的 Linux 核心版本所提供函式可能不同，以本文中所使用的 x86 平台為例，下列的函式在核心版本 5.0 仍存在，但到了 6.0 之後則有些被移除。可透過 [Elixir Bootlin](https://elixir.bootlin.com/linux/v6.12.6/source/kernel) 比較不同核心版本之間的差異

| 函式                  | 說明                                                      |
| -------------------- | -------------------------------------------------------- |
| start_kernel         | 大概可以視為 Linux 的 main function                         |
| syscall_init         | 設定 system call 的進入點                                  |
| set_intr_gate        | 設定中斷向量表                                              |
| entry_SYSCALL_64     | System call 的進入點， `rax` 暫存器內放的是系統呼叫的編號       |
| apic_timer_interrupt | 時間中斷的進入點                                            |
| interrupt_entry      | 負責將所有暫存器 push 到堆疊，在中斷完成之後可以繼續原先的工作     |
| do_IRQ               | Linux 處理中斷的地方，中斷編號放在 `vector = ~regs->orig_ax;` |


首先我們將中斷點設定在 `start_kernel` ，並且輸入 c (continue) 讓程式繼續執行至設定的中斷點。

```shell
(gdb) b start_kernel
(gdb) c
```
![image](https://hackmd.io/_uploads/SkhTN3CEye.png)

可以發現 GDB 會將程式暫停在進入 `start_kerne()` (Line 871) 的位置，接下來可以透過輸入 n (next) 來進行單步執行。

```shell
(gdb) n
```

![image](https://hackmd.io/_uploads/r1Aer3ANJx.png)

在輸入 n 之後可以發現 GDB 將程式暫停在第 875 行的 `set_task_stack_end_magic()` ，若是想要進入該函式可以輸入 s (step) ，並且游標會移動至第 1096 行，也就是進入 `set_task_stack_end_magic()` 內。

```shell
(gdb) s
```

![image](https://hackmd.io/_uploads/rkY8HnCNJx.png)

當我們希望程式繼續執行至下一個中斷點時的時候可以輸入 c ，由於我們沒有設定其他的中斷點，因此可以發現在 QEMU 的環境內 Linux 核心會直接完成載入。

# 觀察 Linux 核心中斷機制

以 Intel 為例，在 [Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 3A](https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf) 的 Table 6-1 中提供了異常和中斷向量表 (Exceptions and Interrupts Table ，簡稱 IDT) ，並且列出了 21 個 traps 。

![image](https://hackmd.io/_uploads/Sy8RzlkUye.png)


接下來可以透過 GDB 觀察以下函式，來了解 Linux 核心的中斷機制。


| 函式名稱                  | 目的     |
| ----------------------- | -------- |
| `trap_init()`           | 由 `start_kernel()` (相當於 Linux 的 main) 呼叫，初始化 Intel 處理器的 traps                                        |
| `init_IRQ()`            | 由 `start_kernel()` (相當於 Linux 的 main) 呼叫，設定外部中斷的中斷向量表                                             |
| `native_init_IRQ`       | `init_IRQ` 呼叫此函數「真正」去設定中斷向量表                                                                        |
| `serial_link_irq_chain` | UART 驅動程式向 Linux 核心 註冊當 serial port 裝置發生中斷時，應該呼叫哪個函數，此函數屬於驅動程式的一部分                   |
| `common_interrupt`      | 所有的中斷服務函式都會「跳到」這段組合語言，它的主要功能是將所有的暫存器除存下來，然後呼叫 `do_IRQ` ，從 `do_IRQ` 開始就是 C 語言 |
| `serial8250_interrupt`  | 如果這個裝置會發出中斷，那麼這樣的函數就是開發驅動程式的人必須撰寫的「其中一部分」。此部份稱之為 top halve ，由 `common_ingterrupt->do_IRQ` 開始一層層呼叫，直到此函數 |


# 參考資料

- [Debugging kernel and modules via gdb](https://docs.kernel.org/dev-tools/gdb-kernel-debugging.html)
- [Preparing Linux for Kernel Debugging](https://www.intel.com/content/www/us/en/docs/programmable/730783/24-1/preparing-linux-for-kernel-debugging.html)
- [通過 QEMU + GDB 調適 Linux 內核](https://github.com/beacer/notes/blob/master/kernel/kernel-qemu-gdb.md)
- [用 gdb debug 在 QEMU 上跑的 Linux Kernel](https://blog.austint.in/2022/01/16/run-and-debug-linux-kernel-in-qemu-vm.html)
- [Debugging the Linux kernel with GDB](https://sergioprado.blog/debugging-the-linux-kernel-with-gdb/)
- [Booting a Custom Linux Kernel in QEMU and Debugging It With GDB](http://nickdesaulniers.github.io/blog/2018/10/24/booting-a-custom-linux-kernel-in-qemu-and-debugging-it-with-gdb/)