《MIT 6.828 Lab1: Booting a PC》實驗報告
本實驗的網站鏈接見:Lab 1: Booting a PC。
實驗內容
- 熟悉x86匯編語言、QEMU x86仿真器、PC開機引導流程
- 測試6.828 內核的啟動加載器(boot loader)
- 研究6.828 內核的初始化模板(JOS)
實驗題目
注意:部分Exercise的解答過程較長,因此專門新建一個文檔來記錄解答過程,而在本文中給出其鏈接。
Exercise 1:閱讀匯編語言資料
Exercise 1. Familiarize yourself with the assembly language materials available on the 6.828 reference page. You don't have to read them now, but you'll almost certainly want to refer to some of this material when reading and writing x86 assembly.
We do recommend reading the section "The Syntax" in Brennan's Guide to Inline Assembly. It gives a good (and quite brief) description of the AT&T assembly syntax we'll be using with the GNU assembler in JOS.
解答
-
PC Assembly Language Book是一本學習x86匯編語言的好書,不過要注意此書的例子是為NASM匯編器而設計,而我們課程使用的是GNU匯編器。我的學習筆記:《PC Assembly Language》讀書筆記。
-
NASM匯編器使用Intel語法,而GNU匯編器使用AT&T語法。兩者的語法差異可以參考Brennan's Guide to Inline Assembly。我的學習筆記:《Brennan's Guide to Inline Assembly》學習筆記。
Exercise 2:使用GDB命令跟蹤BIOS做了哪些事情
見《MIT 6.828 Lab 1 Exercise 2》實驗報告。
Exercise 3: 使用GDB命令跟蹤boot loader做了哪些事情
見《MIT 6.828 Lab 1 Exercise 3》實驗報告。
Exercise 4: 閱讀C指針材料和pointer.c代碼
見《MIT 6.828 Lab 1 Exercise 4》實驗報告。
Exercise 5: 修改鏈接地址並觀察boot loader運行情況
Exercise 5. Trace through the first few instructions of the boot loader again and identify the first instruction that would "break" or otherwise do the wrong thing if you were to get the boot loader's link address wrong. Then change the link address in boot/Makefrag to something wrong, run make clean, recompile the lab with make, and trace into the boot loader again to see what happens. Don't forget to change the link address back and make clean again afterward!
解答
練習5包括兩部分:一是閱讀boot loader開頭的代碼,並找出修改鏈接地址后會導致指令出錯的地方;二是動手實戰,修改boot/Makeflag中的鏈接地址並觀察boot loader運行情況。
- 閱讀代碼時沒找到會受鏈接地址影響的指令,因此直接實戰。將
-Ttext 0x7C00
改為-Ttext 0x1C00
后,重新編譯,然后gdb調試。我在0x7C00和0x1C00這兩個地址均設置了斷點,然后敲c,發現仍然是在0x7C00停住,再敲一次c,會報異常:“Program received signal SIGTRAP, Trace/breakpoint trap.”我預期修改后boot loader的起始地址應該從0x1c00開始,而gdb調試顯示並沒跑到地址為0x1c00的地方,所以懷疑對鏈接地址的修改沒生效。后來看了fatsheep9146的博客,才知道這是正常的:BIOS是默認把boot loader加載到0x7C00內存地址處,所以boot loader的起始地址仍然是0x7C00.修改鏈接地址后,會導致lgdt gdtdesc
和ljmp $PROT_MODE_CSEG, $protcseg
兩句指令出錯,兩者都需要計算地址,計算方法為鏈接地址加上偏移,因此將鏈接地址修改成與加載地址不一樣后,會導致地址計算失敗。比如這里的gdtdesc和$protcseg的正確地址為0x7c64和0x7c32,修改鏈接地址后兩者的地址分別變為0x1c64和0x1c32。
Exercise 6: 為什么當BIOS進入boot loader時,與當boot loader進入內核時,這兩個時刻在地址為0x00100000的內存中的內容不相同?
Exercise 6. Reset the machine (exit QEMU/GDB and start them again). Examine the 8 words of memory at 0x00100000 at the point the BIOS enters the boot loader, and then again at the point the boot loader enters the kernel. Why are they different? What is there at the second breakpoint? (You do not really need to use QEMU to answer this question. Just think.)
解答
0x00100000這個地址是內核加載到內存中的地址。當BIOS進入boot loader時,還沒將內核加載到這塊內存,其內容是隨機的;而當boot loader進入內核時,內核已經加載完成,其內容就是內核文件內容。因此這兩個階段對應的0x00100000地址的內容是不相同的。可以通過gdb來驗證:
- 當BIOS剛進入boot loader時,地址0x00100000往后的8個word取值均為0.
(gdb) c
Continuing.
[ 0:7c00] => 0x7c00: cli
Breakpoint 1, 0x00007c00 in ?? ()
(gdb) x/8xw 0x00100000
0x100000: 0x00000000 0x00000000 0x00000000 0x00000000
0x100010: 0x00000000 0x00000000 0x00000000 0x00000000
- 當boot loader進入內核時,地址0x00100000往后的8個word已經出現非零值。我們還可以多加幾個斷點,以觀察此內存地址內容最早是什么時候被修改的。實驗證明是在bootmain函數中的for循環第一次結束后被修改的,而for循環做的事情就是將內核中的各個program segment加載到內存中。
(gdb) c
Continuing.
=> 0x7d6b: call *0x10018
Breakpoint 4, 0x00007d6b in ?? ()
(gdb) x/8xw 0x00100000
0x100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766
0x100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8
Exercise 7: 觀察內存地址映射瞬間及分析地址映射失敗的影響
見《MIT 6.828 Lab 1 Exercise 7》實驗報告。
Exercise 8: 串口格式化打印
見《MIT 6.828 Lab 1 Exercise 8》實驗報告。
Exercise 9: 分析內核棧初始化
Exercise 9. Determine where the kernel initializes its stack, and exactly where in memory its stack is located. How does the kernel reserve space for its stack? And at which "end" of this reserved area is the stack pointer initialized to point to?
解答
首先從entry標簽的位置往下閱讀,在relocated標簽下面有句指令:movl $(bootstacktop),%esp
,這句指令把棧指針的值賦給%esp寄存器。繼續往下看,找到bootstack標簽,其中.space KSTKSIZE
語句申請了大小為KSTKSIZE = 8 * PGSIZE = 8 * 4096 字節、初始值全為0的棧空間。再往后定義了bootstacktop標簽,可見棧頂位置處於棧的最高地址上,而棧指針指向棧頂,亦即指向棧的最高地址,這也說明棧是由上到下(高地址向低地址)生長的。棧頂的位置我不知道怎么確定的,通過gdb調試觀察發現$bootstacktop的值為0xf0110000,這個是虛擬地址,實際的物理地址為0x00110000.
.data
.p2align PGSHIFT # force page alignment
.globl bootstack
bootstack:
.space KSTKSIZE
.globl bootstacktop
bootstacktop:
Exercise 10: 研究test_backtrace函數
詳見《MIT 6.828 Lab 1 Exercise 10》實驗報告。
Exercise 11: 實現mon_backtrace函數
詳見《MIT 6.828 Lab 1 Exercise 11》實驗報告。
Exercise 12: backtrace函數增加打印文件名、函數名及行號等信息
詳見《MIT 6.828 Lab 1 Exercise 12》實驗報告。
實驗筆記
環境部署
安裝編譯工具鏈
參考Tools Used in 6.828: Compiler Toolchain。根據objdump -i
和gcc -m32 -print-libgcc-file-name
命令的輸出結果,可以確認我的Ubuntu環境已經支持6.828所需的工具鏈,因此跳過這一環節。
安裝QEMU仿真器
參考Tools Used in 6.828: QEMU Emulator以及Xin Qiu: MIT 6.828 Lab 1。
Part 1: PC Bootstrap
模擬x86
- make命令
make
:編譯最小的6.828啟動加載器和內核make qemu
:運行QEMU。控制台輸出會同時打印在QEMU虛擬VGA顯示和虛擬PC的虛擬串口make qemu-nox
:運行QEMU。控制台輸出只會打印在虛擬串口
PC物理地址空間
-
早期基於8088處理器的PC只支持1MB的物理地址尋址
- 0x00000000 ~ 0x000A0000:640KB,Low Memory,早期PC能夠使用的RAM地址。
- 0x000A0000 ~ 0x000FFFFF:384KB,預留給硬件使用,比如視頻顯示緩存、存儲固件等。
- 0x000A0000 ~ 0x000C0000: 128KB,VGA顯示
- 0x000C0000 ~ 0x000F0000: 192KB,16-bit devices, expansion ROMs
- 0x000F0000 ~ 0x00100000: 64KB,BIOS RAM
-
后來80286和80386處理器出現,能夠支持16MB乃至4GB的物理地址空間,但仍然預留最低的1MB物理地址空間,以便后向兼容已有軟件。因此現代PC在0x000A0000和0x00100000這段內存空間中存在hole,把RAM划分成“low memory”(或“conventional memory”,最低的640KB內存)和“extend memory”(其他內存)兩部分。
Part 2: The Boot loader
-
當BIOS發現一個可啟動的硬盤時,會將512字節的啟動扇區加載到地址為0x7c00到0x7dff的內存中,然后使用jmp指令將CS:IP設置為0000:7c00,從而將控制權交給boot loader。
-
6.828的boot loader由
boot/boot.S
和boot/main.c
兩個文件組成。 -
boot loader主要完成兩個任務:
- 將處理器由實模式切換到虛模式。
- 從硬盤中讀取內核(通過直接訪問IDE磁盤設備寄存器)
-
使用
readelf -a
或objdump -h|-x
命令可以查看elf文件的信息。 -
load_addr(加載地址)和link_addr(鏈接地址)的區別:
- 一個section的load_addr(或“LMA”)是指這個section加載到內存中的地址
- 一個section的link_addr(或“VMA”)是指這個section預期在內存中的運行地址
Part 3: The Kernel
-
內核的VMA和LMA
- 輸入
objdump -h obj/kern/kernel
可以發現內核的VMA(鏈接地址)和LMA(加載地址)不同。 - 一般傾向於將操作系統內核鏈接到很高的虛擬地址來運行,這是為了把低地址留給用戶程序使用。
- 很多機器不具有0xf0100000這個物理地址,我們不能指望一定可以將內核加載到那里。因此,我們使用處理器的內存管理硬件來進行地址映射,將0xf0100000映射到0x00100000.
- lab1中我們只會映射最小的4MB物理內存,將0xf00000000xf0400000與0x000000000x00400000均映射到0x00000000~0x00400000,任何不在這兩段范圍的地址均會導致硬件異常。
- 輸入
-
.space size , fill
This directive emits size bytes, each of value fill. Both size and fill are absolute expressions. If the comma and fill are omitted, fill is assumed to be zero. -
如果同一程序的所有函數開頭都遵循“ebp壓棧,然后將esp的值復制給ebp”的約定,那么沿着被保存的ebp指針這條鏈,將可以追蹤整個調用棧。
問題匯總
-
Q:
make qemu
進入QEMU界面后如何退出?目前我只能通過關閉終端來退出。 -
Q:
make qemu-gdb
進入QEMU界面,然后通過關閉終端退出,再次make qemu-gdb
時報錯:“qemu-system-i386: -gdb tcp::25000: Failed to bind socket: Address already in use”,怎么解決?
A: 發生這種問題是由於端口被程序綁定而沒有釋放造成。可以使用netstat -lp
命令查詢當前處於連接的程序以及對應的進程信息。然后用ps pid
察看對應的進程,並使用kill pid
關閉該進程即可。 -
Q: BIOS, boot_loader和kernel的區別是什么?它們做的事情分別是什么?
-
Q: lab1中我們將0xf00000000xf0400000與0x000000000x00400000均映射到0x000000000x00400000,這樣不會造成沖突嗎?還是說這兩個地址段不會共存(一開始是0x000000000x00400000直接線性映射為自身,而此時程序中的虛擬地址始終也在0x000000000x00400000這個區間內;等到加載內核時,才將0xf00000000xf0400000映射到0x000000000x00400000,而內核程序中的虛擬地址始終也在0xf00000000xf0400000這個區間內)?
-
Q: 將boot loader或kernel加載到內存時,代碼段和數據段分別加載到什么地方?兩者是緊挨着的還是隔得很遠?
-
Q: 理解call和ret指令?