掌握 GDB 調試工具,輕鬆排除 bug

一、什麼是 GDB

gdb 是 GNU debugger 的縮寫,是編程調試工具。

目前 release 的最新版本爲 8.0,GDB 可以運行在 Linux 和 Windows 操作系統上。

1.1 安裝與啓動 GDB

  1. gdb -v 檢查是否安裝成功,未安裝成功則安裝 (必須確保編譯器已經安裝,如 gcc) 。

  2. 啓動 gdb

  3. gdb test_file.exe 來啓動 gdb 調試, 即直接指定需要調試的可執行文件名

  4. 直接輸入 gdb 啓動,進入 gdb 之後採用命令 file test_file.exe 來指定文件名

  5. 如果目標執行文件要求出入參數 (如 argv[] 接收參數),則可以通過三種方式指定參數:

  6. 在啓動 gdb 時,gdb --args text_file.exe

  7. 在進入 gdb 之後,運行 set args param_1

  8. 在 進入 gdb 調試以後,run param_1 或者 start para_1

1.2gdb 的功能

1.3gdb 的使用

運行程序

run(r)運行程序,如果要加參數,則是run arg1 arg2 ...

查看源代碼

list(l):查看最近十行源碼
list fun:查看fun函數源代碼
list file:fun:查看flie文件中的fun函數源代碼

設置斷點與觀察斷點

break 行號/fun設置斷點。
break file:行號/fun設置斷點。
break if<condition>:條件成立時程序停住。
info break(縮寫:i b):查看斷點。
watch expr:一旦expr值發生改變,程序停住。
delete n:刪除斷點。

單步調試

continue(c):運行至下一個斷點。
step(s):單步跟蹤,進入函數,類似於VC中的step in。
next(n):單步跟蹤,不進入函數,類似於VC中的step out。
finish:運行程序,知道當前函數完成返回,並打印函數返回時的堆棧地址和返回值及參數值等信息。
until:當厭倦了在一個循環體內單步跟蹤時,這個命令可以運行程序知道退出循環體。

查看運行時數據

print(p):查看運行時的變量以及表達式。
ptype:查看類型。
print array:打印數組所有元素。
print *array@len:查看動態內存。len是查看數組array的元素個數。
print x=5:改變運行時數據。

1.4 程序錯誤

1.5gdb 調試段錯誤

什麼是段錯誤?段錯誤是由於訪問非法地址而產生的錯誤。

gdb 調試段錯誤,可以直接運行程序,當程序運行崩潰後,gdb 會打印運行的信息,比如:收到了 SIGSEGV 信號,然後可以使用bt命令,打印棧回溯信息,然後根據程序發生錯誤的代碼,修改程序。

1.6.core 文件調試

6.1 core 文件

在程序崩潰時,一般會生成一個文件叫core文件。core 文件記錄的是程序崩潰時的內存映像,並加入調試信息,core 文件生成過程叫做core dump(核心已轉儲)。系統默認不會生成該文件。

6.2 設置生成 core 文件

6.3 gdb 調試 core 文件

當設置完ulimit -c xxxx後,再次運行程序發生段錯誤,此時就會生成一個core文件,使用gdb core調試 core 文件,使用bt命令打印棧回溯信息。

二、GDB 常用命令

2.1 查看源碼

list [函數名][行數]

2.2 打斷點調試

(1)設置斷點:

刪除斷點

(gdb) clear location:參數 location 通常爲某一行代碼的行號或者某個具體的函數名。當 location 參數爲某個函數的函數名時,表示刪除位於該函數入口處的所有斷點。

(gdb) delete [breakpoints] [num]:breakpoints 參數可有可無,num 參數爲指定斷點的編號,其可以是 delete 刪除某一個斷點,而非全部。

禁用斷點

disable [breakpoints] [num...]:breakpoints 參數可有可無;num... 表示可以有多個參數,每個參數都爲要禁用斷點的編號。如果指定 num...,disable 命令會禁用指定編號的斷點;反之若不設定 num...,則 disable 會禁用當前程序中所有的斷點。

