1. 程式人生 > >6.828

6.828

目錄

Lab 1: Booting a PC

Part 1: PC Bootstrap

Brennan's Guide to Inline Assembly

Simulating the x86

The PC's Physical Address Space

The ROM BIOS

Part 2: The Boot Loader

Loading the Kernel

Part 3: The Kernel

Using virtual memory to work around position dependence

Formatted Printing to the Console

The Stack

Lab 2: Memory Management

Introduction

Part 1: Physical Page Management

Part 2: Virtual Memory

Virtual, Linear, and Physical Addresses

Page Table Management

Part 3: Kernel Address Space

Permissions and Fault Isolation

Initializing the Kernel Address Space

Address Space Layout Alternatives

Lab 3: User Environments

Basic inline assembly

Part A: User Environments and Exception Handling

Environment State

Allocating the Environments Array

Basics of Protected Control Transfer

Types of Exceptions and Interrupts

Nested Exceptions and Interrupts

Setting Up the IDT

Part B: Page Faults, Breakpoints Exceptions, and System Calls

The Breakpoint Exception

System calls

User-mode startup

Page faults and memory protection

Lab 4: Preemptive Multitasking

Part A: Multiprocessor Support and Cooperative Multitasking

Application Processor Bootstrap

Locking

Round-Robin Scheduling

Part B: Copy-on-Write Fork

User-level page fault handling

Setting the Page Fault Handler

Normal and Exception Stacks in User Environments

Invoking the User Page Fault Handler

User-mode Page Fault Entrypoint

Implementing Copy-on-Write Fork

Part C: Preemptive Multitasking and Inter-Process communication (IPC)

Clock Interrupts and Preemption

Interrupt discipline

Inter-Process communication (IPC)

IPC in JOS

Sending and Receiving Messages

Transferring Pages


 



Lab 1: Booting a PC

 

實驗室分為三個部分。第一部分著重於熟悉x86組合語言、QEMU x86模擬器和PC的開機啟動過程。第二部分為我們的6.828核心檢查引導載入程式,它駐留在lab樹的目錄中。最後,第三部分將深入研究6.828核心本身的初始模板,名為JOS,它駐留在kernel目錄中。

Part 1: PC Bootstrap

第一個練習的目的是向您介紹x86組合語言和PC引導過程,並開始QEMU和QEMU/GDB除錯。您不需要為實驗室的這一部分編寫任何程式碼,但是為了您自己的理解,您應該閱讀它,並準備好回答下面提出的問題.

練習1:  熟悉可在6.828參考頁上找到的組合語言材料。您現在不必閱讀它們,但是在閱讀和編寫x86程式集時,您肯定希望參考其中的一些內容。我們確實建議閱讀Brennan's Guide to Inline Assembly中的“語法”一節。它對我們將在JOS中與GNU彙編器一起使用的AT&T程式集語法提供了一個很好的(而且相當簡短的)描述。

 

Brennan's Guide to Inline Assembly

好的。這是對DJGPP下的內聯程式集的介紹。DJGPP是基於GCC的,因此它使用AT&T/UNIX語法,並具有某種獨特的內聯組裝方法。我花了很多時間去弄清楚這些東西,我討厭它,很多次。DJGPP使用AT&T程式集語法。這對你意味著什麼?

  • 暫存器的命名:暫存器名稱需要加字首“%”。
AT&T:  %eax
Intel: eax
  • 源/目標排序:

在AT&T語法(這是UNIX標準,順便說一下)中,源總是在左邊,而目標總是在右邊。
那麼讓我們用eax中的值載入ebx:

AT&T:  movl %eax, %ebx
Intel: mov ebx, eax
  • 常量/立即數格式:

必須在所有常量/立即數前加上“$”。
讓我們用“C”變數booga的地址載入eax,它是靜態的。

AT&T:  movl $_booga, %eax
Intel: mov eax, _booga

現在讓我們用0xd00d載入ebx:

AT&T:  movl $0xd00d, %ebx
Intel: mov ebx, d00dh
  • 操作大小規格:

您必須將指令字尾指定位為b、w或l之一,以便將目標暫存器的寬度指定為位元組、字或長字。如果省略這個,GAS (GNU彙編程式)將嘗試猜測。你不想讓GAS 去猜! 那麼不要忘記這個。

AT&T:  movw %ax, %bx
Intel: mov bx, ax
  • 引用記憶體:

DJGPP使用386保護模式,因此您可以忘記所有的真實模式定址遺留垃圾,包括對哪些暫存器具有哪些預設段的限制,哪些暫存器可以是基指標或索引指標。現在,我們得到6個通用暫存器。(當然也可以是7個,如果你使用ebp,但一定要自己恢復它)
以下是32位定址的規範格式:

AT&T:  immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + immed32]

Simulating the x86

我們沒有在真實的物理個人計算機(PC)上開發作業系統,而是使用一個模擬完整的PC的程式:您為模擬器編寫的程式碼也將在真實的PC上啟動。使用模擬器可以簡化除錯;例如,您可以在模擬的x86中設定斷點。

