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 = (struct Elf *) binary;
struct Proghdr *ph; //Program Header
int ph_num; //Program entry number
if (Elf->e_magic != ELF_MAGIC) {
panic("binary is not ELF format\n");
}
ph = (struct Proghdr *) (binary+ Elf->e_phoff);
ph_num = Elf->e_phnum;
lcr3(PADDR(e->env_pgdir));
for (int i = 0; i < ph_num; i++) {
if (ph[i].p_type == ELF_PROG_LOAD) {
region_alloc(e, (void *)ph[i].p_va, ph[i].p_memsz);
memset((void *)ph[i].p_va, 0, ph[i].p_memsz); // 初始化
memcpy((void *)ph[i].p_va, binary + ph[i].p_offset, ph[i].p_filesz);
}
}
lcr3(PADDR(kern_pgdir));
e->env_tf.tf_eip = Elf->e_entry;
// 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],0,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;
} s
tart += 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); r
eturn 0;
在sys_cputs
中增加mem_check
static 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
。