激活斷點

  1. enable [breakpoints] [num...] 激活用 num... 參數指定的多個斷點,如果不設定 num...,表示激活所有禁用的斷點

  2. enable [breakpoints] once num… 臨時激活以 num... 爲編號的多個斷點,但斷點只能使用 1 次,之後會自動回到禁用狀態

  3. enable [breakpoints] count num... 臨時激活以 num... 爲編號的多個斷點,斷點可以使用 count 次,之後進入禁用狀態

  4. enable [breakpoints] delete num… 激活 num.. 爲編號的多個斷點,但斷點只能使用 1 次,之後會被永久刪除。

break(b): 打的是普通斷點,打斷點有兩種形式

(gdb) break location // b location,location 代表打斷點的位置

(gdb) break ... if cond // b .. if cond,代表如果 cond 條件爲 true,則在 “...” 處打斷點

通過藉助 condition 命令爲不同類型斷點設置條件表達式,只有當條件表達式成立(值爲 True)時,相應的斷點纔會觸發從而使程序暫停運行。

tbreak: tbreak 命令可以看到是 break 命令的另一個版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在於,使用 tbreak 命令打的斷點僅會作用 1 次,即使程序暫停之後,該斷點就會自動消失。

rbreak: 和 break 和 tbreak 命令不同,rbreak 命令的作用對象是 C、C++ 程序中的函數,它會在指定函數的開頭位置打斷點。

watch: 此命令打的是觀察斷點,可以監控某個變量或者表達式的值。只有當被監控變量(表達式)的值發生改變,程序纔會停止運行。

rwatch 命令:只要程序中出現讀取目標變量(表達式)的值的操作,程序就會停止運行;

awatch 命令:只要程序中出現讀取目標變量(表達式)的值或者改變值的操作,程序就會停止運行。

catch: 捕捉斷點的作用是,監控程序中某一事件的發生,例如程序發生某種異常時、某一動態庫被加載時等等,一旦目標時間發生,則程序停止執行。

(2)觀察斷點:

(3)設置捕捉點:

catch + event 當 event 發生時,停住程序。

event 可以是下面的內容:

(4)捕獲信號:

handle + [argu] + signals

signals:是 Linux/Unix 定義的信號,SIGINT 表示中斷字符信號,也就是 Ctrl+C 的信號,SIGBUS 表示硬件故障的信號;SIGCHLD 表示子進程狀態改變信號; SIGKILL 表示終止程序運行的信號,等等。

argu:

(5)線程中斷:

break [linespec] thread [threadno] [if ...]

linespec 斷點設置所在的源代碼的行號。如: test.c:12 表示文件爲 test.c 中的第 12 行設置一個斷點。

threadno 線程的 ID。是 GDB 分配的,通過輸入 info threads 來查看正在運行中程序的線程信息。

if ... 設置中斷條件。

查看信息:

(1)查看數據:

print variable 查看變量

print *array@len 查看數組(array 是數組指針,len 是需要數據長度)

可以通過添加參數來設置輸出格式:

/ 按十六進制格式顯示變量。
/d 按十進制格式顯示變量。
/u 按十六進制格式顯示無符號整型。
/o 按八進制格式顯示變量。
/t 按二進制格式顯示變量。 
/a 按十六進制格式顯示變量。
/c 按字符格式顯示變量。
/f 按浮點數格式顯示變量。

(2)查看內存

examine /n f u + 內存地址(指針變量)

  如:x /10cw pFilePath  (pFilePath爲一個字符串指針,指針佔4字節)
     x 爲examine命令的簡寫。

(3)查看棧信息

backtrace [-n][n]

2.3 單步調試

run(r)

continue(c)

next(n)

step(s)

until(u)

(gdb) until location:參數 location 爲某一行代碼的行號

查看變量的值

print(p)

isplay

GDB handle 命令: 信號處理

→(gdb) handle signal mode 其中,signal 參數表示要設定的目標信號,它通常爲某個信號的全名(SIGINT)或者簡稱(去除‘SIG’後的部分,如 INT);如果要指定所有信號,可以用 all 表示。
mode 參數用於明確 GDB 處理該目標信息的方式,其值可以是如下幾個:

可以在 gdb 模式下,通過 info signals 或者 info signals <signal_name> (例如 info signals SIGINT) 查看不同 signal 的信息。

GDB frame 和 backtrace 命令:查看棧信息