在6.828中,我們將使用QEMU模擬器,這是一種現代且相對快速的模擬器。雖然QEMU內建監視器只提供有限的除錯支援,但QEMU可以充當GNU偵錯程式(GDB)的遠端除錯目標,我們將在本實驗室中使用GDB來逐步完成早期啟動過程。

現在可以執行QEMU了,提供了obj/kern/kernel.img,創建於上面,作為模擬PC的“虛擬硬碟”的內容。這個硬碟映像包含我們的引導載入程式(obj/boot/boot)和核心(obj/kernel)。

執行命令, make qemu.

K>是由核心中包含的小型監視器或互動控制程式列印的提示符。如果您使用make qemu,那麼核心列印的這些行將出現在執行qemu的常規shell視窗和qemu顯示視窗中。這是因為為了測試和實驗室評分的目的,我們已經建立了JOS核心,不僅將其控制檯輸出寫入虛擬VGA顯示(如QEMU視窗所示),還寫入模擬PC的虛擬串列埠.

只有兩個命令可以給核心監視器,help和kerninfo。

幫助命令很明顯,我們稍後將討論kerninfo命令列印內容的含義。儘管很簡單,但是需要注意的是,這個核心監視器是在模擬PC的“原始(虛擬)硬體”上“直接”執行的。這意味著您應該能夠複製bj/kern/kernel.img的內容。在一個真正的硬碟的前幾個扇區,插入硬碟到一個真正的PC,開啟它,在PC的真正螢幕上將看到完全一樣的事情,就像你在上面做的QEMU視窗。我們不建議你在一個真正的機器上做這個.

The PC's Physical Address Space

現在我們將更詳細地介紹PC是如何啟動的。PC機的實體地址空間有以下的總佈局

 

早期pc基於16位的英特爾8088處理器,只能處理1MB的實體記憶體。因此,早期PC的實體地址空間將從0x00000000開始,而結束於0x000FFFFF而不是0xFFFFFFFF。640KB標記為“低記憶體”的區域是早期PC可以使用的唯一隨機訪問記憶體(RAM);事實上,最早的pc只能配置16KB、32KB或64KB的RAM!

從0x000A0000到0x000FFFFF的384KB區域由硬體保留,用於特殊用途,例如視訊顯示緩衝區和非易失性記憶體中的韌體。這個預留區域中最重要的部分是基本輸入/輸出系統(BIOS),它佔用了從0x000F0000到0x000FFFFF的64KB區域。在早期的pc中BIOS儲存在真正的只讀儲存器(ROM)中,但是現在的pc將BIOS儲存在可更新的快閃記憶體中。BIOS負責執行基本的系統初始化,例如啟用顯示卡和檢查安裝的記憶體數量。在執行此初始化之後,BIOS從一些適當的位置(如軟盤、硬碟、CD-ROM或網路)載入作業系統,並將對機器的控制傳遞給作業系統。

當英特爾最終用80286和80386處理器“突破了1兆位元組的限制”,分別支援16MB和4GB的實體地址空間時,PC架構師仍然保留了原來的佈局,只有1MB的實體地址空間,以確保與現有軟體的向後相容性。因此,現代pc機在從0x000A0000到0x00100000的實體記憶體中有一個“洞”,將RAM分為“低”或“常規記憶體”(最初的640KB)和“擴充套件記憶體”(其他所有東西)。此外,PC機的32位實體地址空間頂部的一些空間,首先是物理RAM,現在通常由BIOS保留給32位PCI裝置使用。

x86處理器可以支援超過4GB的物理RAM,因此RAM可以進一步擴充套件到0xFFFFFFFF以上。在這種情況下,BIOS必須安排在32位可定址區域頂部的系統RAM中留下第二個洞,以便為這些32位裝置留下對映空間。由於設計上的限制,JOS將只使用PC機實體記憶體的前256MB。但是處理複雜的實體地址空間和經過多年發展的硬體組織的其他方面是作業系統開發的一個重要的實際挑戰。

The ROM BIOS

在實驗室的這一部分中,您將使用QEMU的除錯工具來研究IA-32相容的計算機引導。
開啟兩個終端視窗,並將兩個shell都cd到您的lab目錄中。其中一個 make qemu-gdb(或make qemu-nox-gdb)。這會啟動QEMU,但QEMU會在處理器執行第一個指令之前停止,並等待GDB的除錯連線。在第二個終端中,從執行make的相同目錄中執行make gdb。你應該看到這樣的東西:

