lab3是實現(xiàn)用戶環(huán)境雪营,或者說是用戶進(jìn)程弓千。exercize代碼見 這里。
1 概述
繼內(nèi)存管理之后献起,實驗3是實現(xiàn)用戶環(huán)境洋访,這里的用戶環(huán)境,其實就類比Unix/Linux下的進(jìn)程即可谴餐。因為JOS的環(huán)境與Unix進(jìn)程提供了不同的接口和語義姻政,所以用環(huán)境一詞代替進(jìn)程,在本文中進(jìn)程和環(huán)境兩個詞就不做區(qū)分了岂嗓。
2 進(jìn)程定義
在 inc/env.h
中包含了一些用戶環(huán)境的基本定義汁展,JOS內(nèi)核使用 Env
結(jié)構(gòu)體來追蹤用戶進(jìn)程。其中 envs變量是指向所有進(jìn)程的鏈表的指針厌殉,其操作方式跟實驗2的pages類似食绿,env_free_list是空閑的進(jìn)程結(jié)構(gòu)鏈表。注意下公罕,在早起的JOS實驗中器紧,pages和envs都是用的雙向鏈表,現(xiàn)在的版本用的單向鏈表操作起來更加簡單和清晰楼眷。
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list
注意铲汪,現(xiàn)代操作系統(tǒng)中通常都可以多進(jìn)程并發(fā)執(zhí)行的,這取決于 PCB 表的大小罐柳。在 JOS 系 統(tǒng)中掌腰,evns 數(shù)組就等價于 PCB 表,其共有 1024(NENV)個表項张吉,即 JOS 系統(tǒng)并發(fā)度為 1024辅斟。 其相關(guān)宏在 inc/Env.h 中定義:
// +1+---------------21-----------------+--------10--------+
// |0| Uniqueifier | Environment |
// | | | Index |
// +------------------------------------+------------------+
// \--- ENVX(eid) --/
#define LOG2NENV 10
#define NENV (1 << LOG2NENV)
#define ENVX(envid) ((envid) & (NENV - 1))
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
};
進(jìn)程結(jié)構(gòu)體 Env 各字段定義如下:
- env_tf: 當(dāng)進(jìn)程停止運行時用于保存寄存器的值,比如當(dāng)發(fā)生中斷切換到內(nèi)核環(huán)境運行了或者切換到另一個進(jìn)程運行的時候需要保存當(dāng)前進(jìn)程的寄存器的值以便后續(xù)該進(jìn)程繼續(xù)執(zhí)行芦拿。
- env_link:指向空閑進(jìn)程鏈表 env_free_list 中的下一個 Env 結(jié)構(gòu)士飒。
- env_id: 進(jìn)程ID。因為進(jìn)程ID是正數(shù)蔗崎,所以符號位是0酵幕,而中間的21位是標(biāo)識符,標(biāo)識在不同的時間創(chuàng)建但是卻共享同一個進(jìn)程索引號的進(jìn)程缓苛,最后10位是進(jìn)程的索引號芳撒,要用envs索引進(jìn)程管理結(jié)構(gòu) Env 就要用
ENVX(env_id)
。 - env_parent_id: 進(jìn)程的父進(jìn)程ID未桥。
- env_type:進(jìn)程類型笔刹,通常是 ENV_TYPE_USER,后面實驗中可能會用到其他類型冬耿。
- env_status:進(jìn)程狀態(tài)舌菜,進(jìn)程可能處于下面幾種狀態(tài)
- ENV_FREE:標(biāo)識該進(jìn)程結(jié)構(gòu)處于不活躍狀態(tài),存在于 env_free_list 鏈表亦镶。
- ENV_RUNNABLE: 標(biāo)識該進(jìn)程處于等待運行的狀態(tài)日月。
- ENV_RUNNING: 標(biāo)識該進(jìn)程是當(dāng)前正在運行的進(jìn)程。
- ENV_NOT_RUNNABLE: 標(biāo)識該進(jìn)程是當(dāng)前運行的進(jìn)程缤骨,但是處于不活躍的狀態(tài)爱咬,比如在等待另一個進(jìn)程的IPC。
- ENV_DYING: 該狀態(tài)用于標(biāo)識僵尸進(jìn)程绊起。在實驗4才會用到這個狀態(tài)精拟,實驗3不用。
- env_pgdir:用于保存進(jìn)程頁目錄的虛擬地址虱歪。
3 進(jìn)程初始化及運行
進(jìn)程管理結(jié)構(gòu)envs對應(yīng)的1024個Env結(jié)構(gòu)體在物理內(nèi)存中緊接著pages存儲蜂绎。進(jìn)程初始化流程主要包括:
- 給NENV個Env結(jié)構(gòu)體在內(nèi)存中分配空間,并將 envs 結(jié)構(gòu)體的物理地址映射到 從 UENV 所指向的線性地址空間实蔽,該線性地址空間允許用戶訪問且只讀荡碾,所以頁面權(quán)限被標(biāo)記為PTE_U。
- 調(diào)用
env_init
函數(shù)初始化envs局装,將 NENV 個進(jìn)程管理結(jié)構(gòu)Env通過env_link串聯(lián)起來坛吁,注意,env_free_list要指向第一個 Env铐尚,所以這里要用倒序的方式拨脉。在env_init
函數(shù)中調(diào)用了env_init_percpu
函數(shù),加載新的全局描述符表宣增,設(shè)置內(nèi)核用到的寄存器 es, ds, ss的值為GD_KD玫膀,即內(nèi)核的段選擇子,DPL為0爹脾。然后通過ljmp指令asm volatile("ljmp %0,$1f\n 1:\n" : : "i" (GD_KT));
設(shè)置CS為 GD_KT帖旨。這句匯編用到了unnamed local labels
箕昭,含義就是跳轉(zhuǎn)到GD_KT, 1:
這個地址處,其中的$1f
的意思是指跳轉(zhuǎn)到后一個1:
標(biāo)簽處解阅,如果是前一個落竹,用$1b
,而這個后一個1:
標(biāo)簽就是語句后面货抄,所以最終效果只是設(shè)置了CS寄存器的值為GD_KT而已述召。 - 初始化好了envs和env_free_list后,接著調(diào)用
ENV_CREATE(user_hello, ENV_TYPE_USER)
創(chuàng)建用戶進(jìn)程蟹地。ENV_CREATE
是kern/env.h
中的宏定義积暖,展開就是調(diào)用的env_create
,只是參數(shù)設(shè)置成了env_create(_binary_obj_user_hello_start, ENV_TYPE_USER)
。env_create也是我們要實現(xiàn)的函數(shù)怪与,它的功能就是先調(diào)用env_alloc
函數(shù)分配好Env結(jié)構(gòu)夺刑,初始化Env的各個字段值(如env_id,env_type琼梆,env_status以及env_tf的用于存儲寄存器值的字段性誉,運行用戶程序時會將 env_tf 的字段值加載到對應(yīng)的寄存器中),為該用戶進(jìn)程分配頁目錄表并調(diào)用load_icode
函數(shù)加載程序代碼到內(nèi)存中茎杂。- env_alloc調(diào)用env_setup_vm函數(shù)分配好頁目錄的頁表错览,并設(shè)置頁目錄項和env_pgdir字段)。
-
load_icode
函數(shù)則是先設(shè)置cr3寄存器切換到該進(jìn)程的頁目錄env_pgdir煌往,然后通過region_alloc
分配每個程序段的內(nèi)存并按segment將代碼加載到對應(yīng)內(nèi)存中倾哺,加載完成后設(shè)置 env_tf->tf_eip為Elf的e_entry刽脖,即程序的初始執(zhí)行位置。
- 加載完程序代碼后,萬事俱備膳殷,調(diào)用
env_run(e)
函數(shù)開始運行程序茅姜。如果當(dāng)前有進(jìn)程正在運行击孩,則設(shè)置當(dāng)前進(jìn)程狀態(tài)為ENV_RUNNABLE
,并將需要運行的進(jìn)程e的狀態(tài)設(shè)置為ENV_RUNNING
撬腾,然后加載e的頁目錄表地址 env_pgdir 到cr3寄存器中,調(diào)用env_pop_tf(struct Trapframe *tf)
開始執(zhí)行程序e恢恼。 - env_pop_tf其實就是將棧指針esp指向該進(jìn)程的env_tf民傻,然后將 env_tf 中存儲的寄存器的值彈出到對應(yīng)寄存器中,最后通過 iret 指令彈出棧中的元素分別到 EIP, CS, EFLAGS 到對應(yīng)寄存器并跳轉(zhuǎn)到
CS:EIP
存儲的地址執(zhí)行(當(dāng)使用iret指令返回到一個不同特權(quán)級運行時牵署,還會彈出堆棧段選擇子及堆棧指針分別到SS與SP寄存器)喧半,這樣,相關(guān)寄存器都從內(nèi)核設(shè)置成了用戶程序?qū)?yīng)的值,EIP存儲的是程序入口地址爽柒。 - env_id的生成規(guī)則很有意思浩村,注意一下在env_free中并沒有重置env_id的值,這就是為了用來下一次使用這個env結(jié)構(gòu)體時生成一個新的env_id酿矢,區(qū)分之前用過的env_id怎燥,從generation的生成方式就能明白了。
用戶程序運行路徑如下所示:
start (kern/entry.S)
i386_init (kern/init.c)
cons_init
mem_init
env_init
trap_init (still incomplete at this point)
env_create
env_alloc
env_setup_vm
load_icode
region_alloc
env_run
env_pop_tf
關(guān)于Trapframe
Trapframe結(jié)構(gòu)體存儲的是當(dāng)前進(jìn)程的寄存器的值策肝,可以看到env_pop_tf
函數(shù)中便是將trapframe的起始地址賦值給esp之众,然后用的這個順序?qū)V性貜棾龅綄?yīng)寄存器中的依许。其中popal是彈出tf_regs到所有的通用寄存器中,接著彈出值到es膘婶,ds寄存器坦康,接著跳過trapno和errcode滞欠,調(diào)用iret分別將棧中存儲數(shù)據(jù)彈出到 EIP, CS, EFLAGS寄存器中古胆。
struct Trapframe {
struct PushRegs tf_regs;
uint16_t tf_es;
uint16_t tf_padding1;
uint16_t tf_ds;
uint16_t tf_padding2;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding3;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding4;
} __attribute__((packed));
//
// Restores the register values in the Trapframe with the 'iret' instruction.
// This exits the kernel and starts executing some environment's code.
//
// This function does not return.
//
void env_pop_tf(struct Trapframe *tf)
{
asm volatile(
"\tmovl %0,%%esp\n"
"\tpopal\n"
"\tpopl %%es\n"
"\tpopl %%ds\n"
"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
"\tiret\n"
: : "g" (tf) : "memory");
panic("iret failed"); /* mostly to placate the compiler */
}
關(guān)于CPL, RPL, DPL
CPL是當(dāng)前正在執(zhí)行的代碼所在的段的特權(quán)級逸绎,存在于CS寄存器的低兩位(對CS來說,選擇子的RPL=當(dāng)前段的CPL)巫糙。RPL指的是進(jìn)程對段訪問的請求權(quán)限参淹,是針對段選擇子而言的乏悄,不是固定的。DPL則是在段描述符中存儲的开呐,規(guī)定了段的訪問級別规求,是固定的阻肿。為什么需要RPL呢?因為同一時刻只能有一個CPL伤极,而低權(quán)限的用戶程序去調(diào)用內(nèi)核的功能來訪問一個目標(biāo)段時姨伤,進(jìn)入內(nèi)核代碼段時CPL 變成了內(nèi)核的CPL乍楚,如果沒有RPL,那么權(quán)限檢查的時候就會用CPL忿偷,而這個CPL 權(quán)限比用戶程序權(quán)限高臊泌,也就可能去訪問需要高權(quán)限才能訪問的數(shù)據(jù)渠概,導(dǎo)致安全問題嫂拴。所以引入RPL贮喧,讓它去代表訪問權(quán)限箱沦,因此在檢查CPL 的同時,也會檢查RPL灶伊。一般來說如果RPL 的數(shù)字比CPL大(權(quán)限比CPL的低)寒跳,那么RPL會起決定性作用冯袍,這個權(quán)限檢查是CPU硬件層面做的碾牌。
用戶程序代碼
實驗中用到的用戶程序代碼位于user目錄舶吗,如user_hello對應(yīng)的源文件是user/hello.c
,因為還沒有實現(xiàn)文件系統(tǒng)检激,所以這些用戶程序代碼通過一系列編譯命令后最終會編譯到內(nèi)核中腹侣。比如 user/hello.c
編譯到內(nèi)核中地址是 0xf011c356
傲隶。詳見 kern/Makefrag
,用命令make V=1
可以顯示完整的編譯命令复濒。
之前有個疑惑就是 user/hello.c
是怎么編譯到kernel里面后在obj/kern/kernel.sym
有了 _binary_obj_user_hello_start
乒省、_binary_obj_user_hello_end
以及_binary_obj_user_hello_size
這幾個符號的袖扛。這個其實是 ld
命令生成的。具體命令是下面這個晾嘶,ld -b binary
會自動在最終的可kernel文件中生成對應(yīng)開始結(jié)束符號垒迂,以_binary_
開頭,因為我們的用戶程序編譯后代碼目錄結(jié)構(gòu)是 obj/user/hello
楷拳,所以符號名就是將目錄換成了下劃線吏奸。
ld -o obj/kern/kernel -m elf_i386 -T kern/kernel.ld -nostdlib obj/kern/entry.o obj/kern/entrypgdir.o ... /usr/lib/gcc/x86_64-linux-gnu/4.8/32/libgcc.a
-b binary obj/user/hello ...
另外提下的是奋蔚,kern.sym 這個符號表文件里面存儲的是符號的地址。符號有類型坤按,大寫表示全局符號馒过,小寫則是局部符號腹忽。類型說明:
- A:符號是絕對值。比如表示代碼長度的符號
_binary_obj_user_hello_size
嘹锁。 - T: 代碼段符號兼耀。
- D:已初始化數(shù)據(jù)段符號求冷。
- B:未初始化數(shù)據(jù)段符號。
用戶代碼中的系統(tǒng)調(diào)用
在 Env初始化后匠题,運行編譯好的 user_hello
進(jìn)程會報錯韭山,因為 user_hello
里面調(diào)用了 cprintf
打印輸出冷溃,我們不能讓用戶程序來操作硬件設(shè)備似枕,因此cprintf最終要通過系統(tǒng)調(diào)用來實現(xiàn)年柠,此時系統(tǒng)調(diào)用功能還沒有實現(xiàn)冗恨。
注意 kern
和 lib
目錄下面都有 printf.c和syscall.c
文件,kern目錄下面的是內(nèi)核專用的虐拓。而用戶要cprintf輸出蓉驹,就要使用lib目錄下面的printf.c中的函數(shù)揪利,最后經(jīng)由lib/syscall.c
的sys_cputs()
,最終通過該文件中的syscall()
來實現(xiàn)輸出。
4 中斷和異常處理
4.1 中斷/異常概述
中斷和異常都是”保護(hù)控制轉(zhuǎn)移(PCT)”機(jī)制献汗,它們將處理器從用戶模式轉(zhuǎn)換到內(nèi)核模式罢吃。在英特爾的術(shù)語中昭齐,中斷是指處理器之外的異步事件導(dǎo)致的PCT阱驾,比如外部的IO設(shè)備活動。而異常則是當(dāng)前運行代碼同步觸發(fā)的PCT丧荐,如除0或者非法內(nèi)存訪問等喧枷。根據(jù)異常被報告的方式以及導(dǎo)致異常的指令是否能重新執(zhí)行,異常還可以細(xì)分為故障(Fault)渡冻,陷阱(Trap)和中止(Abort)忧便。JOS中斷在門描述符中的type為STS_IG32茬腿,異常的type為 STS_TG32。
- Fault是通澄沾。可以被糾正的異常悴品,糾正后可以繼續(xù)運行。出現(xiàn)Fault時,處理器會把機(jī)器狀態(tài)恢復(fù)到產(chǎn)生Fault指令之前的狀態(tài)岖妄,此時異常處理程序返回地址會指向產(chǎn)生Fault的指令寂祥,而不是后一條指令丸凭,產(chǎn)生Fault的指令在中斷處理程序返回后會重新執(zhí)行。如Page Fault铛碑。
- Trap處理程序返回后執(zhí)行的指令是引起陷阱指令的后一條指令亚茬。
- Abort則不允許異常指令繼續(xù)執(zhí)行浓恳。
中斷描述符表將每個中斷向量和一個中斷門描述符對應(yīng)起來,中斷門描述符里面存儲中斷或異常的處理程序的入口地址以及DPL。x86 允許256個中斷和異常入口噪奄,每個對應(yīng)一個唯一的整數(shù)值勤篮,稱為中斷向量色罚。中斷描述符表的起始地址存儲在IDT寄存器中戳护,當(dāng)發(fā)生中斷/異常時,CPU使用中斷向量作為中斷描述符表的索引铺董,通過中斷門描述符中存儲的段選擇子和偏移量精续,可以到GDT中找到中斷處理程序的地址驻右。
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
| | // ...
+----------------+
x86 使用0-31號中斷向量作為處理器內(nèi)部的同步的異常類型堪夭,比如除零和缺頁異常拣凹。而32號之上的中斷向量用于軟件中斷(如int指令產(chǎn)生的軟件中斷)或者外部設(shè)備產(chǎn)生的異步的硬件中斷嚣镜。lab 3我們會用到0-31號以及48號(用于系統(tǒng)調(diào)用)中斷向量,在后面實驗中還會處理外部的時鐘中斷付呕,JOS 用到的中斷如下:
#define T_DIVIDE 0 // divide error
#define T_DEBUG 1 // debug exception
#define T_NMI 2 // non-maskable interrupt
#define T_BRKPT 3 // breakpoint
#define T_OFLOW 4 // overflow
#define T_BOUND 5 // bounds check
#define T_ILLOP 6 // illegal opcode
#define T_DEVICE 7 // device not available
#define T_DBLFLT 8 // double fault
/* #define T_COPROC 9 */ // reserved (not generated by recent processors)
#define T_TSS 10 // invalid task switch segment
#define T_SEGNP 11 // segment not present
#define T_STACK 12 // stack exception
#define T_GPFLT 13 // general protection fault
#define T_PGFLT 14 // page fault
/* #define T_RES 15 */ // reserved
#define T_FPERR 16 // floating point error
#define T_ALIGN 17 // aligment check
#define T_MCHK 18 // machine check
#define T_SIMDERR 19 // SIMD floating point error
// These are arbitrarily chosen, but with care not to overlap
// processor defined exceptions or interrupt vectors.
#define T_SYSCALL 48 // system call
4.2 中斷/異常處理流程
在用戶程序內(nèi)發(fā)生中斷/異常時徽职,CPU會自動將控制器轉(zhuǎn)移到中斷處理程序處姆钉。前面提到潮瓶,中斷門描述符存儲了中斷處理程序的信息,包括其所在的段選擇子埂伦、代碼地址等赤屋。CPU通過IDT寄存器找到中斷描述符表的起始地址壁袄,然后通過中斷向量(即中斷號)找到對應(yīng)的中斷門描述符嗜逻,接著通過中斷門描述符中存儲的段選擇子到GDT中找到段基址,加上偏移地址即可得到中斷處理程序的地址逆日。具體流程如下圖所示:
在跳轉(zhuǎn)到中斷處理程序執(zhí)行之前室抽,處理器需要一個地方保存處理器出現(xiàn)中斷/異常前的狀態(tài)坪圾,如調(diào)用異常處理程序之前的EIP 和 CS的值惑朦,這樣處理完中斷/異常后可以從出現(xiàn)中斷/異常前的位置繼續(xù)執(zhí)行漾月。需要注意的是,這塊區(qū)域不能被用戶模式的代碼訪問到蜓陌』つ危基于這個考慮,當(dāng)x86遇到異常/中斷導(dǎo)致特權(quán)級從用戶模式轉(zhuǎn)移到內(nèi)核模式時痴奏,它會將堆棧切換到內(nèi)核棧读拆。TSS就是存儲這個堆棧位置的結(jié)構(gòu)鸵闪,包括堆棧的段選擇子和地址等蚌讼。發(fā)生特權(quán)級別切換時,切換到內(nèi)核棧后芥喇,處理器會在內(nèi)核棧中壓入 SS, ESP, EFLAGS, CS, EIP继控。然后它從中斷門描述符將對應(yīng)的值到加載到寄存器器CS, EIP中胖眷,并將 ESP和SS設(shè)置為指向新的堆棧珊搀。盡管TSS有很多字段境析,但是在JOS中只用到了ESP0和SS0來存儲內(nèi)核棧的地址,其他字段都沒有使用眶拉。其中TSS的段選擇子通過ltr
指令加載到TR寄存器中,TR寄存器是個段寄存器谒臼,內(nèi)容為段選擇子的值,注意段寄存器存的是段選擇子在全局描述符的偏移值拾氓,并不是索引值咙鞍。
ts.ts_esp0 = KSTACKTOP;
ts.ts_ss0 = GD_KD;
中斷門描述符在 trap_init()
中初始化趾徽,通過 SETGATE
定義孵奶。大部分的中斷門描述符的DPL為0了袁,少量的需要允許用戶模式調(diào)用的設(shè)置為3载绿,如系統(tǒng)調(diào)用SYSCALL和斷點BRKPT.
SETGATE(idt[T_DIVIDE], 0, GD_KT, &handler1, 0);
...
SETGATE(idt[T_SYSCALL], 0, GD_KT, &handler48, 3);
注意,異常/中斷處理時切換堆棧到內(nèi)核棧是處理器執(zhí)行的臀脏,在內(nèi)核棧壓入SS, ESP, EFLAGS, CS, EIP等寄存器的值也是處理器做的揉稚。我們要做的是將TSS的ESP0和SS0設(shè)置為內(nèi)核棧地址搀玖,然后將錯誤碼和異常代號trapno壓入內(nèi)核棧驻呐,接著將ds含末,es佣盒,通用寄存器等寄存器的值壓入內(nèi)核棧中,切換ds和es寄存器的值到內(nèi)核數(shù)據(jù)段GD_KD(_alltraps中處理)盯仪,這樣棧中的數(shù)據(jù)滿足了Trapframe結(jié)構(gòu)全景,后面調(diào)用trap()函數(shù)統(tǒng)一處理。trap()函數(shù)最終通過trap_dispatch()
函數(shù)根據(jù)中斷向量來分發(fā)中斷/異常處理滞伟,在lab 3中我們只處理了 T_PGFLT诗良,T_BRKPT鉴裹,T_SYSCALL 這三個中斷向量钥弯,其他的則直接銷毀env并進(jìn)入monitor()脆霎。
4.3 中斷/異常示例
用戶模式下發(fā)生除零中斷時睛蛛,處理器會先切換到 TSS 中存儲的esp0和ss0對應(yīng)的內(nèi)核棧鹦马,并在內(nèi)核棧壓入必要的信息,如下所示:
+--------------------+ KSTACKTOP
| 0x00000 | old SS | " - 4
| old ESP | " - 8
| old EFLAGS | " - 12
| 0x00000 | old CS | " - 16
| old EIP | " - 20 <---- ESP
+--------------------+
然后處理器讀取 IDT 中的第0項并設(shè)置 CS 和 EIP指向第0項中斷處理程序的地址忆肾。而對于缺頁異常荸频,還會壓入一個error code,這一步不是處理器做的工作客冈,而是中斷處理程序做的旭从。
處理器可以處理用戶模式或者內(nèi)核模式下的異常/中斷。如果是內(nèi)核模式下發(fā)生了異常/中斷场仲,則因為不需要切換堆棧和悦,只需要內(nèi)核棧壓入 EFLAGS, CS, EIP的值即可渠缕,不用壓入SS和ESP的值鸽素。通過這種機(jī)制,處理器可以優(yōu)雅的處理內(nèi)核代碼出現(xiàn)的嵌套的異常/中斷亦鳞。
+--------------------+ <---- old ESP
| old EFLAGS | " - 4
| 0x00000 | old CS | " - 8
| old EIP | " - 12
+--------------------+
4.4 系統(tǒng)調(diào)用
在 JOS 中馍忽,使用 int $0x30
指令引起處理器中斷完成系統(tǒng)調(diào)用澜汤。用戶進(jìn)程通過系統(tǒng)調(diào)用讓內(nèi)核為其完成一些功能,如打印輸出cprintf舵匾,當(dāng)內(nèi)核執(zhí)行完系統(tǒng)調(diào)用后,返回用戶進(jìn)程繼續(xù)執(zhí)行谁不。
注意系統(tǒng)調(diào)用的中斷門描述符的DPL必須設(shè)置為3坐梯,允許用戶調(diào)用。如前面提過刹帕,在int n這類軟中斷調(diào)用時會檢查 CPL 和 DPL吵血,只有當(dāng)前的 CPL 比要調(diào)用的中斷的 DPL值小或者相等才可以調(diào)用,否則就會產(chǎn)生General Protection
偷溺。用戶程序通過 lib/syscall.c
觸發(fā)系統(tǒng)調(diào)用蹋辅,最終由kern/trap.c
中的trap_dispatch()統(tǒng)一分發(fā),并調(diào)用kern/syscall.c
中的syscall()處理挫掏。其參數(shù)必須設(shè)置到寄存器中侦另,其中系統(tǒng)調(diào)用號存儲在%eax,其他參數(shù)依次存放到 %edx, %ecx, %ebx, %edi, 和%esi尉共,返回值通過 %eax 來傳遞褒傅。
asm volatile("int %1\n"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a1),
"c" (a2),
"b" (a3),
"D" (a4),
"S" (a5)
: "cc", "memory");
注意,在kern/trap.c 中對syscall()的返回值要保存在Trapframe的tf_regs.reg_eax字段中袄友,這樣在返回用戶程序執(zhí)行時殿托, env_pop_tf將reg_eax值彈出到 %eax寄存器中,從而實現(xiàn)了返回值傳遞剧蚣。
5 用戶模式開啟
用戶程序的入口在 lib/entry.S
支竹,在其中設(shè)置了 envs,pages鸠按,uvpt等全局變量以及_start符號礼搁。_start是整個程序的入口,鏈接器在鏈接時會查找目標(biāo)文件中的_start符號代表的地址目尖,把它設(shè)置為整個程序的入口地址叹坦,所以每個匯編程序都要提供一個_start符號并且用.globl聲明。entry.S中會判斷 USTACKTOP 和 寄存器esp的值是否相等卑雁,若相等募书,則表示沒有參數(shù),則會默認(rèn)在用戶棧中壓入兩個0测蹲,然后調(diào)用libmain函數(shù)莹捡。當(dāng)然lab 3中的用戶程序代碼都沒有傳參數(shù)的。
而libmain()則需要設(shè)置 thisenv 變量(因為測試的用戶程序里面會引用thisenv的一些字段)扣甲,然后調(diào)用umain函數(shù)篮赢,而umain函數(shù)就是我們在 user/hello.c
這些文件中定義的主函數(shù)齿椅。最后,執(zhí)行完umain启泣,會調(diào)用 exit退出涣脚。exit就是調(diào)用了系統(tǒng)調(diào)用 sys_env_destroy,最終內(nèi)核通過 env_destroy()
銷毀用戶進(jìn)程并回到monitor()寥茫。
內(nèi)存保護(hù)可以確保用戶進(jìn)程中的bug不能破壞其他進(jìn)程或者內(nèi)核遣蚀。當(dāng)用戶進(jìn)程試圖訪問一個無效的或者沒有權(quán)限的地址時,處理器就會中斷進(jìn)程并陷入到內(nèi)核纱耻,若錯誤可修復(fù)芭梯,則內(nèi)核就修復(fù)它并讓用戶進(jìn)程繼續(xù)執(zhí)行;如果無法修復(fù)弄喘,那么用戶進(jìn)程就不能繼續(xù)執(zhí)行玖喘。許多系統(tǒng)調(diào)用接口運行把指針傳給 kernel,這些指針指向用戶buffer蘑志,為防止惡意用戶程序破壞內(nèi)核累奈,內(nèi)核需要對用戶傳遞的指針進(jìn)行權(quán)限檢查。內(nèi)存保護(hù)由 user_mem_check()和 user_mem_assert()實現(xiàn)急但。檢查用戶進(jìn)程訪存權(quán)限费尽,并檢查是否越界。