Lab3
這個實驗分成了兩個大部分。
1. PartA User Environments and Exception Handling
kernel使用Env這個資料結構來trace每一個user enviroment,你需要設計JOS來支援多environments。
kernel維護三個主要的全域性變數來完成上面的內容
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list
1.1Creating and Running Environments
1. Environment State
Env結構體定義在inc/env.h
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run
// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
};
具體的解釋在實驗指導書中有,後面用到了在來解釋
2 Allocating the Environments Array
這裡要求我們修改mem_init
來為env結構體分配空間
其實這個分配空間以及對映什麼的lab2都熟了。但是這裡我遇到了一個問題。
就是切換到lab3之後直接make qemu
會報下面的錯誤
這個問題我修了好久好久。。。。我剛開始是以為我lab2有bug但是lab2的評測沒有測出來,然後就去瘋狂printf找問題。。。後面google了一下發現好像是一個很簡單的問題。只需要修改kern/kernel.ld
裡面多加一行就可以了
--- a/kern/kernel.ld
+++ b/kern/kernel.ld
@@ -50,6 +50,7 @@ SECTIONS
.bss : {
PROVIDE(edata = .);
*(.bss)
+ *(COMMON)
PROVIDE(end = .);
BYTE(0)
}
然後就是lab3的內容了
第一部分非常簡單
//your lab3 code
sizes = sizeof(struct Env) * NENV;
envs = (struct Env*)boot_alloc(sizes);
memset(envs, 0, sizes);
//your lab3 code
boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U);
這樣就可以過掉第一部分的程式碼,但這裡其實是把低地址和高地址的一部分都對映到了相同的實體地址。應該是為了使用者模式的方便(猜的)
3 Creating and Runing Environments
您現在將在執行使用者環境所需的kern / env.c中編寫程式碼。 由於我們尚未擁有檔案系統,因此我們將設定核心以載入嵌入在核心本身內的靜態二進位制映象。 JOS將該二進位制檔案嵌入核心中作為ELF可執行映象
在i386_init()
這個函式中有在一個環境中執行這些二進位制映象的程式碼,但是它們還不完整。在Exercise2
中你需要補齊下面的函式
3.1 env_init
這個函式的實現比較簡單,基本上根據提示可以秒寫。但是注意提示說我們從free_env_list
返回的env應該是有順序的.如先返回env[0]、env[1]
以此類推。。所以要用尾插法
void
env_init(void)
{
// Set up envs array
// LAB 3: Your code here.
size_t i = 0;
for (; i < NENV; i++) {
envs[i].env_id = 0;
// ATTENTION: must tail insert
if (i == 0) {
env_free_list = &envs[0];
} else {
env_free_list->env_link = &envs[i];
}
}
// Per-CPU part of the initialization
env_init_percpu();
}
這裡補充一些關於gdt和ldt的知識
主要看下面這張圖
LDT和GDT從本質上說是相同的,只是LDT巢狀在GDT之中。LDTR記錄區域性描述符表的起始位置,與GDTR不同LDTR的內容是一個段選擇子。由於LDT本身同樣是一段記憶體,也是一個段,所以它也有個描述符描述它,這個描述符就儲存在GDT中,對應這個表述符也會有一個選擇子,LDTR裝載的就是這樣一個選擇子。LDTR可以在程式中隨時改變,通過使用lldt指令。如上圖,如果裝載的是Selector 2則LDTR指向的是表LDT2。
其實只要知道LDT就是GDT中的一些段。然後我們有LDTR來指向LDT的起始地址,所以LDTR裡面裝的是段選擇子
下面具體分析一下GDT
它具體的結構如下
在程式碼中表現形式如下
// Segment Descriptors
struct Segdesc {
unsigned sd_lim_15_0 : 16; // Low bits of segment limit
unsigned sd_base_15_0 : 16; // Low bits of segment base address
unsigned sd_base_23_16 : 8; // Middle bits of segment base address
unsigned sd_type : 4; // Segment type (see STS_ constants)
unsigned sd_s : 1; // 0 = system, 1 = application
unsigned sd_dpl : 2; // Descriptor Privilege Level
unsigned sd_p : 1; // Present
unsigned sd_lim_19_16 : 4; // High bits of segment limit
unsigned sd_avl : 1; // Unused (available for software use)
unsigned sd_rsv1 : 1; // Reserved
unsigned sd_db : 1; // 0 = 16-bit segment, 1 = 32-bit segment
unsigned sd_g : 1; // Granularity: limit scaled by 4K when set
unsigned sd_base_31_24 : 8; // High bits of segment base address
};
上面的圖和這裡的設定完全一致。具體的細節這裡就不放了這篇部落格寫的非常好
3.2 env_setup_vm
這裡說實話,我剛開始沒太看懂註釋的意思,什麼在utop之上是完全一樣。。balabala的
但是總體的思路就是給e指向的Env結構分配頁目錄,並且繼承核心的頁目錄結構,這裡唯一需要修改的就是UVPT要對映到當前環境的頁目錄實體地址e->env_pgdir處。而不是核心的頁目錄實體地址kern_pgdir
處。
同時這個實驗要求。物理對映只需要對映utop之上的。也就是要把從uenvs - utop這一部分初始化為0就好。
p->pp_ref++;
e->env_pgdir = page2kva(p);
memcpy(e->env_pgdir,kern_pgdir,PGSIZE);
size_t i = 0;
for (; i < PDX(UTOP); i++) {
e->env_pgdir[i] = 0;
}
// UVPT maps the env's own page table read-only.
// Permissions: kernel R, user R
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
3.3 region_alloc
要給當前環境分配和對映實體記憶體。只要向虛擬地址va分配並對映物理頁就行.
主要是根據提示以及lab2的一些知識就可以完成下面的內容
static void
region_alloc(struct Env *e, void *va, size_t len)
{
// LAB 3: Your code here.
// (But only if you need it for load_icode.)
//
// Hint: It is easier to use region_alloc if the caller can pass
// 'va' and 'len' values that are not page-aligned.
// You should round va down, and round (va + len) up.
// (Watch out for corner-cases!)
struct PageInfo *p;
for (size_t i = ROUNDDOWN(va, PGSIZE); i < ROUNDUP(va + len, PGSIZE); i+= PGSIZE) {
if (!(p = page_alloc(0))) {
panic("region_alloc error");
} else {
if (!page_insert(e->env_pgdir,p,i,(PTE_P | PTE_U | PTE_W))) {
panic("region_alloc map error");
}
}
}
}
3.4 load_icode
你需要將ELF的binary imgae parse進使用者空間的新的環境中
這裡需要參照boot_main
讀取elf方式進行讀取。
- 首先在核心態載入ELF檔案,然後利用
lcr3
函式跳轉到使用者態獲取檔案內容。讀取完後在用lcr3
跳轉回到核心態 - 設定入口elf-entry,分配一個頁作為使用者程式的棧,這裡只考慮一個使用者程式,設定從
USTACKTOP - PGSIZE
開始的PGSIZE
大小的地址空間
這個地方還是比較難寫的。這裡我又去看了看csapp的第七章
主要是下面的內容,就是我們把下面的二進位制elf可執行目標檔案讀到當前的環境變數中。
上面有一些引數對於的解釋就是。
-
程式段頭表,描述了我們要load進記憶體的段的資訊,因此我們先找到程式段頭表
-
上圖中描述了兩個段分別是隻讀程式碼段喝讀寫記憶體段,其中vaddr喝paddr分別表示這一段段實體地址和虛擬地址,這裡可以發現它們是完全一樣的。
因為這裡這些段還沒有被讀進對應程式的虛擬地址空間內,也就不知道他們執行的時候對應的實體地址空間是什麼,所以它們在此時是和虛擬地址完全一樣的。
filesz
表示這一段的大小,而memsz
表示這段存入記憶體後要佔用的大小。這兩個值大部分時間都是一樣的,但是當有.bss這種佔位符存在的時候就變得不一樣了。 -
off
表示的我們要把可執行目標檔案中偏移off
位置處的filesz
大小的資料來初始化(其實就是把這部分的data直接copy過去)而這部分的data其實就是一些對映關係。當前程式環境變數。如果filesz < memsz
剩餘的地方將被初始化成0.
struct Elf *ELF_Header = (struct Elf *) binary;
if (ELF_Header->e_magic != ELF_MAGIC) {
panic("The binary is not a ELF magic!\n");
}
if (ELF_Header->e_entry == 0) {
panic("The program can't be executed because the entry point is invalid!\n");
}
// e->env_tf holds the saved register values for the environment
e->env_tf.tf_eip = ELF_Header->e_entry;
lcr3(PADDR(e->env_pgdir));
struct Proghdr *ph,eph;
ph = (struct Proghdr *)((uint8_t *) ELF_Header + ELF_Header->e_phoff); // program sector table
eph = ph + ELF_Header->e_phnum;
for (; ph < eph; ph++) {
if (ph->p_type == ELF_PROG_LOAD) {
if (ph->p_memsz < ph->p_filesz) {
panic("segment out of memory!\n");
}
region_alloc(e, (void *)ph->p_va, ph->p_memsz);
memset((void *)ph->p_va, 0, ph->p_memsz);
memcpy((void *)ph->p_va, binary+ph->p_offset, ph->p_filesz);//主要是binary+ph->p_offset
}
}
lcr3(PADDR(kern_pgdir));
// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
// LAB 3: Your code here.
region_alloc(e, (void *)(USTACKTOP - PGSIZE), PGSIZE);
}
3.5 env_create
使用Env_Alloc分配環境並呼叫Load_icode以將ELF二進位制載入到其中。
void
env_create(uint8_t *binary, enum EnvType type)
{
// LAB 3: Your code here.
struct Env * e;
if (env_alloc(&e, 0)) {
panic("env_create: env alloc failed!\n");
}
load_icode(e,binary);
e->env_type = type;
}
3.6 env_run
在使用者態執行一個給定的環境
if (curenv != e) {
if (curenv != NULL && curenv->env_status == ENV_RUNNING) {
curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
curenv->env_status = ENV_RUNNING;
curenv->env_runs++;
lcr3(PADDR(curenv->env_pgdir));
env_pop_tf(&curenv->env_tf);
}
env_pop_tf(&curenv->env_tf);
panic("env_run not yet implemented");
1.2 recall上面
首先我們跟隨實驗指導書的說明,來確認一下我們是否進入了user mode
-
在
env_pop_tf
處設定一個斷點,這個是你進入user mode之前的最後一個函式 -
逐步執行你會發現在執行完
iter
指令進入使用者態 -
使用者態的第一條指令就是
label start
inlib/entry.S
. -
然後在
obj/user/heelo.asm
的SYS_cputs所在的地方打上斷點(此int是系統呼叫,以向控制檯顯示字元。) -
這裡發現確實可以進入int 30這裡。感覺前面應該米問題。
1.3 Handling Interrupts and Exceptions
在剛才我們看見了使用者空間中的第一個INT $ 0x30系統呼叫指令是一個死衚衕:一旦處理器進入使用者模式,就無法退出。現在需要實現基本的異常和系統呼叫處理,以便核心可以從使用者模式程式碼中恢復處理器的控制。 您應該做的第一件事就是徹底熟悉X86中斷和異常機制。
Read Chapter 9, Exceptions and Interrupts in the 80386 Programmer's Manual (or Chapter 5 of the IA-32 Developer's Manual), if you haven't already.
1. Basics of Protected Control Transfer
異常和中斷都是“受保護的控制傳輸”,它導致處理器從使用者切換到核心模式(CPL = 0)。 在英特爾的術語中,中斷是一種受保護的控制傳輸,其由通常在處理器外部的非同步事件引起的,例如外部裝置I / O導致的終端。 而異常是由當前執行的程式碼引起的受保護控制傳輸,例如由於除零或無效的記憶體訪問。
2. Types of exceptiopns and interrupts
x86處理器可以在內部使用0到31之間的中斷向量,因此對映到IDT條目0-31。 例如,頁面故障始終通過向量14引起異常。大於31的中斷向量僅被軟體中斷使用,該軟體中斷可以由INT指令或非同步硬體中斷,或者在需要注意時由外部裝置引起的。
在本節中,我們將擴充套件JOS在第0-31頁中處理內部生成的X86異常。 在下一部分中,我們將使JOS處理軟體中斷向量48(0x30),JOS(相當任意)用作其系統呼叫中斷向量。 在Lab 4中,我們將擴充套件JOS以處理外部生成的硬體中斷,例如時鐘中斷。
An Example
+--------------------+ KSTACKTOP
| 0x00000 | old SS | " - 4
| old ESP | " - 8
| old EFLAGS | " - 12
| 0x00000 | old CS | " - 16
| old EIP | " - 20 <---- ESP
+--------------------+
- 處理器切換到由TSS中的SS0(包含GD_KD)與ESP0(包含KSTACKTOP)指向的stack。
- 處理器將old ss、old ESP、異常資料EFLAGS等推入堆疊
- 除零異常的中斷向量是0,所以處理器讀取IDT條目0並設定'CS:EIP'指向條目0描述的the handler function(處理函式)。
- 處理函式控制和處理這個exception,如結束這個使用者環境
對於某些x86異常,除零這種five words "standard",處理器還會把"error code"推入堆疊,如The page fault exception, number 14
+--------------------+ KSTACKTOP
| 0x00000 | old SS | " - 4
| old ESP | " - 8
| old EFLAGS | " - 12
| 0x00000 | old CS | " - 16
| old EIP | " - 20
| error code | " - 24 <---- ESP
+--------------------+
3. Nested Exceptions and interrupts
處理器可以從核心和使用者模式採用異常和中斷。 但是,只有當從使用者模式進入核心模式時,X86處理器儲存當前暫存器狀態之前。會自動切換堆疊並通過IDT呼叫相應的異常處理程式。 如果發生中斷或異常的處理器已處於核心模式(CS暫存器的低2位已經為零),則CPU只需在同一核心堆疊上推動更多值。 通過這種方式,核心可以優雅地處理由核心本身內的程式碼引起的巢狀異常。 此功能是實現保護的重要工具,因為我們將在系統呼叫的部分中看到。
如果處理器已經處於核心模式並呈現巢狀異常,因為它不需要切換堆疊,它不會儲存舊的SS或ESP暫存器。 也就不用push error code,因此核心堆疊如此看起來像是進入異常處理程式的以下內容:
+--------------------+ <---- old ESP
| old EFLAGS | " - 4
| 0x00000 | old CS | " - 8
| old EIP | " - 12
+--------------------+
對於需要push error code 的異常,處理器如前所述在舊EIP之後立即push error code
4. Setting Up the IDT
以便在JOS中設定IDT和處理異常。 目前,您將設定IDT來處理中斷向量0-31(處理器異常)。 我們將在此實驗室後面處理系統呼叫中斷,並在後面的實驗室中新增中斷32-47(裝置IRQ)。
在檔案Inc / Trap.h和kern / trap.h包含與您需要熟悉的中斷和異常相關的重要定義。
注意:0-31範圍內的一些例外由英特爾定義為保留。 由於它們永遠不會被處理器生成,因此您如何處理它們並不重要。
您應該實現的整體控制流程如下所示:
IDT trapentry.S trap.c
+----------------+
| &handler1 |---------> handler1: trap (struct Trapframe *tf)
| | // do stuff {
| | call trap // handle the exception/interrupt
| | // ... }
+----------------+
| &handler2 |--------> handler2:
| | // do stuff
| | call trap
| | // ...
+----------------+
.
.
.
+----------------+
| &handlerX |--------> handlerX:
| | // do stuff
| | call trap
| | // ...
1.4 Exercise4
好了又到了寫程式碼的地方了。
這裡要求我們在trap.c
和trapentry.S
實現IDT表的初始化,由於執行中斷處理程式要從使用者模式切換到核心模式,因此在使用者模式中當前程式的資訊必須要以trapframe
的結構儲存在棧上,當中斷處理程式執行完畢後則進行返回。
1. 在trap.c中初始化IDT表
這裡初始化IDT要用到SETGATE
這個巨集定義,下面先看一下這個巨集定義的功能
#define SETGATE(gate, istrap, sel, off, dpl) \
{ \
(gate).gd_off_15_0 = (uint32_t) (off) & 0xffff; \
(gate).gd_sel = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t) (off) >> 16; \
}
下面是函式引數的說明
Sel : 表示對於中斷處理程式程式碼所在段的段選擇子
off:表示中斷處理程式程式碼的段內偏移
(gate).gd_off_15_0 : 儲存偏移值的低16位
(gate).gd_off_31_16 : 儲存偏移值的高16位
(gate).gd_sel : 儲存段選擇子
(gate).gd_dpl : dpl 表示該段對應的
熟悉了這些之後參考intel的開發手冊找一下istrap的值,這裡注意系統呼叫的dpl = 3不然我們無法從使用者模式進去
這裡只要按照上述巨集定義的格式書寫就好,而且這裡的中斷處理函式我們都不用關心怎麼實現,只用給他一個佔位符。
SETGATE(idt[T_DIVIDE],0,GD_KT,divide_handler,0);
SETGATE(idt[T_DEBUG],0,GD_KT,debug_handler,0);
SETGATE(idt[T_NMI],0, GD_KT,nmi_handler,0);
SETGATE(idt[T_BRKPT],0,GD_KT,brkpt_handler,3);
SETGATE(idt[T_OFLOW],0,GD_KT,overflow_handler,0);
SETGATE(idt[T_BOUND],0,GD_KT,bounds_handler,0);
SETGATE(idt[T_ILLOP],0,GD_KT,illegalop_handler,0);
SETGATE(idt[T_DEVICE],0,GD_KT,device_handler,0);
SETGATE(idt[T_DBLFLT],0,GD_KT,double_handler,0);
SETGATE(idt[T_TSS],0,GD_KT,taskswitch_handler,0);
SETGATE(idt[T_SEGNP],0,GD_KT,segment_handler,0);
SETGATE(idt[T_STACK],0,GD_KT,stack_handler,0);
SETGATE(idt[T_GPFLT],0,GD_KT,protection_handler,0);
SETGATE(idt[T_PGFLT],0,GD_KT,page_handler,0);
SETGATE(idt[T_FPERR],0,GD_KT,floating_handler,0);
SETGATE(idt[T_ALIGN],0,GD_KT,aligment_handler,0);
SETGATE(idt[T_MCHK],0,GD_KT,machine_handler,0);
SETGATE(idt[T_SIMDERR],0,GD_KT,simd_handler,0);
SETGATE(idt[T_SYSCALL],1,GD_KT,syscall_handler,3);
SETGATE(idt[T_DEFAULT],0,GD_KT,default_handler,0);
2. 在trapentry.S中實現對於不同trap的entry point
實驗指導書中提示我們使用TRAPHANDLER
and TRAPHANDLER_NOEC
這兩個巨集定義。它們的作用都是把傳入的trap number入棧然後跳轉到我們後面要實現的__alltraps
中。唯一的區別是前者cpu會自動把error code入棧。而對於後者則要手動入棧一個0當作錯誤碼.
因此這裡按照上面巨集定義的要求初始化所有trap和對應的entry point
TRAPHANDLER_NOEC(divide_handler, T_DIVIDE);
TRAPHANDLER_NOEC(debug_handler, T_DEBUG);
TRAPHANDLER_NOEC(nmi_handler, T_NMI);
TRAPHANDLER_NOEC(brkpt_handler, T_BRKPT);
TRAPHANDLER_NOEC(overflow_handler, T_OFLOW);
TRAPHANDLER_NOEC(bounds_handler, T_BOUND);
TRAPHANDLER_NOEC(illegalop_handler, T_ILLOP);
TRAPHANDLER_NOEC(device_handler, T_DEVICE);
TRAPHANDLER(double_handler, T_DBLFLT);
TRAPHANDLER(taskswitch_handler, T_TSS);
TRAPHANDLER(segment_handler, T_SEGNP);
TRAPHANDLER(stack_handler, T_STACK);
TRAPHANDLER(protection_handler, T_GPFLT);
TRAPHANDLER(page_handler, T_PGFLT);
TRAPHANDLER_NOEC(floating_handler, T_FPERR);
TRAPHANDLER_NOEC(aligment_handler, T_ALIGN);
TRAPHANDLER_NOEC(machine_handler, T_MCHK);
TRAPHANDLER_NOEC(simd_handler, T_SIMDERR);
TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL);
TRAPHANDLER_NOEC(default_handler, T_DEFAULT);
3. 在trapentry.S中實現alltraps
這裡說的是要push values to make the stack look like a struct Trapframe。這個意思就是棧內的資料排列要和Trapframe是一樣的。因為這樣當回覆環境的時候,才能以正確的順序把棧內的值恢復到Trapframe中。
.global _alltraps_alltraps:// make the stack look like a struct Trapframe pushl %ds; pushl %es; pushal;// load GD_KD into %ds and %es movl $GD_KD, %edx movl %edx, %ds movl %edx, %es// push %esp as an argument to trap() pushl %esp; call trap;
1.5 partA整理
這裡是時候停下來,來看目前為止我們做了什麼。
-
首先計算機的開始是從BIOS開始,BIOS會做一些關於硬體的檢查,以及初始化之後。它搜尋可引導裝置,如軟盤,硬碟驅動器或CD-ROM。 最終,當它找到可啟動磁碟時,BIOS將引導載入程式從磁碟讀取。隨後轉移到引導啟動程式上去。
-
而主載入程式所在地址就是0x7c00也就是
boot/boot.S
-
主載入程式會把處理器從真實模式轉換為32bit的保護模式,因為只有在這種模式下軟體可以訪問超過1MB空間的內容。
-
隨後主載入程式會load核心。會把核心load到0x10000處
-
隨後到核心執行,核心呼叫
i386_init
隨即轉移到c語言中 -
在
i386_init
中我們要呼叫各種初始化。有lab1實現的cons_init和lab2實現的mem_init -
以及partA實現的env_init()、和剛才實現的trap_init。
-
隨後我們要呼叫
env_run
不過在呼叫env_run
之前要先ENV_CREATE(user_hello, ENV_TYPE_USER);
-
ENV_CREATE
根據提供的二進位制elf檔案建立一個env。 -
隨後呼叫
env_run
執行我們剛才建立的env(這個時候我們只有一個env) -
這個時候我們進入
env_run
繼續跟蹤。在呼叫env_pop_tf之前我們輸出當前的env_tf
-
進入
env_pop_tf
之後我們把當前的env_tf存取trapframe中.然後執行iret
指令進入使用者態 -
使用者態的第一條指令就是
label start
inlib/entry.S
.首先會比較一下esp暫存器的值是否小於使用者棧。因為這表示我們已經處於使用者態。
-
隨後呼叫libmain然後進入libmain.c。在此呼叫umain(argc, argv);進入user routine。如果是shell的話就會進入shell
-
這裡我們測試用的是一個hello.c在裡面我們會cprinf很多東西,而cprinf會陷入系統呼叫。
-
這裡我們直接在
obj/user/hello.asm
去找一下系統呼叫的地址吧。。一行一行執行好慢。。。。 -
這裡通過系統呼叫我們就會陷入
可以發現這裡就是我們剛才設定的對於syscall的處理。
這裡是如何準備準確的找到trapentry.S中對應的條目,是通過我們在前面
trap_init
設定好的IDT表來找到對應的entry圖片來自於一位大佬的簡書
所以通過IDT和我們設定的段選擇子(其實這裡就是核心的程式碼段)以及偏移就可以找到對應的中斷處理程式。
因此這裡我們進入
TRAPHANDLER_NOEC
的巨集定義。因為syscall是沒有error number所以我們進入這個巨集定義- 進入之後把trap number入棧隨即呼叫trap這個函式
- 對於trap的實現後面的lab涉及到了之後在進行整理
1.6 partA的一些疑惑
1. 對trap_init_percpu的分析
我們在trap_init
中設定了對不同中斷/陷阱對應的在IDT中的一些資訊。隨即我們就呼叫了trap_init_percpu
。
下面來詳細解釋一下這個函式
void
trap_init_percpu(void)
{
// Setup a TSS so that we get the right stack
// when we trap to the kernpel.
// 這裡設定
ts.ts_esp0 = KSTACKTOP;
ts.ts_ss0 = GD_KD;
ts.ts_iomb = sizeof(struct Taskstate);
上面的程式碼是用來設定任務狀態段的一些資訊。因為任務狀態段TSS是核心用來任務管理的,所以它的棧幀指標esp是指向核心棧的。它的資料段指向核心的資料段。
// Initialize the TSS slot of the gdt.
gdt[GD_TSS0 >> 3] = SEG16(STS_T32A, (uint32_t) (&ts),
sizeof(struct Taskstate) - 1, 0);
gdt[GD_TSS0 >> 3].sd_s = 0;
這裡剛開始看的時候真的非常疑惑,這裡我們要秉持一個概念,就是gdt的下標是段選擇子。那我們看一下段選擇子的結構
因此這裡我們把任務狀態段存入gdt中的時候他對應的索引就是前13位,因此這裡我們要GD_TSS0 >> 3
來表示對應的索引
至於SEG16則是按照gdt段描述符佔的格式來設定這一段。具體格式我們上面已經提到了。同樣這裡我們要把這一段的sd_s位設定為0
來表示system,因為這個是核心用來任務管理的
// Load the TSS selector (like other segment selectors, the // bottom three bits are special; we leave them 0) ltr(GD_TSS0); // Load the IDT lidt(&idt_pd); }
這裡是兩個load操作分別load 段選擇子和IDT表,IDT表為我們接下來的操作做準備
2. lgdt和lidt是如何工作的
下圖為GDTR和IDTR的結構
-
先看lgdt
就以
env_init_cpu
這個中的lgdt
為例env_init_percpu(void){ lgdt(&gdt_pd);
這裡的
lgdt
會呼叫下面的內聯彙編static inline voidlgdt(void *p){ asm volatile("lgdt (%0)" : : "r" (p));}
這裡是呼叫LGDT這條彙編指令,將p所指向的值載入到GDTR。那這裡傳過去的p就是執向gdt_pd的指標
因此上面的執行實際上等價於把gdt_pd載入到GDTR。
下面看一下gdt_pd分別表示了什麼
// Pseudo-descriptors used for LGDT, LLDT and LIDT instructions.struct Pseudodesc { uint16_t pd_lim; // Limit uint32_t pd_base; // Base address} __attribute__ ((packed));
struct Pseudodesc gdt_pd = { sizeof(gdt) - 1, (unsigned long) gdt};
emmm這裡其實就想當於定義了一個結構體儲存了gdt的大小和基地址。而這正好和gdtr相對應。
-
再看lidt
通過上面的操作其實可以速推這個操作,就是呼叫LIDT把對應的LDTR暫存器初始化
同樣還是利用了
ldt_pd
這樣的結構體,整體操作和上面完全一樣// Load the IDT lidt(&idt_pd);
struct Pseudodesc idt_pd = { sizeof(idt) - 1, (uint32_t) idt};
3. lcr3是如何工作的
我們在之前的lab中利用了lcr3來改變page_dir。那麼它到底是如何工作的
其實查了一下非常簡單。crx暫存器一家
cr3級存取是頁目錄基地暫存器,儲存頁目錄表的實體地址
可以發現lrc3的程式碼就是把val -> cr3暫存器。
static inline voidlcr3(uint32_t val){ asm volatile("movl %0,%%cr3" : : "r" (val));}
4. user hello的系統呼叫是如何處理的
前面我們分析了IDT表的構建,以及我們如何找到trap對應的條目
那麼我們分析一下整個系統呼叫的全過程
-
umain --> lib/cprintf --> vcprintf --> lib/systemcall/sys_cputs --> syscall
,systemcall
中使用 int 0x30 陷入核心態
這裡我們在0x800bcb
打一個斷點就會進入到系統呼叫 -
然後就是int指令的執行過程。。這裡我不知道怎麼debug追蹤就去網上查了一下
- 取中斷型別碼n;
2)標誌暫存器入棧(pushf),IF=0,TF=0(重置中斷標誌位);
3)CS、IP入棧;
4)查中斷向量表, (IP)=(n x 4),(CS)=(n x 4+2)。
-
所以整個的執行流程就如下圖
-
2. PartB Page Faults, Breakpoints Exceptions, and System Calls
2.1 Handing Page Faults
有了前面的鋪墊之後這個exercise就非常簡答了。就是要讓我們修改trap_dispatch
中針對頁故障執行已經提供好的page_falut_handler
。所以只需要2行
if (tf->tf_trapno == T_PGFLT) { page_fault_handler(tf); }
2.2 The Breakpoint Exception
斷點異常(具有中斷向量3)通常用於允許偵錯程式在程式程式碼中插入斷點,即在相關的程式碼位置暫時使用int $3
來代替原本應該執行的指令。在jos中我們將會大量使用這個異常來實現一個原始的偽系統呼叫,使得使用者環境可以使用它來呼叫jos核心監視器(如果我們將jos核心監視器視為原始偵錯程式,這種做法是適當的)。比如說lib/panic.c
中user mode下的panic()
函式,實際上就是在顯示了panic資訊之後使用了int $3
。
Exercise6 修改trap_dispatch()
來實現核心監視器中的斷點異常。
這個和上面一樣也是2行
if (tf->tf_trapno == T_BRKPT) { monitor(tf); }
2.3 Systemcalls
為核心增加系統呼叫處理函式。我們需要修改kern/trapentry.S
以及kern/trap.c
中的trap_init()
函式。我們還需要修改trap_dispatch()
,使其能夠以正確引數呼叫syscall()
(這個是kern/syscall.c
下的而非之前lib
中的)並將返回結果存放在%eax
中返回給使用者(呼叫者)。
我們還需要實現kern/syscall.c
下的syscall()
,使得呼叫號無效的時候返回-E_INVAL
。通過系統呼叫函式處理inc/syscall.h
中的所有系統呼叫。
實驗指導書中的提示
應用程式將會通過暫存器傳遞系統呼叫號以及相應的系統呼叫引數。這種方式下核心就不會訪問使用者環境棧或者指令流。系統呼叫號存放在
%eax
暫存器中,其餘引數(最多五個)相應地存放在%edx
,%ecx
,%ebx
,%edi
,%esi
中。核心將返回值存放在暫存器%eax
中。用於喚醒系統呼叫的彙編程式碼已經實現在lib/syscall.c
中的syscall()
。我們需要閱讀這個函式以確保理解了如何喚醒系統呼叫。
.4 User-mode startup
使用者程式在lib/entry.S
的頂部開始執行,經過一些操作之後,程式碼會呼叫lib/libmain.c
中的libmain()
。我們需要修改libmain()
以初始化指向當前環境struct Env
(在envs[]
陣列中)的全域性指標thisenv
(注意lib/entry.S
已經定義了我們在part A中指向UENVS
的對映envs
)。
提示:我們可以檢視
inc/env.h
以及使用sys_getenvid()
。
隨後libmain
呼叫umain
,對於hello
程式而言,列印出”hello world”之後,其試圖訪問thisenv->env_id
,這就是為什麼hello程式會出現fault(我們還沒有初始化thisenv
)。
exercise 8
實際上就是要求我們需要修改libmain()
函式使其初始化thisenv
,指向envs
中代表當前使用者環境的Env
結構體。
這裡我們去inc/env.h
可以看到下面這段話
The environment index ENVX(eid) equals the environment's index in the 'envs[]' array.
所以我們需要使用ENVX(eid)
總這個巨集定義來獲取當前使用者環境的Env結構體位於envs中的索引。同時sys_getenvid
就可以獲取到當前環境的eid了。。加上一行就可以過
thisenv = envs + ENVX(sys_getenvid());
2.5 Page faults and memory protection
真的看不懂一些英語。。。煩死了
系統呼叫為記憶體保護提出了一個有趣的問題。大多數系統呼叫介面允許使用者程式將指標傳遞給核心。這些指標指向要讀取或寫入的使用者緩衝區。然後核心在執行系統呼叫時取消引用這些指標。這有兩個問題:
- 核心的缺頁錯誤潛在地比使用者程式的缺頁錯誤更加嚴重。如果核心在操作其私有資料結構的時候發生了缺頁錯誤,那麼核心產生bug,錯誤處理程式應該panic核心。但是當核心解引用由使用者程式傳遞的指標時,需要某種方式來標記由解引用導致的缺頁實際上代表的是使用者程式引發的。
- 核心比使用者程式具有更多的地址許可權。在這種情況下使用者程式可能會傳遞一個指標,這個指標指向的地址只能由核心讀寫而不能通過使用者程式讀寫。在這種情況下核心不能對這個指標進行解引用(這樣做顯然會暴露核心的私有資訊)。
因此我們需要解決這兩個問題,通過檢查傳遞從使用者空間傳遞到核心的指標是否應該被解引用。
exercise 9
修改kern/trap.c
,使得核心在核心程式碼觸發缺頁錯誤的時候panic。
提示:為了確認引發異常的程式碼是使用者程式碼還是核心程式碼,可以檢查
tf_cs
的暫存器值的低位。
-
閱讀
kern/pmap.c
中的user_mem_assert
然後在相同的的檔案中實現user_mem_check
。intuser_mem_check(struct Env *env, const void *va, size_t len, int perm){ // LAB 3: Your code here. // 1. must below ULIM if ((uintptr_t) va >= ULIM) { user_mem_check_addr = (uintptr_t)va; return -E_FAULT; } size_t start = (size_t) ROUNDDOWN(va, PGSIZE); size_t end = (size_t) ROUNDUP(va + len, PGSIZE); while (start < end) { pte_t *pte = pgdir_walk(env->env_pgdir, (void *) start, 0); if (start >= ULIM || !pte || !(*pte & PTE_P) || ((*pte & perm) != perm)) { if(start <= (uintptr_t)va){ user_mem_check_addr = (uintptr_t)va; } else if(start >= (uintptr_t)va + len){ user_mem_check_addr = (uintptr_t)va + len; } else{ user_mem_check_addr = start; } return -E_FAULT; } start += PGSIZE; } return 0;}
-
修改
kern/syscall.c
來仔細檢查系統呼叫的引數。在syscall中增加這一assert函式
// Return any appropriate return value. // LAB 3: Your code here. switch (syscallno) { case SYS_cputs: user_mem_assert(curenv,(void *)a1, (size_t)a2, PTE_U); sys_cputs((const char *)a1, a2); return 0;
在
sys_cputs
中增加mem_checkstatic voidsys_cputs(const char *s, size_t len){ // Check that the user has permission to read memory [s, s+len). // Destroy the environment if not. // LAB 3: Your code here. user_mem_check(curenv,(void *)s, len,PTE_U); // Print the string supplied by the user. cprintf("%.*s", len, s);}
-
修改
kern/kedebug.c
中的debuginfo_eip
函式,使其在usd
,stabs
,stabstr
呼叫user_mem_check
。