我們提供了一個.gdbinit檔案,它設定了GDB來除錯早期引導期間使用的16位程式碼,並將其附加到偵聽的QEMU上。(如果無法工作,您可能需要在您的主目錄下的.gdbinit中新增一個add-auto-load-safe-path,以說服gdb處理我們提供的.gdbinit。gdb會告訴你是否需要這樣做。

The following line:

[f000:fff0] 0xffff0:	ljmp   $0xf000,$0xe05b

是GDB反彙編要執行的第一個指令。從這個輸出可以得出以下結論:
IBM PC從實體地址0x000ffff0開始執行,它位於為ROM BIOS保留的64KB區域的最頂端。
PC開始執行CS = 0xf000, IP = 0xfff0。要執行的第一個指令是jmp指令,它跳轉到分段地址CS = 0xf000和IP = 0xe05b。

QEMU為什麼會這樣開始呢?這就是英特爾設計8088處理器的方式,IBM在他們最初的PC機上使用了8088處理器。因為在PC BIOS是“天生的”實體地址範圍0 x000f0000-0x000fffff,這種設計可以確保機器的BIOS總是控制任何系統重啟後開啟電源,這是至關重要的,因為沒有其他軟體在機器的記憶體,處理器可以執行。QEMU模擬器自帶BIOS,它將BIOS放在處理器模擬實體地址空間的這個位置。在處理器復位時,(模擬的)處理器進入真實模式,將CS設定為0xf000,將IP設定為0xfff0,以便從(CS:IP)段地址開始執行。分段地址0xf000:fff0如何變成實體地址?

為了回答這個問題,我們需要知道一些關於真實模式定址的知識。在真實模式(PC機開始的模式)中,地址轉換按公式進行:實體地址= 16 *段+偏移量。因此,當PC機將CS設為0xf000, IP設為0xfff0時,所引用的實體地址為:

 16 * 0xf000 + 0xfff0   # in hex multiplication by 16 is
   = 0xf0000 + 0xfff0     # easy--just append a 0.
   = 0xffff0 

0xffff0是BIOS結束前的16位元組(0x100000)。因此,我們不應該對BIOS所做的第一件事感到驚訝,即jmp返回到BIOS中較早的位置;畢竟它能在16個位元組內完成多少任務?

練習2。使用GDB的si (Step Instruction)命令跟蹤到ROM BIOS中以獲得更多的指令,並嘗試猜測它可能在做什麼。您可能希望檢視Phil Storrs I/O埠描述,以及6.828參考資料頁面上的其他材料。不需要弄清楚所有的細節,只需要知道BIOS首先要做什麼。

當BIOS執行時,它會設定一箇中斷描述符表,並初始化各種裝置,比如VGA顯示。這就是您在QEMU視窗中看的“Starting SeaBIOS”訊息的來源。在初始化PCI匯流排和BIOS所知道的所有重要裝置之後,它將搜尋可引導的裝置,如軟盤、硬碟或CD-ROM。最終,當找到一個可引導的磁碟時,BIOS從磁碟中讀取引導載入程式並將控制權轉移到它.

Part 2: The Boot Loader

pc機的軟盤和硬碟被分成512位元組的區域,稱為扇區。扇區是磁碟的最小傳輸粒度:每個讀或寫操作必須是一個或多個扇區大小並在扇區邊界上對齊。如果磁碟是可引導的,第一個扇區稱為引導扇區,因為這是引導載入程式程式碼駐留的地方。當BIOS找到可引導的軟盤或硬碟時,它將512位元組的引導扇區載入到實體地址0x7c00到0x7dff的記憶體中,然後使用jmp指令將CS:IP設定為0000:7c00,並將控制權傳遞給引導載入程式. 像BIOS載入地址,這些地址是相當任意的-但他們是固定的和標準化的pc。

在PC的發展過程中,從CD-ROM引導的能力姍姍來遲,因此PC架構師利用這個機會稍微反思了引導過程。因此,從CD-ROM引導現代BIOS的方式要複雜一些(而且更強大)。cd - rom使用的扇區大小為2048位元組,而不是512位元組,BIOS可以在將控制權轉移到它之前將一個更大的引導映像從磁碟載入到記憶體(而不僅僅是一個扇區)。

然而,對於6.828,我們將使用傳統的硬碟驅動器引導機制,這意味著我們的引導載入程式必須適合512位元組。引導載入程式由一個組合語言原始檔boot/boot.S組成。和一個C原始檔,boot/main.c仔細檢查這些原始檔,確保你明白髮生了什麼。引導載入程式必須執行兩個主要功能:

1.首先,引導載入程式將處理器從真實模式切換到32位保護模式,因為只有在這種模式下,軟體才能訪問處理器實體地址空間中超過1MB的所有記憶體。保護模式在PC組合語言的1.2.7和1.2.8節中進行了簡要介紹,並在Intel體系結構手冊中有詳細介紹。此時,您只需要瞭解在保護模式下,將分段地址(段:偏移對)轉換為實體地址的方式是不同的,轉換偏移量是32位而不是16位。

2.其次,引導載入程式通過x86特殊的I/O指令直接訪問IDE磁碟裝置暫存器,從硬碟讀取核心。如果您想更好地理解這裡的特定I/O指令的含義,請檢視6.828參考頁面上的“IDE硬碟驅動器控制器”部分。在這門課中,您將不需要學習太多關於特定裝置的程式設計:編寫裝置驅動程式實際上是作業系統開發的一個非常重要的部分,但從概念或體系結構的觀點來看,它也是最無趣的部分之一。

在理解引導載入程式原始碼之後,檢視檔案obj/boot/boot.asm。這個檔案是我們的GNUmakefile在編譯引導載入程式後建立的引導載入程式的分解。這個反彙編檔案可以很容易地檢視實體記憶體中所有引導載入程式程式碼的駐留位置,並使跟蹤在GDB中的引導載入程式時發生的事情變得更容易。同樣,obj /kern/kernel.asm包含JOS核心的分解,這通常對除錯有用。

可以使用b命令在GDB中設定地址斷點。例如,b *0x7c00在地址0x7c00設定一個斷點。在一個斷點處,您可以使用c和si命令繼續執行:c使QEMU繼續執行,直到下一個斷點(或者直到您在GDB中按Ctrl-C),並且si N每次執行指令N步。

為了檢查記憶體中的指令(除了下一個要執行的指令,GDB會自動列印),您可以使用x/i命令。這個命令有語法x/Ni ADDR,其中N是要列印的連續指令的數量,ADDR是開始列印的記憶體地址。

練習3。看看實驗工具指南,特別是GDB命令部分。即使您熟悉GDB,這也包括一些對作業系統有用的深奧的GDB命令。
在地址0x7c00設定一個斷點,這是載入引導扇區的地方。繼續執行直到那個斷點。跟蹤引導/引導中的程式碼。使用原始碼和反彙編檔案obj/boot/boot.asm來跟蹤你的位置。還可以使用GDB中的x/i命令來拆解引導載入程式中的指令序列,並比較原始引導載入程式原始碼。在boot/main.c中跟蹤引導到bootmain(),然後進入readsect()。識別與readsect()中的每個語句對應的確切彙編指令。跟蹤readsect()的其餘部分並將其返回到bootmain()中,並標識從磁碟讀取核心其餘扇區的for迴圈的開始和結束部分。找出迴圈結束後執行的程式碼,在那裡設定一個斷點,並繼續該斷點。然後逐步執行引導載入程式的其餘部分。

你應該能夠回答以下問題:

  • 處理器從什麼時候開始執行32位程式碼?到底是什麼導致了從16位模式到32位模式?

ljmp   $0x8,$0x7c32這條指令以後開始執行32位程式碼; 開啟A20,將CR0某個標誌位置1。

  • 引導載入程式執行的最後一條指令是什麼?剛剛載入的核心的第一條指令是什麼?

call *0x10018;       movw $0x1234, 0x472

  • 核心的第一個指令在哪裡?

位於/kern/entry.S檔案中

  • 引導載入程式如何決定必須讀取多少扇區才能從磁盤獲取整個核心?它在哪裡找到這些資訊?

Loading the Kernel

現在,我們將在boot/main.c中更詳細地研究引導載入程式的C語言部分。但在此之前,現在是停止並回顧一些C程式設計基礎。

練習4。閱讀關於用C語言編寫指標的文章。C語言最好的參考文獻是Brian Kernighan和Dennis Ritchie(被稱為“K&R”)的C語言。我們建議購買這本書。讀K&R中的5.1(指標和地址)到5.5(字元指標和函式)。然後下載程式碼。執行它,並確保您理解所有列印值的來源。特別是,確保您理解列印第1行和第6行中的指標地址來自哪裡,列印第2行到第4行中的所有值是如何到達那裡的,以及為什麼列印第5行中的值看起來是損壞的。警告:除非你已經精通C語言,否則不要跳過甚至略讀這個閱讀練習。如果你不真正理解C語言中的指標,你將會在後續的實驗中遭受難以言喻的痛苦和痛苦,然後最終以艱難的方式理解它們。相信我們;你不想知道什麼是“艱難的方法”。

boot/main.c 的意義。你需要知道什麼是ELF二進位制。當您編譯並連結一個C程式(如JOS核心)時,編譯器會將每個C原始檔('. C ')檔案轉換為一個物件('.o')檔案,其中包含用硬體期望的二進位制格式編碼的組合語言指令。然後連結器將所有已編譯的物件檔案組合成一個二進位制映像,例如obj/kern/kernel,在本例中,它是ELF格式的二進位制映像,表示“可執行的和可連結的格式”。

關於這種格式的完整資訊可以在我們參考頁面的ELF規範中找到,但是在這個類中您不需要深入研究這種格式的細節。雖然總的來說,這種格式非常強大和複雜,但是大多數複雜的部分都是為了支援共享庫的動態載入,這是我們在這個類中不會做的。維基百科頁面有一個簡短的描述。

對於6.828,您可以將ELF可執行檔案看作是一個帶有載入資訊的標頭檔案,後面跟著幾個程式節,每一個都是一個連續的程式碼塊或資料塊,目的是將其載入到指定地址的記憶體中。引導載入程式不修改程式碼或資料;它將它載入到記憶體中並開始執行。

ELF二進位制檔案以固定長度的ELF頭開始,後面是一個可變長度的程式頭,列出了要載入的每個程式節。這些ELF頭的C定義在inc/ ELF .h中。我們感興趣的程式節有:

.text:程式的可執行指令。

.rodata:只讀資料,如由C編譯器生成的ASCII字串常量。(不過,我們不會費心設定硬體來禁止寫入。)

.data: data部分儲存程式初始化的資料,例如使用int x = 5這樣的初始化宣告的全域性變數;

當連結器計算程式的記憶體佈局時,它為未初始化的全域性變數預留空間,例如int x;,在記憶體中緊跟著.data的.bss部分中。C要求“未初始化”的全域性變數從0開始。因此,不需要在ELF二進位制中儲存.bss的內容;相反,連結器只記錄.bss部分的地址和大小。載入器或程式本身必須將.bss節設定為零。

通過輸入以下命令檢查核心可執行檔案中所有節的名稱、大小和連結地址的完整列表:

objdump -h obj/kern/kerne

l

 

您將看到比上面列出的更多的部分,但是其他部分對於我們的目的並不重要。其他大多數都用來儲存除錯資訊,這些資訊通常包含在程式的可執行檔案中,但是程式載入器不會載入到記憶體中。

請特別注意.text部分的“VMA”(或連結地址)和“LMA”(或載入地址)。段的載入地址是將該段載入到記憶體中的記憶體地址。

節的連結地址是節期望執行的記憶體地址。連結器以不同的方式對二進位制檔案中的連結地址進行編碼,比如當代碼需要全域性變數的地址時,結果是,如果二進位制檔案是從沒有連結的地址執行的,那麼通常不會工作。(可以生成不包含任何絕對地址的位置無關程式碼。這被現代共享庫廣泛使用,但是它有效能和複雜性成本,所以我們在6.828中不會使用它)。

通常,連結和載入地址是相同的。例如,檢視引導載入程式的.text部分:

objdump -h obj/boot/boot.out

引導載入程式使用ELF程式頭來決定如何載入節。程式頭指定要載入到記憶體中的ELF物件的哪些部分以及每個部分應該佔用的目標地址。

      objdump -x obj/kern/kernel            

然後,在objdump的輸出中,程式頭被列在“程式頭”下。需要載入到記憶體中的ELF物件區域是那些標記為“LOAD”的區域。每個程式頭都提供了其他資訊,例如虛擬地址(“vaddr”)、實體地址(“paddr”)和載入區域的大小(“memsz”和“filesz”)。

回到boot/main.c,每個程式頭的ph->p_pa欄位包含段的目標實體地址(在本例中,它實際上是一個實體地址,儘管ELF規範對該欄位的實際含義含糊其辭)。

BIOS將引導扇區載入到從地址0x7c00開始的記憶體中,因此這是引導扇區的載入地址。這也是引導扇區執行的地方,所以這也是它的連結地址。我們通過將-Ttext 0x7C00傳遞給boot/Makefrag中的連結器來設定連結地址,這樣連結器將在生成的程式碼中生成正確的記憶體地址。

練習5。再次跟蹤引導載入程式的前幾條指令,並確定如果引導載入程式的連結地址出錯,第一條指令將“中斷”或執行錯誤操作。然後將boot/Makefrag中的連結地址更改為一些錯誤的值,執行make clean,使用make重新編譯實驗,並再次跟蹤到引導載入程式中,以檢視發生了什麼。別忘了把連結地址改回來,然後重新清理!

回顧一下核心的載入和連結地址。與引導載入程式不同,這兩個地址並不相同:核心告訴引導載入程式以低地址(1兆位元組)將其載入到記憶體中,但它希望從高地址執行。在下一節中,我們將深入探討如何使其工作

除了節資訊,ELF頭中還有一個對我們很重要的欄位e_entry。這個欄位包含程式入口點的連結地址:程式text節的記憶體地址,程式應該在該地址開始執行。你可以看到入口點:

objdump -f obj/kern/kernel

現在您應該能夠理解boot/main.c中的最小ELF載入程式了。它將核心的每個節從磁碟讀入位於該節載入地址的記憶體中,然後跳到核心的入口點。

練習6。我們可以使用GDB的x命令檢查記憶體。GDB手冊有完整的細節,但是現在,只要知道命令x/Nx ADDR在ADDR上列印了N個word就足夠了。(注意,命令中的兩個'x'都是小寫字母。)警告:單詞的大小不是通用的標準。在GNU彙編中,一個word是兩個位元組(xorw中的“w”代表單詞,意思是2位元組)。重置機器(退出QEMU/GDB並重新啟動)。在BIOS進入引導載入程式時檢查0x00100000處的記憶體8個字,然後在引導載入程式進入核心時再檢查一次。

 

Part 3: The Kernel

現在 , 我們將開始更詳細地研究最小JOS核心。(你最終會寫一些程式碼!)與引導載入程式一樣,核心從一些組合語言程式碼開始,這些程式碼設定了一些東西,以便C語言程式碼能夠正確執行。

Using virtual memory to work around position dependence

當您檢查上面的引導載入程式的連結和載入地址時,它們完全匹配,但是核心的連結地址(由objdump列印)和載入地址之間存在(相當大的)差異。回去檢查這兩個,確保你知道我在說什麼。(連結核心比引導載入程式更復雜,因此連結和載入地址位於kern/kernel.ld的頂部。)

作業系統核心通常喜歡在非常高的虛擬地址(例如0xf0100000)上進行連結和執行,以便將處理器虛擬地址空間的較低部分留給使用者程式使用。這種安排的原因將在下一個實驗中變得更清楚。

許多機器在地址0xf0100000處沒有任何實體記憶體,所以我們不能指望能夠在那裡儲存核心。相反,我們將使用處理器的記憶體管理硬體將虛擬地址0xf0100000(核心程式碼期望執行的連結地址)對映到實體地址0x00100000(在那裡引導載入程式將核心載入到實體記憶體中)。這樣,雖然核心的虛擬地址足夠高,可以為使用者程序留下足夠的地址空間,但它將被載入到實體記憶體中,在PC的RAM中1MB的地方,就在BIOS ROM的上方。這種方法要求PC至少有幾兆的實體記憶體(實體地址是在0x00100000工作),對於大約在1990年以後構建的任何PC來說,這是完全沒問題的。

實際上,在下一個實驗室中,我們將對映PC機的整個底層256MB的實體地址空間,從實體地址0x00000000到0x0fffffff,到虛擬地址0xf0000000到0xffffffff。現在您應該知道為什麼JOS只能使用第一個256MB的實體記憶體了。

現在,我們只需要對映第一個4MB的實體記憶體,這足以讓我們啟動並執行。我們使用kern/entrypgdir.c中的手寫的、靜態初始化的頁面目錄和頁表來完成此操作。現在,您不需要了解它如何工作的細節,只需要瞭解它實現的效果。直到kern/entry.S。S設定CR0_PG標誌,記憶體引用被視為實體地址(嚴格來說,它們是線性地址,但是引導/引導)。我們建立了一個從線性地址到實體地址的恆等對映,我們永遠不會改變它)。