(gdb) frame spec 該命令可以將 spec 參數指定的棧幀選定爲當前棧幀。spec 參數的值,常用的指定方法有 3 種:

  1. 通過棧幀的編號指定。0 爲當前被調用函數對應的棧幀號,最大編號的棧幀對應的函數通常就是 main() 主函數;

  2. 藉助棧幀的地址指定。棧幀地址可以通過 info frame 命令(後續會講)打印出的信息中看到;

  3. 通過函數的函數名指定。注意,如果是類似遞歸函數,其對應多個棧幀的話,通過此方法指定的是編號最小的那個棧幀。

(gdb) info frame 我們可以查看當前棧幀中存儲的信息

該命令會依次打印出當前棧幀的如下信息:

除此之外,還可以使用 info args 命令查看當前函數各個參數的值;使用 info locals 命令查看當前函數中各局部變量的值。

(gdb) backtrace [-full] [n] 用於打印當前調試環境中所有棧幀的信息

其中,用 [ ] 括起來的參數爲可選項,它們的含義分別爲:

GDB 編輯和搜索源碼

GDB edit 命令:編輯文件

GDB search 命令:搜索文件

三、GDB 調試程序用法

一般來說,GDB 主要幫忙你完成下面四個方面的功能:

1、啓動你的程序,可以按照你的自定義的要求隨心所欲的運行程序。
2、可讓被調試的程序在你所指定的調置的斷點處停住。(斷點可以是條件表達式)
3、當程序被停住時,可以檢查此時你的程序中所發生的事。
4、動態的改變你程序的執行環境。

從上面看來,GDB 和一般的調試工具沒有什麼兩樣,基本上也是完成這些功能,不過在細節上,你會發現 GDB 這個調試工具的強大,大家可能比較習慣了圖形化的調試工具,但有時候,命令行的調試工具卻有着圖形化工具所不能完成的功能。讓我們一一看來。

一個調試示例:

源程序:tst.c

1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i<n; i++)
7 {
8 sum+=i;
9 }
10 return sum;
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
21 }
22
23 printf("result[1-100] = %d /n", result );
24 printf("result[1-250] = %d /n", func(250) );
25 }

編譯生成執行文件:(Linux 下)

hchen/test> cc -g tst.c -o tst

使用 GDB 調試:

hchen/test> gdb tst <---------- 啓動GDB
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-SUSE-linux"...
(gdb) l <-------------------- l命令相當於list,從第一行開始例出原碼。
1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i<n; i++)
7 {
8 sum+=i;
9 }
10 return sum;
(gdb) <-------------------- 直接回車表示,重複上一次命令
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
(gdb) break 16 <-------------------- 設置斷點,在源程序第16行處。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func <-------------------- 設置斷點,在函數func()入口處。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break <-------------------- 查看斷點信息。
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048496 in main at tst.c:16
2 breakpoint keep y 0x08048456 in func at tst.c:5
(gdb) r <--------------------- 運行程序,run命令簡寫
Starting program: /home/hchen/test/tst

Breakpoint 1, main () at tst.c:17 <---------- 在斷點處停住。
17 long result = 0;
(gdb) n <--------------------- 單條語句執行,next命令簡寫。
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) n
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) c <--------------------- 繼續運行程序,continue命令簡寫。
Continuing.
result[1-100] = 5050 <----------程序輸出。

Breakpoint 2, func (n=250) at tst.c:5
5 int sum=0,i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p i <--------------------- 打印變量i的值,print命令簡寫。
$1 = 134513808
(gdb) n
8 sum+=i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8 sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb) bt <--------------------- 查看函數堆棧。
#0 func (n=250) at tst.c:5
#1 0x080484e4 in main () at tst.c:24
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6
(gdb) finish <--------------------- 退出函數。
Run till exit from #0 func (n=250) at tst.c:5
0x080484e4 in main () at tst.c:24
24 printf("result[1-250] = %d /n", func(250) );
Value returned is $6 = 31375
(gdb) c <--------------------- 繼續運行。
Continuing.
result[1-250] = 31375 <----------程序輸出。

Program exited with code 027. <--------程序退出,調試結束。
(gdb) q <--------------------- 退出gdb。
hchen/test>

好了,有了以上的感性認識,還是讓我們來系統地認識一下 gdb 吧。

基本 gdb 命令:

