利用QEMU+GDB搭建Linux核心除錯環境

嵌入式與Linux那些事發表於2022-04-25

前言

對使用者態程式,利用gdb除錯程式碼是很方便的手段。而對於核心態的問題,可以利用crash等工具基於coredump檔案進行除錯。

其實我們也可以利用一些手段對Linux核心程式碼進行gdb除錯,qemu就是一種。

qemu是一款完全軟體模擬(Binary translation)的虛擬化軟體,在虛擬化的實現中效能相對較差。但利用它在測試環境中gdb除錯Linux核心程式碼,是熟悉Linux核心程式碼的一個好方法。

本文實驗環境:

  • ubuntu 20.04
  • busybox-1.32.1
  • Linux kernel 4.9.3
  • QEMU
  • GDB 10.1

編譯核心原始碼

git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
tar -xvzf linux-4.9.301.tar.gz
cd linux-4.9.301
make menuconfig

在核心編譯選項中,開啟如下"Compile the kernel with debug info"

Kernel hacking --->
  Compile-time checks and compiler options --->
    [ ] Compile the kernel with debug info

示意圖如下,利用鍵盤選中debug選項,然後敲"Y"勾選:

以上配置完成後會在當前目錄生成 .config 檔案,我們可以使用 grep 進行驗證:

grep CONFIG_DEBUG_INFO .config
CONFIG_DEBUG_INFO=y

編譯核心

make bzImage -j4

編譯完成後,會在當前目錄下生成vmlinux,這個在 gdb 的時候需要載入,用於讀取 symbol 符號資訊,包含了所有除錯資訊,所以比較大。

壓縮後的映象檔案為bzImage, 在arch/x86/boot/目錄下。

➜  linux-4.9.301 ls -hl vmlinux
-rwxrwxr-x 1 ubuntu ubuntu 578M Apr 15 08:14 vmlinux

➜  linux-4.9.301 ls -hl ./arch/x86_64/boot/bzImage
lrwxrwxrwx 1 ubuntu ubuntu 22 Apr 15 08:15 ./arch/x86_64/boot/bzImage -> ../../x86/boot/bzImage

➜  linux-4.9.301 ls -hl ./arch/x86/boot/bzImage 
-rw-rw-r-- 1 ubuntu ubuntu 9.3M Apr 15 08:15 ./arch/x86/boot/bzImage

幾種linux核心檔案的區別:

vmlinux 編譯出來的最原始的核心檔案,未壓縮。

zImage 是vmlinux經過gzip壓縮後的檔案。

bzImage bz表示“big zImage”,不是用bzip2壓縮的。兩者的不同之處在於,zImage解壓縮核心到低端記憶體(第一個640K)。

bzImage解壓縮核心到高階內 存(1M以上)。如果核心比較小,那麼採用zImage或bzImage都行,如果比較大應該用bzImage。

uImage U-boot專用的映像檔案,它是在zImage之前加上一個長度為0x40的tag。

vmlinuz 是bzImage/zImage檔案的拷貝或指向bzImage/zImage的連結。

initrd 是“initial ramdisk”的簡寫。一般被用來臨時的引導硬體到實際核心vmlinuz能夠接管並繼續引導的狀態。

編譯busybox

Linux系統啟動階段,boot loader載入完核心檔案vmlinuz後,核心緊接著需要掛載磁碟根檔案系統,但如果此時核心沒有相應驅動,無法識別磁碟,就需要先載入驅動。

而驅動又位於/lib/modules,得掛載根檔案系統才能讀取,這就陷入了一個兩難境地,系統無法順利啟動。

於是有了initramfs根檔案系統,其中包含必要的裝置驅動和工具,bootloader載入initramfs到記憶體中,核心會將其掛載到根目錄/,然後執行/init指令碼,掛載真正的磁碟根檔案系統。

這裡藉助BusyBox構建極簡initramfs,提供基本的使用者態可執行程式。

可以從busybox官網地址下載最新版本,或者直接使用wget下載我使用的版本。

wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2
$ tar -xvf busybox-1.32.1.tar.bz2
$ cd busybox-1.32.1/
$ make menuconfig

在編譯busybox之前,我們需要對其進行設定,執行make menuconfig,如下

這裡一定要選擇靜態編譯,編譯好的可執行檔案busybox不依賴動態連結庫,可以獨立執行,方便構建initramfs。

之後選擇Exit退出,到這裡我們就可以編譯busybox了,執行下面的命令

make -j 8
# 安裝完成後生成的相關檔案會在 _install 目錄下
make && make install

構建initramfs根檔案系統

[root@localhost temp]# ls
busybox-1.29.0  busybox-1.29.0.tar.bz2
[root@localhost temp]# mkdir initramfs
[root@localhost temp]# cd initramfs
[root@localhost initramfs]# cp ../busybox-1.32.1/_install/* -rf ./
[root@localhost initramfs]# mkdir dev proc sys
[root@localhost initramfs]# sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
[root@localhost initramfs]# rm -f linuxrc
[root@localhost initramfs]# vim init
[root@localhost initramfs]# chmod a+x init
[root@localhost initramfs]# ls
bin  dev  init  proc  sbin  sys  usr

其中init的內容如下

#!/bin/busybox sh
echo "{==DBG==} INIT SCRIPT"
mount -t proc none /proc
mount -t sysfs none /sys

echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
exec /sbin/init

打包initramfs

find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
[root@localhost initramfs]# ls ../
busybox-1.29.0  busybox-1.29.0.tar.bz2  initramfs  initramfs.cpio.gz