一旦設定了CR0_PG,記憶體引用就是虛擬記憶體硬體將其轉換為實體地址的虛擬地址。entry_pgdir將0xf0000000到0xf0400000範圍內的虛擬地址轉換為實體地址0x00000000到0x00400000,以及虛擬地址0x00000000到0x00400000到實體地址0x00000000到0x00400000。任何不在這兩個範圍內的虛擬地址都會導致硬體異常,因為我們還沒有設定中斷處理,這會導致QEMU轉儲機器狀態並退出(或無休止地重新引導).

練習7。使用QEMU和GDB跟蹤到JOS核心,並在movl %eax, %cr0處停止。檢查0x00100000和0xf0100000的記憶體。現在,使用stepi GDB命令對該指令進行單步操作。再次檢查0x00100000和0xf0100000的記憶體。確保你明白剛才發生了什麼。
在建立新的對映之後,如果沒有對映就不能正常工作的第一個指令是什麼?註釋掉kern/entry.S中的movl %eax和%cr0,追蹤它,看看你是不是對的。

執行movl %eax, %cr0   以後:

如果沒有對映就不能正常工作的第一個指令是jmp *%eax的下一條指令。

改回來以後:

Formatted Printing to the Console

大多數人認為printf()之類的函式是理所當然的,有時甚至認為它們是C語言的“原語”。但在作業系統核心中,我們必須自己實現所有I/O。通讀kern/printf.c, lib/printfmt.c, kern/console.c,確保你理解他們的關係。在以後的實驗室中,你將會清楚為什麼printfmt.c位於獨立的lib目錄中。