GDB常用命令	格式	含義	簡寫
list	List [開始,結束]	列出文件的代碼清單	l
prit	Print 變量名	打印變量內容	p
break	Break [行號或函數名]	設置斷點	b
continue	Continue [開始,結束]	繼續運行	c
info	Info 變量名	列出信息	i
next	Next	下一行	n
step	Step	進入函數(步入)	S
display	Display 變量名	顯示參數	 
file	File 文件名(可以是絕對路徑和相對路徑)	加載文件	 
run	Run args	運行程序	r

四、GDB 實戰

下面是一個使用了上述命令的實戰例子:

[root@www.linuxidc.com bufbomb]# gdb bufbomb 
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 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-RedHat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/Temp/bufbomb/bufbomb...done.
(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) run -t cdai
Starting program: /root/Temp/bufbomb/bufbomb -t cdai
Team: cdai
Cookie: 0x5e5ee04e

Breakpoint 1, 0x08048ad6 in getbuf ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.149.el6_6.4.i686

(gdb) bt
#0  0x08048ad6 in getbuf ()
#1  0x08048db2 in test ()
#2  0x08049085 in launch ()
#3  0x08049257 in main ()
(gdb) info frame 0
Stack frame at 0xffffb540:
 eip = 0x8048ad6 in getbuf; saved eip 0x8048db2
 called by frame at 0xffffb560
 Arglist at 0xffffb538, args: 
 Locals at 0xffffb538, Previous frame's sp is 0xffffb540
 Saved registers:
  ebp at 0xffffb538, eip at 0xffffb53c
(gdb) info registers
eax            0xc      12
ecx            0xffffb548       -19128
edx            0xc8c340 13157184
ebx            0x0      0
esp            0xffffb510       0xffffb510
ebp            0xffffb538       0xffffb538
esi            0x804b018        134524952
edi            0xffffffff       -1
eip            0x8048ad6        0x8048ad6 <getbuf+6>
eflags         0x282    [ SF IF ]
cs             0x23     35
ss             0x2b     43
ds             0x2b     43
es             0x2b     43
fs             0x0      0
gs             0x63     99
(gdb) x/10x $sp
0xffffb510:     0xf7ffc6b0      0x00000001      0x00000001      0xffffb564
0xffffb520:     0x08048448      0x0804a12c      0xffffb548      0x00c8aff4
0xffffb530:     0x0804b018      0xffffffff

(gdb) si
0x08048ad9 in getbuf ()
(gdb) si
0x08048adc in getbuf ()
(gdb) si
0x080489c0 in Gets ()
(gdb) n
Single stepping until exit from function Gets,
which has no line number information.
Type string:123
0x08048ae1 in getbuf ()
(gdb) si
0x08048ae2 in getbuf ()
(gdb) c
Continuing.
Dud: getbuf returned 0x1
Better luck next time

Program exited normally.
(gdb) quit

4.1 逆向調試

GDB 7.0 後加入了 Reversal Debugging 功能。具體來說,比如我在 getbuf() 和 main() 上設置了斷點,當啓動程序時會停在 main() 函數的斷點上。此時敲入 record 後 continue 到下一斷點 getbuf(),GDB 就會記錄從 main() 到 getbuf() 的運行時信息。現在用 rn 就可以逆向地從 getbuf() 調試到 main()。就像《X 戰警:逆轉未來》裏一樣,挺神奇吧!

這種方式適合從 bug 處反向去找引起 bug 的代碼,實用性因情況而異。當然,它也是有侷限性的。像程序假如有 I/O 輸出等外部條件改變時,GDB 是沒法 “逆轉” 的。

[root@www.linuxidc.com bufbomb]# gdb bufbomb 
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 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-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/Temp/bufbomb/bufbomb...done.

(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) b main
Breakpoint 2 at 0x80490c6

(gdb) run -t cdai
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /root/Temp/bufbomb/bufbomb -t cdai

Breakpoint 2, 0x080490c6 in main ()
(gdb) record
(gdb) c
Continuing.
Team: cdai
Cookie: 0x5e5ee04e

Breakpoint 1, 0x08048ad6 in getbuf ()

(gdb) rn
Single stepping until exit from function getbuf,
which has no line number information.
0x08048dad in test ()
(gdb) rn
Single stepping until exit from function test,
which has no line number information.
0x08049080 in launch ()
(gdb) rn
Single stepping until exit from function launch,
which has no line number information.
0x08049252 in main ()

4.2VSCode+GDB+Qemu 調試 ARM64 linux 內核

