熟悉你的環境對高效率的開發和除錯來說是至關重要的。本文將為你簡單概述一下 JOS 環境和非常有用的 GDB 和 QEMU 命令。話雖如此,但你仍然應該去閱讀 GDB 和 QEMU 手冊,來理解這些強大的工具如何使用。
除錯小貼士
核心
GDB 是你的朋友。使用 qemu-gdb target
(或它的變體 qemu-gdb-nox
)使 QEMU 等待 GDB 去繫結。下面在除錯核心時用到的一些命令,可以去檢視 GDB 的資料。
如果你遭遇意外的中斷、異常、或三重故障,你可以使用 -d
引數要求 QEMU 去產生一個詳細的中斷日誌。
除錯虛擬記憶體問題時,嘗試 QEMU 的監視命令 info mem
(提供記憶體高階概述)或 info pg
(提供更多細節內容)。注意,這些命令僅顯示當前頁表。
(在實驗 4 以後)去除錯多個 CPU 時,使用 GDB 的執行緒相關命令,比如 thread
和 info threads
。
使用者環境(在實驗 3 以後)
GDB 也可以去除錯使用者環境,但是有些事情需要注意,因為 GDB 無法區分開多個使用者環境或區分開使用者環境與核心環境。
你可以使用 make run-name
(或編輯 kern/init.c
目錄)來指定 JOS 啟動的使用者環境,為使 QEMU 等待 GDB 去繫結,使用 run-name-gdb
的變體。
你可以符號化除錯使用者程式碼,就像除錯核心程式碼一樣,但是你要告訴 GDB,哪個符號表用到符號檔案命令上,因為它一次僅能夠使用一個符號表。提供的 .gdbinit
用於載入核心符號表 obj/kern/kernel
。對於一個使用者環境,這個符號表在它的 ELF 二進位制檔案中,因此你可以使用 symbol-file obj/user/name
去載入它。不要從任何 .o
檔案中載入符號,因為它們不會被連結器遷移進去(庫是靜態連結進 JOS 使用者二進位制檔案中的,因此這些符號已經包含在每個使用者二進位制檔案中了)。確保你得到了正確的使用者二進位制檔案;在不同的二進位制檔案中,庫函式被連結為不同的 EIP,而 GDB 並不知道更多的內容!
(在實驗 4 以後)因為 GDB 繫結了整個虛擬機器,所以它可以將時鐘中斷看作為一種控制轉移。這使得從底層上不可能實現步進使用者程式碼,因為一個時鐘中斷無形中保證了片刻之後虛擬機器可以再次執行。因此可以使用 stepi
命令,因為它阻止了中斷,但它僅可以步進一個彙編指令。斷點一般來說可以正常工作,但要注意,因為你可能在不同的環境(完全不同的一個二進位制檔案)上遇到同一個 EIP。
參考
JOS makefile
JOS 的 GNUmakefile 包含了在各種方式中執行的 JOS 的許多假目標。所有這些目標都配置 QEMU 去監聽 GDB 連線(*-gdb
目標也等待這個連線)。要在執行中的 QEMU 上啟動它,只需要在你的實驗目錄中簡單地執行 gdb
即可。我們提供了一個 .gdbinit
檔案,它可以在 QEMU 中自動指向到 GDB、載入核心符號檔案、以及在 16 位和 32 位模式之間切換。退出 GDB 將關閉 QEMU。
-
make qemu
在一個新視窗中構建所有的東西並使用 VGA 控制檯和你的終端中的序列控制檯啟動 QEMU。想退出時,既可以關閉 VGA 視窗,也可以在你的終端中按
Ctrl-c
或Ctrl-a x
。 -
make qemu-nox
和
make qemu
一樣,但僅使用序列控制檯來執行。想退出時,按下Ctrl-a x
。這種方式在透過 SSH 撥號連線到 Athena 上時非常有用,因為 VGA 視窗會佔用許多頻寬。 -
make qemu-gdb
和
make qemu
一樣,但它與任意時間被動接受 GDB 不同,而是暫停第一個機器指令並等待一個 GDB 連線。 -
make qemu-nox-gdb
它是
qemu-nox
和qemu-gdb
目標的組合。 -
make run-nam
(在實驗 3 以後)執行使用者程式 name。例如,
make run-hello
執行user/hello.c
。 -
make run-name-nox
,run-name-gdb
,run-name-gdb-nox
(在實驗 3 以後)與
qemu
目標變數對應的run-name
的變體。
makefile 也接受幾個非常有用的變數:
-
make V=1 …
詳細模式。輸出正在執行的每個命令,包括引數。
-
make V=1 grade
在評級測試失敗後停止,並將 QEMU 的輸出放入
jos.out
檔案中以備檢查。 -
make QEMUEXTRA=' _args_ ' …
指定傳遞給 QEMU 的額外引數。
JOS obj/
在構建 JOS 時,makefile 也產生一些額外的輸出檔案,這些檔案在除錯時非常有用:
-
obj/boot/boot.asm
、obj/kern/kernel.asm
、obj/user/hello.asm
、等等。引導載入器、核心、和使用者程式的彙編程式碼列表。
-
obj/kern/kernel.sym
、obj/user/hello.sym
、等等。核心和使用者程式的符號表。
-
obj/boot/boot.out
、obj/kern/kernel
、obj/user/hello
、等等。核心和使用者程式連結的 ELF 映象。它們包含了 GDB 用到的符號資訊。
GDB
完整的 GDB 命令指南請檢視 GDB 手冊。下面是一些在 6.828 課程中非常有用的命令,它們中的一些在作業系統開發之外的領域幾乎用不到。
-
Ctrl-c
在當前指令處停止機器並打斷進入到 GDB。如果 QEMU 有多個虛擬的 CPU,所有的 CPU 都會停止。
-
c
(或continue
)繼續執行,直到下一個斷點或
Ctrl-c
。 -
si
(或stepi
)執行一個機器指令。
-
b function
或b file:line
(或breakpoint
)在給定的函式或行上設定一個斷點。
-
b * addr
(或breakpoint
)在 EIP 的 addr 處設定一個斷點。
-
set print pretty
啟用陣列和結構的美化輸出。
-
info registers
輸出通用暫存器
eip
、eflags
、和段選擇器。更多更全的機器暫存器狀態轉儲,檢視 QEMU 自己的info registers
命令。 -
x/ N x addr
以十六進位制顯示虛擬地址 addr 處開始的 N 個詞的轉儲。如果 N 省略,預設為 1。addr 可以是任何表示式。
-
x/ N i addr
顯示從 addr 處開始的 N 個彙編指令。使用
$eip
作為 addr 將顯示當前指令指標暫存器中的指令。 -
symbol-file file
(在實驗 3 以後)切換到符號檔案 file 上。當 GDB 繫結到 QEMU 後,它並不是虛擬機器中程序邊界內的一部分,因此我們要去告訴它去使用哪個符號。預設情況下,我們配置 GDB 去使用核心符號檔案
obj/kern/kernel
。如果機器正在執行使用者程式碼,比如是hello.c
,你就需要使用symbol-file obj/user/hello
去切換到 hello 的符號檔案。
QEMU 將每個虛擬 CPU 表示為 GDB 中的一個執行緒,因此你可以使用 GDB 中所有的執行緒相關的命令去檢視或維護 QEMU 的虛擬 CPU。
-
thread n
GDB 在一個時刻只關注於一個執行緒(即:CPU)。這個命令將關注的執行緒切換到 n,n 是從 0 開始編號的。
-
info threads
列出所有的執行緒(即:CPU),包括它們的狀態(活動還是停止)和它們在什麼函式中。
QEMU
QEMU 包含一個內建的監視器,它能夠有效地檢查和修改機器狀態。想進入到監視器中,在執行 QEMU 的終端中按入 Ctrl-a c
即可。再次按下 Ctrl-a c
將切換回序列控制檯。
監視器命令的完整參考資料,請檢視 QEMU 手冊。下面是 6.828 課程中用到的一些有用的命令:
-
xp/ N x paddr
顯示從實體地址 paddr 處開始的 N 個詞的十六進位制轉儲。如果 N 省略,預設為 1。這是 GDB 的
x
命令模擬的實體記憶體。 -
info registers
顯示機器內部暫存器狀態的一個完整轉儲。實踐中,對於段選擇器,這將包含機器的 隱藏 段狀態和區域性、全域性、和中斷描述符表加任務狀態暫存器。隱藏狀態是在載入段選擇器後,虛擬的 CPU 從 GDT/LDT 中讀取的資訊。下面是實驗 1 中 JOS 核心處於執行中時的 CS 資訊和每個欄位的含義:
CS =0008 10000000 ffffffff 10cf9a00 DPL=0 CS32 [-R-]
-
CS =0008
程式碼選擇器可見部分。我們使用段 0x8。這也告訴我們參考全域性描述符表(0x8&4=0),並且我們的 CPL(當前許可權級別)是 0x8&3=0。
-
10000000
這是段基址。線性地址 = 邏輯地址 + 0x10000000。
-
ffffffff
這是段限制。訪問線性地址 0xffffffff 以上將返回段違規異常。
-
10cf9a00
段的原始標誌,QEMU 將在接下來的幾個欄位中解碼這些對我們有用的標誌。
-
DPL=0
段的許可權級別。一旦程式碼以許可權 0 執行,它將就能夠載入這個段。
-
CS32
這是一個 32 位程式碼段。對於資料段(不要與 DS 暫存器混淆了),另外的值還包括
DS
,而對於本地描述符表是LDT
。 -
[-R-]
這個段是隻讀的。
-
info mem
(在實驗 2 以後)顯示對映的虛擬記憶體和許可權。比如:
ef7c0000-ef800000 00040000 urw efbf8000-efc00000 00008000 -rw
這告訴我們從 0xef7c0000 到 0xef800000 的 0x00040000 位元組的記憶體被對映為讀取/寫入/使用者可訪問,而對映在 0xefbf8000 到 0xefc00000 之間的記憶體許可權是讀取/寫入,但是僅限於核心可訪問。
-
info pg
(在實驗 2 以後)顯示當前頁表結構。它的輸出類似於
info mem
,但與頁目錄條目和頁表條目是有區別的,並且為每個條目給了單獨的許可權。重複的 PTE 和整個頁表被摺疊為一個單行。例如:VPN range Entry Flags Physical page [00000-003ff] PDE[000] -------UWP [00200-00233] PTE[200-233] -------U-P 00380 0037e 0037d 0037c 0037b 0037a .. [00800-00bff] PDE[002] ----A--UWP [00800-00801] PTE[000-001] ----A--U-P 0034b 00349 [00802-00802] PTE[002] -------U-P 00348
這裡各自顯示了兩個頁目錄條目、虛擬地址範圍 0x00000000 到 0x003fffff 以及 0x00800000 到 0x00bfffff。 所有的 PDE 都存在於記憶體中、可寫入、並且使用者可訪問,而第二個 PDE 也是可訪問的。這些頁表中的第二個對映了三個頁、虛擬地址範圍 0x00800000 到 0x00802fff,其中前兩個頁是存在於記憶體中的、可寫入、並且使用者可訪問的,而第三個僅存在於記憶體中,並且使用者可訪問。這些 PTE 的第一個條目對映在物理頁 0x34b 處。
QEMU 也有一些非常有用的命令列引數,使用 QEMUEXTRA
變數可以將引數傳遞給 JOS 的 makefile。
-
make QEMUEXTRA='-d int' ...
記錄所有的中斷和一個完整的暫存器轉儲到
qemu.log
檔案中。你可以忽略前兩個日誌條目、“SMM: enter” 和 “SMM: after RMS”,因為這些是在進入引導載入器之前生成的。在這之後的日誌條目看起來像下面這樣:4: v=30 e=0000 i=1 cpl=3 IP=001b:00800e2e pc=00800e2e SP=0023:eebfdf28 EAX=00000005 EAX=00000005 EBX=00001002 ECX=00200000 EDX=00000000 ESI=00000805 EDI=00200000 EBP=eebfdf60 ESP=eebfdf28 ...
第一行描述了中斷。
4:
只是一個日誌記錄計數器。v
提供了十六程序的向量號。e
提供了錯誤程式碼。i=1
表示它是由一個int
指令(相對一個硬體產生的中斷而言)產生的。剩下的行的意思很明顯。對於一個暫存器轉儲而言,接下來看到的就是暫存器資訊。注意:如果你執行的是一個 0.15 版本之前的 QEMU,日誌將寫入到
/tmp
目錄,而不是當前目錄下。
via: https://pdos.csail.mit.edu/6.828/2018/labguide.html
作者:csail.mit 選題:lujun9972 譯者:qhwdw 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出