練習8。我們省略了一小段程式碼——使用形式為“%o”的模式列印八進位制數所必需的程式碼。查詢並填寫此程式碼片段。

 

能夠回答以下問題:

1. 解釋printf.c和console.c之間的介面。具體來說,console.c匯出了什麼函式。printf.c如何使用這些函式?‘

console.c

printf.c

2.解釋console.c以下內容:

當快取區字元滿了以後,執行螢幕滾動操作。

3.對於以下問題,您可能希望查閱lecture 2的講義。這些筆記涵蓋了GCC在x86上的呼叫約定。

逐步跟蹤以下程式碼的執行:

int x = 1, y = 3, z = 4;

cprintf("x %d, y %x, z %d\n", x, y, z);

在對cprintf()的呼叫中,fmt指向什麼?ap指的是什麼?列出對cons_putc、va_arg和vcprintf的每個呼叫(按執行順序)。對於cons_putc,也列出它的引數。對於va_arg,列出ap在呼叫前後指向的內容。對於vcprintf,列出它的兩個引數的值。

呼叫cprintf函式:

cprintf函式內部

 

4.執行以下程式碼:

輸出是什麼?按照前面練習的方式解釋如何逐步得到這個輸出。

輸出取決於x86是little-endian的事實。如果x86是big-endian,為了得到相同的輸出,你會將i設為什麼?您需要將57616更 改為不同的值嗎?

