學習作業系統原理最好的方法是自己寫一個簡單的作業系統。
寫程式不免需要偵錯,寫不同的程式偵錯方式也不同。如果做應用軟體開發,相應的程式偵錯方式是建立在有作業系統支援的基礎上的。而我們現在是要開發作業系統,如何偵錯作業系統的程式呢?如果作業系統程式直接跑在真機上或虛擬機器器上(比如VirtualBox)是很難偵錯的,所以我們在開發階段作業系統程式主要在虛擬機器器QEMU上跑,因為QEMU支援偵錯。當然很多事情都是有利也有弊的,QEMU雖然支援偵錯,但它的執行效率比VitrualBox要低,所以我們最終的GrapeOS程式是跑在VirtalBox上的。QEMU需要結合GDB才能實現偵錯,下面我們一起來學習一下。
在Windows的cmd命令列中輸入如下一行命令:
qemu-system-i386 d:\GrapeOS\VMShare\GrapeOS.img -S -s
上面這行命令比之前多了兩個引數,「-S」表示讓CPU在將要執行第一條指令前暫停,「-s」表示讓QEMU開啟自帶的GDB伺服器端功能,且網路埠號是1234。截圖如下:
執行上面的命令後,會彈出QEMU的視窗:
從上圖中可以看到QEMU視窗中間顯示一行文字「Guest has not initialized the display(yet).」,此時QEMU已進入偵錯模式。當QEMU進入偵錯模式後,就在等待GDB使用者端來連線它。當GDB使用者端連線上QEMU的GDB伺服器端就可以偵錯了。就像我們用PowerShell連線到CentOS就可以在PowerShell中操縱CentOS一樣,此時PowerShell是使用者端,CentOS是伺服器端。下面我們來介紹GDB使用者端。
GDB分為伺服器端和使用者端,單說GDB,一般是指GDB使用者端。GDB是Linux中的一個偵錯軟體,所以我們準備在CentOS中使用它。首先我們通過PowerShell登入CentOS。
首次使用GDB可能需要安裝一下:
yum install gdb
敲命令gdb
就執行了,如下圖:
在GDB中輸入如下命令連線QEMU:
target remote 192.168.10.102:1234
上面這行命令中的IP地址「192.168.10.102」是我的Windows的IP地址,你需要替換成你的Windows的IP地址。截圖如下:
如上截圖所示,我們已經通過GDB連線到QEMU了。圖中倒數第二行的十六進位制數「0x0000fff0」表示CPU將要執行的指令地址。還記得前面介紹的真真實模式下1M記憶體的佈局嗎?這個地址在BIOS中,是CPU執行的第一條指令所在的地址。
設定斷點是偵錯必備的一個功能,比如我們在0x7c00處設定個斷點:
b *0x7c00
這樣就設定好了一個斷點。可以用同樣的方式設定多個斷點。
這個命令簡單,只有一個字母「c」,然後回車即可讓CPU繼續執行,當遇到斷點時會自動暫停。截圖如下:
檢視所有暫存器的命令是i r
,截圖如下:
從上圖中可以看到此時CPU中很多暫存器的值,有朋友可能會有個疑問,以前學的暫存器是「ax、bx、cx……」這些,上面截圖中怎麼是「eax、ebx、ecx……」呢?原因是當年8086CPU的暫存器都是16位元的,也就是「ax、bx、cx……」這些,很多講x86組合語言的資料都只講了8086下的情況。而我們現在啟動的是32位元x86模擬器「qemu-system-i386」,所有通用暫存器多了一個字母「e」表示擴充套件,從16位元擴充套件成了32位元。這些32位元通用暫存器中的低16位元就是原來的16位元暫存器,比如eax的低16位元還是ax,ah和al仍然表示ax的高8位元和低8位元,其它暫存器也一樣。這就是相容,能讓舊程式在新CPU上執行。之前的16位元暫存器中只有段暫存器沒有擴充套件,還是16位元的,而且還增加了2個,分別是fs和gs。增加的這2個段暫存器作用和es基本一樣,之所以增加是怕在複雜的程式中出現段暫存器不夠用的情況。當資料比較多的時候GDB一般只輸出一部分,此時如果按確認鍵還會顯示出其它一些暫存器,但我們用不上,按「q」鍵退出繼續輸出即可。
下面來看一下如何檢視單個暫存器,比如我們要檢視暫存器ax的值,輸入命令p $ax
,如果想以十六進位制顯示可以輸入命令p /x $ax
,截圖如下:
命令格式:x /nfu addr
n表示數量
f表示格式:x(hex), d(decimal), c(char)等。
u表示顯示單位:b(byte), h(halfword), w(word), g(giant, 8 bytes)。
下面我們分別演示檢視0x7c00開始的8個單位元組、8個雙位元組、8個四位元組、8個八位元組的記憶體值。截圖如下:
雖然目前看到的資料都是0,但我們以後寫上程式就不一樣了。
有時候需要將機器碼反組合成組合程式碼方便檢視,下面我們以反組合0x7c00開始的10個位元組為例:
disas 0x7c00,+10
截圖如下:
上面截圖中顯示的組合程式碼是GDB預設的AT&T語法,我們可以設定改成Intel語法:
set disassembly-flavor intel
截圖如下:
需要說明一下的是這裡的反組合結果是錯的。因為它是按照32位元模式反組合的,而我們現在還處在16位元真真實模式中,所以這個反組合功能只能等後面我們進入32位元模式才有用。至於反組合16位元程式碼我們會在後續教學中介紹其它方法。
在偵錯的時候有時需要一條指令一條指令的單步執行,單步執行的命令是si
。
從上面的截圖可以看到,每輸入一個si回車,就會執行一條命令。每個si
命令下面一行中的十六進位制數表示下一條指令的地址,可以看到地址在不斷增加,說明的確在執行指令。如果想知道每一步都執行了什麼指令,可以用下面這個命令來反組合下一條要執行的指令:
set disassemble-next-line on
從上面截圖中可以看到每一步的指令,但這個反組合結果也是錯的,原因和上面的一樣。
順便介紹個小技巧,如果不輸入命令直接回車會重複上一個GDB命令,就像上圖中最後兩步,什麼命令都沒有直接回車就表示重複執行si
這個GDB命令。
本講最後介紹的指令是退出GDB,非常簡單,輸入q
,然後再輸入y
即可。 截圖如下:
退出GDB後就又回到進入GDB前的Linux命令列環境中了。
如果大家按照上面順序做實驗,退出GDB後,CPU佔用率會比較高,和上講中的情況一樣,直接關閉QEMU視窗即可。這個問題我們在下一講中解決。
本講視訊版地址:https://www.bilibili.com/video/BV18G4y1P7CU/
本教學程式碼和資料:https://gitee.com/jackchengyujia/grapeos-course
GrapeOS作業系統QQ群:643474045