Linux內核0-使用QEMU和GDB調試Linux內核

原文地址:Linux內核0-使用QEMU和GDB調試Linux內核

(文章大部分轉載於:https://consen.github.io/2018/01/17/debug-linux-kernel-with-qemu-and-gdb/

排查Linux內核Bug,研究內核機制,除了查看資料閱讀源碼,還可通過調試器,動態分析內核執行流程。

QEMU模擬器原生支持GDB調試器,這樣可以很方便地使用GDB的強大功能對操作系統進行調試,如設置斷點;單步執行;查看調用棧、查看寄存器、查看內存、查看變量;修改變量改變執行流程等。

編譯調試版內核

對內核進行調試需要解析符號信息,所以得編譯一個調試版內核。

$ cd linux-4.14
$ make menuconfig
$ make -j 20

這裏需要開啓內核參數CONFIG_DEBUG_INFOCONFIG_GDB_SCRIPTS。GDB提供了Python接口來擴展功能,內核基於Python接口實現了一系列輔助腳本,簡化內核調試,開啓CONFIG_GDB_SCRIPTS參數就可以使用了。

Kernel hacking  ---> 
    [*] Kernel debugging
    Compile-time checks and compiler options  --->
        [*] Compile the kernel with debug info
        [*]   Provide GDB scripts for kernel debugging

構建initramfs根文件系統

Linux系統啓動階段,boot loader加載完內核文件vmlinuz後,內核緊接着需要掛載磁盤根文件系統,但如果此時內核沒有相應驅動,無法識別磁盤,就需要先加載驅動,而驅動又位於/lib/modules,得掛載根文件系統才能讀取,這就陷入了一個兩難境地,系統無法順利啓動。於是有了initramfs根文件系統,其中包含必要的設備驅動和工具,boot loader加載initramfs到內存中,內核會將其掛載到根目錄/,然後運行/init腳本,掛載真正的磁盤根文件系統。

這裏藉助BusyBox構建極簡initramfs,提供基本的用戶態可執行程序。

編譯BusyBox,配置CONFIG_STATIC參數,編譯靜態版BusyBox,編譯好的可執行文件busybox不依賴動態鏈接庫,可以獨立運行,方便構建initramfs。

$ cd busybox-1.28.0
$ make menuconfig

選擇配置項:

Settings  --->
    [*] Build static binary (no shared libs)

執行編譯、安裝:

$ make -j 20
$ make install

會安裝在_install目錄:

$ ls _install 
bin  linuxrc  sbin  usr

創建initramfs,其中包含BusyBox可執行程序、必要的設備文件、啓動腳本init。這裏沒有內核模塊,如果需要調試內核模塊,可將需要的內核模塊包含進來。init腳本只掛載了虛擬文件系統procfssysfs,沒有掛載磁盤根文件系統,所有調試操作都在內存中進行,不會落磁盤。

$ mkdir initramfs
$ cd initramfs
$ cp ../_install/* -rf ./
$ mkdir dev proc sys
$ sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
$ rm linuxrc
$ vim init
$ chmod a+x init
$ ls
$ bin   dev  init  proc  sbin  sys   usr

init文件內容:

#!/bin/busybox sh         
mount -t proc none /proc  
mount -t sysfs none /sys  

exec /sbin/init

打包initramfs:

$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz

調試

$ cd busybox-1.28.0
$ qemu-system-i386 -s -kernel ./linux-4.4.203/arch/i386/boot/bzImage -initrd ./initramfs.cpio.gz -nographic -append "console=ttyS0"
  • -s-gdb tcp::1234縮寫,監聽1234端口,在GDB中可以通過target remote localhost:1234連接;
  • -kernel指定編譯好的調試版內核;
  • -initrd指定製作的initramfs;
  • -nographic取消圖形輸出窗口,使QEMU成簡單的命令行程序;
  • -append "console=ttyS0"將輸出重定向到console,將會顯示在標準輸出stdio。

啓動後的根目錄, 就是initramfs中包含的內容:

/ # ls                    
bin   dev  init  proc  root  sbin  sys   usr

由於系統自帶的GDB版本爲7.2,內核輔助腳本無法使用,重新編譯了一個新版GDB。我的系統比較新,所以gdb版本是7.11,所以不需要重新編譯。

$ cd gdb-7.9.1
$ ./configure --with-python=$(which python2.7)
$ make -j 20
$ sudo make install

啓動GDB:

$ cd linux-4.14
$ /usr/local/bin/gdb vmlinux
(gdb) target remote localhost:1234

使用內核提供的GDB輔助調試功能:

(gdb) apropos lx                                    
function lx_current -- Return current task          
function lx_module -- Find module by name and return the module variable                                 
function lx_per_cpu -- Return per-cpu variable      
function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable                    
...(此處省略若干行)                      
lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules                             
lx-version --  Report the Linux Version of the current kernel
(gdb) lx-cmdline 
console=ttyS0

在函數cmdline_proc_show設置斷點,虛擬機中運行cat /proc/cmdline命令即會觸發。

(gdb) b cmdline_proc_show                           
Breakpoint 1 at 0xffffffff81298d99: file fs/proc/cmdline.c, line 9.                                      
(gdb) c                                             
Continuing.                                         

Breakpoint 1, cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
9               seq_printf(m, "%s\n", saved_command_line);                                              
(gdb) bt
#0  cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
#1  0xffffffff81247439 in seq_read (file=0xffff880006058b00, buf=<optimized out>, size=<optimized out>, ppos=<optimized out>) at fs/seq_file.c:234
......(此處省略)
(gdb) p saved_command_line
$2 = 0xffff880007e68980 "console=ttyS0"

獲取當前進程

《深入理解Linux內核》第三版第三章–進程,講到內核採用了一種精妙的設計來獲取當前進程。

Linux把跟一個進程相關的thread_info和內核棧stack放在了同一內存區域,內核通過esp寄存器獲得當前CPU上運行進程的內核棧棧底地址,該地址正好是thread_info地址,由於進程描述符指針task字段在thread_info結構體中偏移量爲0,進而獲得task。相關彙編指令如下:

movl $0xffffe000, %ecx      /* 內核棧大小爲8K,屏蔽低13位有效位。
andl $esp, %ecx
movl (%ecx), p

指令運行後,p就獲得當前CPU上運行進程的描述符指針。

然而在調試器中調了下,發現這種機制早已經被廢棄掉了。thread_info結構體中只剩下一個字段flags,進程描述符字段task已經刪除,無法通過thread_info獲取進程描述符了。

而且進程的thread_info也不再位於進程內核棧底了,而是放在了進程描述符task_struct結構體中,見提交sched/core: Allow putting thread_info into task_structx86: Move thread_info into task_struct,這樣也無法通過esp寄存器獲取thread_info地址了。

(gdb) p $lx_current().thread_info
$5 = {flags = 2147483648}

thread_info這個變量好像沒有了,打印結果顯示沒有這個成員

這樣做是從安全角度考慮的,一方面可以防止esp寄存器泄露後進而泄露進程描述符指針,二是防止內核棧溢出覆蓋thread_info

Linux內核從2.6引入了Per-CPU變量,獲取當前指針也是通過Per-CPU變量實現的。

(gdb) p $lx_current().pid
$50 = 77
(gdb) p $lx_per_cpu("current_task").pid
$52 = 77

補充

在gdb中輸入命令apropos lx,沒有任何輸出,說明無法調用python輔助函數。

(gdb) apropos lx

從stackoverflow網站上找到一篇文章gdb-lx-symbols-undefined-command,裏邊提到:

gdb -ex add-auto-load-safe-path /path/to/linux/kernel/source/root

Now the GDB scripts are automatically loaded, and lx-symbols is available.

但是,按照上面進行操作後,進入gdb調試畫面後,提示:

To enable execution of this file add
    add-auto-load-safe-path /home/qemu2/qemu/linux-4.4.203/scripts/gdb/vmlinux-gdb.py
line to your configuration file "/home/qemu2/.gdbinit".
To completely disable this security protection add
    set auto-load safe-path /
line to your configuration file "/home/qemu2/.gdbinit".

上面的意思是,爲了能夠使能vmlinux-gdb.py的執行,需要添加

add-auto-load-safe-path /home/qemu2/qemu/linux-4.4.203/scripts/gdb/vmlinux-gdb.py

這行代碼到我的配置文件/home/qemu2/.gdbinit中。但是,查看我的系統環境沒有這個文件,於是自己新建了一個文件,並把上面的代碼加入進入。但是在執行source ./.gdbinit命令時,提示add-auto-load-safe-path這個命令找不到,於是乾脆把

set auto-load safe-path /

這行代碼添加到配置文件/home/qemu2/.gdbinit中,再執行source ./.gdbinit命令,沒有錯誤發生。

於是啓動內核代碼,然後在另一個命令行窗口中執行gdb調試,就像上面的操作一樣,顯示:

function lx_current -- Return current task
function lx_module -- Find module by name and return the module variable
function lx_per_cpu -- Return per-cpu variable
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
lx-dmesg -- Print Linux kernel log buffer
lx-list-check -- Verify a list consistency
lx-lsmod -- List currently loaded modules
lx-ps -- Dump Linux tasks
lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules

至此,終於可以安心調試內核了。

參考:

  1. Tips for Linux Kernel Development
  2. How to Build A Custom Linux Kernel For Qemu
  3. Linux Kernel System Debugging
  4. Debugging kernel and modules via gdb
  5. BusyBox simplifies embedded Linux systems
  6. Custom Initramfs
  7. Per-CPU variables
  8. Linux kernel debugging with GDB: getting a task running on a CPU
  9. gdb-kernel-debugging
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章