i 設定為0x726c6400,  57616不需要更改。

5.在下面的程式碼中,在“y=”之後將列印什麼?(注意:答案不是一個特定的值。)為什麼會這樣呢?

      cprintf("x=%d y=%d", 3);

6.假設GCC改變了它的呼叫約定,以便它按照宣告順序在堆疊上推送引數,這樣最後一個引數就會被推送到最後。您將如何更改cprintf或其介面,以便仍然可以向其傳遞數量可變的引數?

The Stack

在這個實驗的最後練習中,我們將更詳細地探討C語言使用x86上的堆疊的方式,並在這一過程中寫一個有用的新的核心監控功能,輸出一個堆疊回溯:儲存從巢狀呼叫指令導致的當前執行點的指標(IP)的列表值。

練習9。確定核心初始化其棧的位置,以及棧在記憶體中的確切位置。核心如何為其棧保留空間?在這個保留區域的“末端”,棧指標被初始化指向什麼?

x86棧指標(esp暫存器)指向當前正在使用的堆疊上的最低位置。為棧保留的區域中該位置以下的所有記憶體都是空閒的。將一個值壓入堆疊涉及到減少堆疊指標,然後將該值寫入堆疊指標指向的位置。從堆疊中取出一個值涉及到讀取堆疊指標指向的值,然後增加堆疊指標。在32位模式下,堆疊只能儲存32位值,esp總是能被4整除。各種x86指令,比如call,都是“硬連線”來使用棧指標暫存器。