linux kernel 是一個非常複雜的系統,初學者會很難入門。如果有一個方便的調試環境,學習效率至少能有 5-10 倍的提升。

爲了學習 linux 內核,通常有這兩個需要:

  1. 可以擺脫硬件,方便的編譯和運行 linux

  2. 可以使用圖形化的工具來調試 linux

筆者使用 VSCode+GDB+Qemu 完成了這兩個需求:

最終效果大致如下:

qemu 運行界面:

vscode 調試界面:

下面將一步一步介紹如何搭建上述環境。本文所有操作都在 Vmware Ubuntu16 虛擬機上進行。

安裝編譯工具鏈

由於 Ubuntu 是 X86 架構,爲了編譯 arm64 的文件,需要安裝交叉編譯工具鏈

sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install libncurses5-dev  build-essential git bison flex libssl-dev

製作根文件系統

linux 的啓動需要配合根文件系統,這裏我們利用 busybox 來製作一個簡單的根文件系統

編譯 busybox

wget  https://busybox.net/downloads/busybox-1.33.1.tar.bz2
tar -xjf busybox-1.33.1.tar.bz2
cd busybox-1.33.1

打開靜態庫編譯選項

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

指定編譯工具

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

編譯

make
make install

編譯完成,在 busybox 目錄下生成_install 目錄

定製文件系統

爲了 init 進程能正常啓動, 需要再額外進行一些配置

根目錄添加 etc、dev 和 lib 目錄

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ mkdir etc dev lib
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ ls
bin  dev  etc  lib  linuxrc  sbin  usr

在 etc 分別創建文件:

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:13]
$ cat profile
#!/bin/sh
export HOSTNAME=bryant
export USER=root
export HOME=/home
export PS1="[$USER@$HOSTNAME \W]\# "
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:16]
$ cat inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:19]
$ cat fstab
#device  mount-point    type     options   dump   fsck order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
kmod_mount /mnt 9p trans=virtio 0 0

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:26]
$ ls init.d
rcS

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:30]
$ cat init.d/rcS
mkdir -p /sys
mkdir -p /tmp
mkdir -p /proc
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

這裏對這幾個文件做一點說明:

  1. busybox 作爲 linuxrc 啓動後, 會讀取 / etc/profile, 這裏面設置了一些環境變量和 shell 的屬性

  2. 根據 / etc/fstab 提供的掛載信息, 進行文件系統的掛載

  3. busybox 會從 /etc/inittab 中讀取 sysinit 並執行, 這裏 sysinit 指向了 / etc/init.d/rcS

  4. /etc/init.d/rcS 中 ,mdev -s 這條命令很重要, 它會掃描 / sys 目錄,查找字符設備和塊設備,並在 / dev 下 mknod

dev 目錄:

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/dev [1:17:36]
$ sudo mknod console c 5 1

這一步很重要, 沒有 console 這個文件, 用戶態的輸出沒法打印到串口上

lib 目錄:拷貝 lib 庫,支持動態編譯的應用程序運行:

# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/lib [1:18:43]
$ cp /usr/aarch64-linux-gnu/lib/*.so*  -a .

編譯內核

配置內核

linux 內核源碼可以在 github 上直接下載。

根據 arch/arm64/configs/defconfig 文件生成. config

make defconfig ARCH=arm64

將下面的配置加入. config 文件中

CONFIG_DEBUG_INFO=y 
CONFIG_INITRAMFS_SOURCE="./root"
CONFIG_INITRAMFS_ROOT_UID=0
CONFIG_INITRAMFS_ROOT_GID=0

CONFIG_DEBUG_INFO 是爲了方便調試

CONFIG_INITRAMFS_SOURCE 是指定 kernel ramdisk 的位置,這樣指定之後 ramdisk 會直接被編譯到 kernel 鏡像中。

我們將之前製作好的根文件系統 cp 到 root 目錄下:

# bryant @ ubuntu in ~/Downloads/linux-arm64 on git:main x [1:26:56]
$ cp -r ../busybox-1.33.1/_install root

執行編譯

make ARCH=arm64 Image -j8  CROSS_COMPILE=aarch64-linux-gnu-

這裏指定 target 爲 Image 會只編譯 kernel, 不會編譯 modules, 這樣會增加編譯速度

啓動 qemu

下載 qemu

需要注意的,qemu 最好源碼編譯, 用 apt-get 直接安裝的 qemu 可能版本過低,導致無法啓動 arm64 內核。筆者是使用 4.2.1 版本的 qemu

apt-get install build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libboost-all-dev autoconf libtool libssl-dev libpixman-1-dev libpython-dev python-pip python-capstone virtualenv
wget https://download.qemu.org/qemu-4.2.1.tar.xz
tar xvJf qemu-4.2.1.tar.xz
cd qemu-4.2.1
./configure --target-list=x86_64-softmmu,x86_64-linux-user,arm-softmmu,arm-linux-user,aarch64-softmmu,aarch64-linux-user --enable-kvm
make 
sudo make install

編譯完成之後,qemu 在 /usr/local/bin 目錄下

$ /usr/local/bin/qemu-system-aarch64 --version
QEMU emulator version 4.2.1
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers

啓動 linux 內核

/usr/local/bin/qemu-system-aarch64 -m 512M -smp 4 -cpu cortex-a57 -machine virt -kernel

這裏對於參數做一些解釋:

這裏說明一下 console=ttyAMA0 是怎麼生效的。

查看 linux 源碼可知 ttyAMA0 對應的是AMBA_PL011這個驅動:

config SERIAL_AMBA_PL011_CONSOLE
    bool "Support for console on AMBA serial port"
    depends on SERIAL_AMBA_PL011=y
    select SERIAL_CORE_CONSOLE
    select SERIAL_EARLYCON
    help
      Say Y here if you wish to use an AMBA PrimeCell UART as the system
      console (the system console is the device which receives all kernel
      messages and warnings and which allows logins in single user mode).

      Even if you say Y here, the currently visible framebuffer console
      (/dev/tty0) will still be used as the system console by default, but
      you can alter that using a kernel command line option such as
      "console=ttyAMA0". (Try "man bootparam" or see the documentation of
      your boot loader (lilo or loadlin) about how to pass options to the
      kernel at boot time.)

AMBA_PL011 是 arm 的一個標準串口設備, qemu 的輸出就是模擬的這個串口。

在 qemu 的源碼文件中,也可以看到 PL011 的相關文件:

# bryant @ ubuntu in ~/Downloads/qemu-4.2.1 [1:46:54]
$ find . -name "*pl011*"
./hw/char/pl011.c

成功啓動 Linux 後, 串口打印如下:

[    3.401567] usbcore: registered new interface driver usbhid
[    3.404445] usbhid: USB HID core driver
[    3.425030] NET: Registered protocol family 17
[    3.429743] 9pnet: Installing 9P2000 support
[    3.435439] Key type dns_resolver registered
[    3.440299] registered taskstats version 1
[    3.443685] Loading compiled-in X.509 certificates
[    3.461041] input: gpio-keys as /devices/platform/gpio-keys/input/input0
[    3.473163] ALSA device list:
[    3.474432]   No soundcards found.
[    3.485283] uart-pl011 9000000.pl011: no DMA platform data
[    3.541376] Freeing unused kernel memory: 10752K
[    3.545897] Run /linuxrc as init process
[    3.548390]   with arguments:
[    3.550279]     /linuxrc
[    3.551073]     nokaslr
[    3.552216]   with environment:
[    3.554396]     HOME=/
[    3.555898]     TERM=linux
[    3.985835] 9pnet_virtio: no channels available for device kmod_mount
mount: mounting kmod_mount on /mnt failed: No such file or directory
/etc/init.d/rcS: line 8: can't create /proc/sys/kernel/hotplug: nonexistent directory

Please press Enter to activate this console.
[root@bryant ]#
[root@bryant ]#

VSCode+GDB

vscode 中集成了 GDB 功能,我們可以用它來圖形化的調試 linux kernel

首先我們添加 vscode 的 gdb 配置文件 (.vscode/launch.json):

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "kernel debug",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/vmlinux",
            "cwd": "${workspaceFolder}",
            "MIMode": "gdb",
            "miDebuggerPath":"/usr/bin/gdb-multiarch",
            "miDebuggerServerAddress": "localhost:1234"
        }
    ]
}

這裏對幾個重點參數做一些說明:

配置完成之後,可以直接啓動 GDB, 連接上 linux kernel

在 vscode 中,可以設置斷點,進行單步調試

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-ToGLcznpKs3SZBenB0lRg