--- 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` 指令做確認:  若要離開測試環境,可以按下 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 ```  可以發現 GDB 會將程式暫停在進入 `start_kerne()` (Line 871) 的位置,接下來可以透過輸入 n (next) 來進行單步執行。 ```shell (gdb) n ```  在輸入 n 之後可以發現 GDB 將程式暫停在第 875 行的 `set_task_stack_end_magic()` ,若是想要進入該函式可以輸入 s (step) ,並且游標會移動至第 1096 行,也就是進入 `set_task_stack_end_magic()` 內。 ```shell (gdb) s ```  當我們希望程式繼續執行至下一個中斷點時的時候可以輸入 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 。  接下來可以透過 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/)
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up