安裝QEMU

apt install qemu qemu-utils qemu-kvm virt-manager libvirt-daemon-system libvirt-clients bridge-utils

安裝GDB

wget https://ftp.gnu.org/gnu/gdb/gdb-10.1.tar.gz
tar -xzvf gdb-10.1.tar.gz
cd  gdb-10.1
./configure
# 必需要安裝這兩個庫
sudo apt-get install texinfo
sudo apt-get install build-essential
make -j 8
sudo make install

QEMU啟動除錯核心

➜  linux-4.9.301 qemu-system-x86_64 -kernel ./arch/x86/boot/bzImage -initrd ../initramfs.cpio.gz -append "nokaslr console=ttyS0" -s -S -nographic
  • -kernel ./arch/x86/boot/bzImage:指定啟用的核心映象;
  • -initrd ../initramfs.cpio.gz:指定啟動的記憶體檔案系統;
  • -append "nokaslr console=ttyS0" :附加引數,其中 nokaslr 引數必須新增進來,防止核心起始地址隨機化,這樣會導致 gdb 斷點不能命中;
  • -s :監聽在 gdb 1234 埠;
  • -S :表示啟動後就掛起,等待 gdb 連線;
  • -nographic:不啟動圖形介面,除錯資訊輸出到終端與引數 console=ttyS0 組合使用;

在另一個視窗中,輸入gdb,即可開啟除錯。

(gdb) target remote localhost:1234
Remote debugging using localhost:1234
warning: Can not parse XML target description; XML support was disabled at compile time
Remote 'g' packet reply is too long (expected 560 bytes, got 608 bytes): 0000000000000000000000000000000000000000000000006306000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f0ff0000000000000200000000f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f0300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000801f0000
(gdb) Remote debugging using localhost:1234
Undefined command: "Remote".  Try "help".
(gdb) warning: Can not parse XML target description; XML support was disabled at compile timeQuit

但是,在啟動GDP除錯時報錯了,在查閱了諸多資料後,很多部落格都給出了修復方法:原始碼重新安裝gdb,並修改gdb/remote.c檔案的一段程式碼。但是我嘗試了,發現行不通。

出現該問題的原因是:編譯 的是64 位模式的核心程式碼,但是執行是在 32 位保護模式下。64 位程式碼將無法在該環境中正常執行。

終於在stackflow上找到了修復方法:具體可以參考下面兩篇文章。

https://stackoverflow.com/questions/48620622/how-to-solve-qemu-gdb-debug-error-remote-g-packet-reply-is-too-long

https://wiki.osdev.org/QEMU_and_GDB_in_long_mode

文章中給出了三種修復方法,我這裡只列出了一種,即修改GDB原始碼,重新編譯安裝。

--- gdb/remote.c  	2016-04-14 11:13:49.962628700 +0300
+++ gdb/remote.c	2016-04-14 11:15:38.257783400 +0300
@@ -7181,8 +7181,28 @@
   buf_len = strlen (rs->buf);
 
   /* Further sanity checks, with knowledge of the architecture.  */
+// HACKFIX for changing architectures for qemu. It's ugly. Don't use, unless you have to.
+  // Just a tiny modification of the patch of Matias Vara (http://forum.osdev.org/viewtopic.php?f=13&p=177644)
   if (buf_len > 2 * rsa->sizeof_g_packet)
-    error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
+    {
+      warning (_("Assuming long-mode change. [Remote 'g' packet reply is too long: %s]"), rs->buf);
+      rsa->sizeof_g_packet = buf_len ;
+
+      for (i = 0; i < gdbarch_num_regs (gdbarch); i++)
+        {
+          if (rsa->regs[i].pnum == -1)
+            continue;
+
+          if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
+            rsa->regs[i].in_g_packet = 0;
+          else
+            rsa->regs[i].in_g_packet = 1;
+        }
+
+      // HACKFIX: Make sure at least the lower half of EIP is set correctly, so the proper
+      // breakpoint is recognized (and triggered).
+      rsa->regs[8].offset = 16*8;
+    }
 
   /* Save the size of the packet sent to us by the target.  It is used
      as a heuristic when determining the max size of packets that the
cd gdb-10.1
./configure
make -j 8
sudo make install

接著就可以敲gdb 啟動除錯。

➜  linux-4.9.301 gdb                                                                                                                             
GNU gdb (GDB) 10.1
Copyright (C) 2020 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 "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://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".
(gdb) file vmlinux
Reading symbols from vmlinux...
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
warning: Can not parse XML target description; XML support was disabled at compile time
warning: Assuming long-mode change. [Remote 'g' packet reply is too long: PU]
0x000000000000fff0 in exception_stacks ()
(gdb) break start_kernel
Breakpoint 1 at 0xffffffff81fc6a95: file init/main.c, line 486.
(gdb) break  rest_init
Breakpoint 2 at 0xffffffff818aa1e1: file init/main.c, line 385.
(gdb) c
Continuing.

Breakpoint 1, start_kernel () at init/main.c:486
486             set_task_stack_end_magic(&init_task);
(gdb) c
Continuing.

Breakpoint 2, rest_init () at init/main.c:385
385     {
(gdb) 

在start_kernel 和 rest_init 打了兩個斷點, 兩個斷點都成功命中了。

本文參考

https://www.shuzhiduo.com/A/kjdw2a2q5N/

https://cloud.tencent.com/developer/article/1793157

https://blog.csdn.net/alexanderwang7/article/details/113180447

https://blog.csdn.net/sjc2870/article/details/122017247

相關文章