相反,ebp(基指標)暫存器主要通過軟體約定與堆疊關聯。在進入C函式時,函式的建立堆疊框架的程式碼通常通過將前一個函式的基指標推入堆疊來儲存它,然後在函式執行期間將當前esp值複製到ebp中。如果一個程式的所有的函式都遵守這個約定,那麼在在程式的執行期間任何給定的點, 通過跟蹤儲存的ebp指標鏈,並準確地確定是哪些巢狀的函式呼叫序列導致程式中的這個特定點到達,可以進行堆疊跟蹤返回。這種功能可能特別有用,例如,當某個特定函式由於傳遞了錯誤的引數而導assert failure or panic時,但是你不確定是誰傳遞了錯誤的引數時。堆疊回溯可以讓你找到有問題的函式。

練習10.  熟悉x86上的C呼叫約定,在obj/kern/kernel.asm中查詢test_backtrace函式的地址,在那裡設定一個斷點,並檢查每次在核心啟動後呼叫它時發生了什麼。test_backtrace的每個遞迴巢狀層在堆疊上推了多少個32位word?這些word是什麼?
注意,為了使這個練習正常工作,您應該使用工具頁面上可用的補丁版本QEMU。否則,您必須手動將所有斷點和記憶體地址轉換為線性地址。

呼叫test_backtrace(5)

在test_backtrace(5)堆疊框架建好後esp, ebp的值。

呼叫test_backtrace(4)

4後面那個5是cprintf壓棧壓進去的。

在test_backtrace(4)堆疊框架建好後esp, ebp的值。

 3 

  

2

1

0

上面的練習應該為您提供實現堆疊回溯函式所需的資訊,您應該呼叫mon_backtrace()。在kern/monitor.c中,這個函式的原型已經在等待您了, 用C語言中實現這個函式,你可以inc/x86中找到read_ebp()函式,它是有用的。你還必須將這個新函式連線到核心監視器的命令列表中,以便使用者可以互動式地呼叫它。

backtrace函式應該以以下格式顯示函式呼叫框架列表:

Stack backtrace:
  ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031
  ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061
  ...

每行包含ebp、eip和args。ebp值表示該函式使用的堆疊的基指標:在進入函式和函式序言程式碼之後,將棧指標的值賦值基指標。列出的eip值是函式的返回指令指標:當函式返回時將返回的指令地址。返回指令指標通常指向呼叫指令之後的指令(為什麼?)最後,args後面列出的5個十六進位制值是該函式的前5個引數,這些引數將在呼叫該函式之前被推送到棧上。當然,如果函式呼叫的引數少於5個,不是所有的5個值都有用。(為什麼回溯程式碼不能檢測到底有多少引數?這一限制如何被修正?)

列印的第一行反映當前執行的函式,即mon_backtrace本身,第二行反映呼叫mon_backtrace的函式,第三行反映呼叫該函式的函式,依此類推。您應該列印所有的堆疊幀。通過研究 kern/entry.S條目。你會發現有一種簡單的方法可以告訴你什麼時候停下來。

以下是你在K&R第五章中讀到的一些特別的觀點,值得你在接下來的練習和未來的實驗中記住。

如果int* p = (int*)100,那麼(int)p + 1和(int)(p + 1)是不同的數字:第一個是101,第二個是104。當向指標中新增整數時,如第二種情況,該整數隱式地乘以指標指向的物件的大小。

p[i]被定義為與*(p+i)相同,指的是p指向記憶體中的第i個物件。當物件大於一個位元組時,上面的加法規則有助於這個定義的工作。

&p[i]與(p+i)相同,表示p指向記憶體中第i個物件的地址。

雖然大多數C程式永遠不需要在指標和整數之間轉換,但是作業系統經常這樣做。每當看到涉及記憶體地址的加法時,請自問它是整數加法還是指標加法,並確保所新增的值是否正確相乘。

練習11.  實現上面指定的回溯函式。使用與示例相同的格式,否則會混淆分級指令碼。當您認為它工作正常時,執行make grade,看看它的輸出是否符合我們的分級指令碼的要求,如果不符合,則修復它。在您提交了lab1程式碼之後,歡迎您以任何方式更改backtrace函式的輸出格式。

如果使用read_ebp(),請注意GCC可能會生成“優化”程式碼,在mon_backtrace()的函式序言之前呼叫read_ebp(),這會導致不完整的堆疊跟蹤(最近一次函式呼叫的堆疊框架丟失)。雖然我們已經嘗試禁用導致這種重新排序的優化,但是你應該檢查mon_backtrace()的彙編程式碼,並確保在函式序言之後呼叫read_ebp()。

 

此時,backtrace函式應該給出堆疊上導致mon_backtrace()被執行的函式呼叫者的地址。然而,在實踐中,您通常希望知道與這些地址對應的函式名。例如,您可能想知道哪些函式可能包含導致核心崩潰的bug。

為了幫助您實現此功能,我們提供了debuginfo_eip()函式,它在符號表中查詢eip並返回該地址的除錯資訊。這個函式在kern/kdebug.c中定義。

// Debug information about a particular instruction pointer
struct Eipdebuginfo {
        const char *eip_file;           // Source code filename for EIP
        int eip_line;                   // Source code linenumber for EIP

        const char *eip_fn_name;        // Name of function containing EIP
                                        //  - Note: not null terminated!
        int eip_fn_namelen;             // Length of function name
        uintptr_t eip_fn_addr;          // Address of start of function
        int eip_fn_narg;                // Number of function arguments
};

練習12。 修改堆疊回溯函式以顯示每個eip對應的函式名、原始檔名和行號。

在debuginfo_eip中,__STAB_*從哪裡來?這個問題的答案很長;為了幫助你找到答案,這裡有一些你可能想做的事情:

  • look in the file kern/kernel.ld for __STAB_*
  • run objdump -h obj/kern/kernel
  • run objdump -G obj/kern/kernel
  • run gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s.
  • see if the bootloader loads the symbol table in memory as part of loading the kernel binary

通過插入對stab_binsearch的呼叫來查詢地址的行號,完成debuginfo_eip的實現。

向核心監視器新增一個回溯命令,並擴充套件mon_backtrace的實現,呼叫debuginfo_eip,並將每個堆疊幀的表單列印:


Stack backtrace:
ebp f010ff68 eip f0100971 args 00000001 f010ff80 00000000 f010ffc8 f0112540
       kern/monitor.c:148  monitor+258
ebp f010ffd8 eip f01000f6 args 00000000 00001aac 00000640 00000000 00000000
       kern/init.c:43  i386_init+89
ebp f010fff8 eip f010003e args 00111021 00000000 00000000 00000000 00000000
       kern/entry.S:83  <unknown>+0

每行給出堆疊幀eip檔案中的檔名和行號,後面跟著函式名和eip與函式第一條指令的偏移量(例如,monitor+106表示返回eip的值比monitor開頭的值多了106位元組)

請確保將檔案和函式名列印在單獨的一行上,以避免干擾評分指令碼。

提示:printf格式字串提供了一種簡單(儘管不太清楚)的方式來列印非空終止的字串,比如STABS表中的字串。printf("%.*s", length, string) 列印最多長度的為length字串字元。請檢視printf手冊頁以瞭解其工作原理。

您可能會發現回溯跟蹤中缺少一些函式。例如,您可能會看到對monitor()的呼叫,但不會看到對runcmd()的呼叫。這是因為編譯器內聯了一些函式呼叫。其他優化可能會導致您看到意外的行號。如果您從GNUMakefile中刪除了-O2,那麼回溯可能更有意義(但是您的核心會執行得更慢)。

程式碼實現如下:

 





 

Lab 2: Memory Management

Introduction

在這個實驗室中,您將為您的作業系統編寫記憶體管理程式碼。記憶體管理有兩個元件。

第一個元件是核心的實體記憶體分配器,這樣核心就可以分配記憶體,然後釋放它。您的分配器將以4096位元組為單位進行操作,稱為頁面。您的任務是維護資料結構,這些資料結構記錄哪些物理頁面是空閒的,哪些是分配的,以及有多少程序共享每個分配的頁面。您還將編寫分配和釋放記憶體頁面的程式。

記憶體管理的第二個元件是虛擬記憶體,它將核心和使用者軟體使用的虛擬地址對映到實體記憶體中的地址。x86硬體的記憶體管理單元(MMU)在指令使用記憶體時執行對映,並參考一組頁表。您將修改JOS以根據我們提供的規範設定MMU的頁表。

Part 1: Physical Page Management

作業系統必須跟蹤物理RAM的哪些部分是空閒的,哪些部分目前正在使用。JOS以頁面粒度管理PC的實體記憶體,這樣它就可以使用MMU來對映和保護分配的每一塊記憶體。現在您將編寫物理頁面分配器。它使用struct PageInfo物件的連結列表(與xv6不同的是,這些物件沒有嵌入到空閒頁面本身中)來跟蹤哪些頁面是空閒的,每個物件都對應於一個物理頁面。在編寫餘