Part A:使用者環境和異常處理
注:根據MIT-JOS的lab指導手冊,以下不明确區分“環境”和“程序”
使用者環境建立
本節中我們将實作一些核心的基本工具來支援受保護的使用者程序的運作。我們将增加JOS核心的功能,為它增加一些資料結構來追蹤使用者程序的一些資訊;建立一個單一使用者的環境,并在其中加載運作一個程式。我們也會使JOS核心處理使用者程序做出的任何系統調用和它導緻的任何異常
核心利用
ENV
資料結構來記錄每一個環境的資訊。目前我們隻建立單一的使用者環境,以後再在此基礎上設計多使用者環境
在
kern/env.c
中,核心維護以下三個關于環境的全局變量:
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list
一旦JOS系統開始運作,
envs
指針指向一個儲存目前系統中所有環境變量的
Env
結構體數組
- JOS核心最多同時支援
個活躍的環境,并為每一個可能的環境(共NENV
個)申請一個NENV
資料結構,存放在Env
數組中envs
- JOS核心将所有未使用的
結構體放在Env
連結清單中,以便使用者環境的配置設定和回收env_free_list
-
指針指向JOS核心中任意時刻正在執行的環境。當核心啟動、還沒有任何使用者環境運作時,它被初始化為curenv
NULL
Env
資料結構設計如下:
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
};
env_tf
:當核心或其他環境執行時,儲存目前未執行的環境的寄存器變量。例如當核心從使用者态切換到核心态運作時,使用者态的重要寄存器将被儲存,以便在回到使用者态運作時恢複它們
env_link
:指向
env_free_list
中該環境的下一個未使用的環境。隻有在這個結構體也未使用時(也在
env_free_list
中時)這個域才有用
env_id
:辨別目前使用此
Env
結構體的環境的唯一值。當一個環境終止、核心将這個環境的
Env
重新配置設定給其他環境後,它們的
env_id
也不相同
env_parent_id
:存放建立此環境的環境的
env_id
env_type
:用于區分一些特殊環境。對于大多數環境,它的值都是
ENV_TYPE_USER
env_status
:該域取值為以下之一:
-
:該ENV_FREE
結構體未被使用,應該被放在Env
裡env_free_list
-
:該ENV_RUNNABLE
結構體對應的環境就緒,等待被配置設定到處理器Env
-
:該ENV_RUNNING
結構體對應的環境正在運作Env
-
:該ENV_NOT_RUNNABLE
結構體對應的環境處于活躍狀态,但此時無法運作:例如他正在等待來自另一個環境的消息Env
-
:該ENV_DYING
結構體對應的環境是一個僵屍環境,它将在系統下一次進入核心态時被回收Env
eng_pgdir
:儲存該
Env
結構體對應的環境的頁表目錄的虛拟位址
與Unix程序相類似,一個JOS環境結合了線程(thread)和位址空間(address space)的概念:線程主要通過儲存寄存器的值(
env_tf
)來定義,位址空間主要通過儲存頁表目錄和頁表(
eng_pgdir
)來定義。要運作一個環境,核心必須為它設定合适的寄存器的值和合适的位址空間
在JOS系統中,環境并沒有在核心中擁有各自獨立的棧。。因為任意時刻隻能有一個環境處于活躍狀态,是以JOS核心隻需要一個核心棧。
配置設定環境數組
在
mem_init()
補充對
envs
數組的記憶體配置設定。這個過程與配置設定頁面數組
pages
是一樣的
envs = (struct Env *) boot_alloc(NENV*sizeof(struct Env));
memset(envs, 0, NENV*sizeof(struct Env));
然後需要建立
envs
的映射關系:虛拟位址為
UENVS
,權限為使用者可讀
建立和運作環境
接下來要編寫運作一個使用者環境的必要代碼。由于現在還沒有檔案系統,是以JOS将一些使用者程式的靜态二進制檔案作為ELF檔案嵌入在核心中,以便被載入和執行
Lab3的
GNUmakefile
在
obj/user/
目錄下生成一系列二進制檔案,它們通過
-b binary
指令作為原始二進制檔案被連結到核心中
在讀取和運作這些二進制檔案前,我們首先完成使用者環境的初始化
env_init()
函數功能:初始化所有
envs
數組中的
Env
結構體并把它們加入到
env_free_list
連結清單中
注意事項:
env_free_list
中結構體的順序應與
envs
數組相同,即第一次調用
env_alloc()
應該傳回
envs[0]
代碼實作如下:
void
env_init(void)
{
// Set up envs array
// LAB 3: Your code here.
int i;
for (i=NENV-1; i>=0; i--) {
envs[i].env_status = ENV_FREE;
envs[i].env_link = env_free_list;
env_free_list = &envs[i];
}
// Per-CPU part of the initialization
env_init_percpu();
}
函數最後調用
env_init_percpu()
配置段式記憶體管理系統,它所做的事包括
- 重新載入GDT表
- 初始化資料段寄存器GS、FS(留給使用者資料段使用)、ES、DS、SS(在使用者态和核心态切換使用)
- 初始化核心的代碼段寄存器CS
- 初始化LDT表為0
env_setup_vm()
函數功能:為新的環境配置設定頁表目錄,并在新環境的位址空間中初始化與核心相關的部分
相關部分是指:使用者和核心一樣理論上能通路完整的4G虛拟記憶體(這裡假設能通路到4G)。核心已建立UTOP以上的虛拟位址到實體位址的映射,是以核心拷貝一份這個映射到自己的頁表目錄中。對應的實體位址使用者究竟能不能通路靠頁表項中通過權限位控制
代碼實作如下:
static int
env_setup_vm(struct Env *e)
{
int i;
struct PageInfo *p = NULL;
// Allocate a page for the page directory
if (!(p = page_alloc(ALLOC_ZERO)))
return -E_NO_MEM;
// Now, set e->env_pgdir and initialize the page directory.
//
// Hint:
// - The VA space of all envs is identical above UTOP
// (except at UVPT, which we've set below).
// See inc/memlayout.h for permissions and layout.
// Can you use kern_pgdir as a template? Hint: Yes.
// (Make sure you got the permissions right in Lab 2.)
// - The initial VA below UTOP is empty.
// - You do not need to make any more calls to page_alloc.
// - Note: In general, pp_ref is not maintained for
// physical pages mapped only above UTOP, but env_pgdir
// is an exception -- you need to increment env_pgdir's
// pp_ref for env_free to work correctly.
// - The functions in kern/pmap.h are handy.
// LAB 3: Your code here.
e->env_pgdir = (uintptr_t *)page2kva(p);
p->pp_ref++;
// 使用者和核心一樣能通路到4G的虛拟位址空間,其中
// UTOP以下的位址空間是使用者自己的
// UTOP以上的位址空間是核心的
// 指派一份UTOP以上的核心頁表目錄給使用者目錄頁表,UTOP以下使用者自由發揮
for (i=PDX(UTOP); i<NPDENTRIES; i++)
e->env_pgdir[i] = kern_pgdir[i];
// 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;
return 0;
}
和核心頁表目錄一樣這裡填寫了
UVPT
的目錄項,以便使用者程序在使用者空間通路到各頁面的頁表項,查詢到頁面的權限。具體在lab4中講到,原理也可以直接參考MIT-JOS系列:使用者态通路頁表項詳解
region_alloc()
函數功能:為環境配置設定
len
位元組的實體記憶體并映射到使用者的虛拟位址空間
- 不需要對配置設定的空間初始化
- 配置設定的頁使用者和核心具有寫權限
- 需要對起始位址
和長度va
進行4K頁面對齊len
代碼實作如下:
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!)
uintptr_t start = ROUNDDOWN(va, PGSIZE);
uintptr_t end = start + ROUNDUP(len, PGSIZE);
struct PageInfo *p = NULL;
int i;
for (i=start; i<end; i+=PGSIZE) {
p = page_alloc(0);
if (!p)
panic("Out of memory!\n");
if (page_insert(e->env_pgdir, p, i, PTE_U | PTE_W) == -E_NO_MEM)
panic("Out of memory!\n");
}
}
load_icode()
函數功能:解析ELF二進制檔案,加載其内容到新環境的使用者位址空間中
- 每個使用者程序都是一個ELF檔案。像boot loader所做的那樣,從ELF檔案加載使用者程序的初始代碼區、堆棧和處理器辨別位
- 這個函數僅在核心初始化期間、第一個使用者态環境運作前被調用
- 函數将ELF檔案中所有可加載的段載入到使用者位址空間中,設定
為ELF檔案頭訓示的入口(虛拟位址),以便它之後能從這裡開始執行程式e->env_tf.tf_eip
- 清零bss節
- 映射程式的初始堆棧到一個頁面
代碼實作如下:
特别注意這裡要進行一下頁表目錄位址的更換以便為使用者空間做正确的虛拟位址映射,以及映射完要把頁表目錄換回來
static void
load_icode(struct Env *e, uint8_t *binary)
{
// Hints:
// Load each program segment into virtual memory
// at the address specified in the ELF section header.
// You should only load segments with ph->p_type == ELF_PROG_LOAD.
// Each segment's virtual address can be found in ph->p_va
// and its size in memory can be found in ph->p_memsz.
// The ph->p_filesz bytes from the ELF binary, starting at
// 'binary + ph->p_offset', should be copied to virtual address
// ph->p_va. Any remaining memory bytes should be cleared to zero.
// (The ELF header should have ph->p_filesz <= ph->p_memsz.)
// Use functions from the previous lab to allocate and map pages.
//
// All page protection bits should be user read/write for now.
// ELF segments are not necessarily page-aligned, but you can
// assume for this function that no two segments will touch
// the same virtual page.
//
// You may find a function like region_alloc useful.
//
// Loading the segments is much simpler if you can move data
// directly into the virtual addresses stored in the ELF binary.
// So which page directory should be in force during
// this function?
//
// You must also do something with the program's entry point,
// to make sure that the environment starts executing there.
// What? (See env_run() and env_pop_tf() below.)
// LAB 3: Your code here.
struct Elf *elf = (struct Elf*)binary;
if (elf->e_magic != ELF_MAGIC)
panic("load_icode() error: Not the ELF file.\n");
// 設定程式的入口
e->env_tf.tf_eip = elf->e_entry;
// 接下來要将kernel的資料複制到使用者空間的虛拟位址,是以暫用一下使用者頁表目錄以找到正确的虛拟位址
lcr3(PADDR(e->env_pgdir));
struct Proghdr *ph, *eph;
ph = (struct Proghdr*)(binary + elf->e_phoff);
eph = ph + elf->e_phnum;
for (; ph<eph; ph++) {
if (ph->p_type == ELF_PROG_LOAD) {
if (ph->p_filesz > ph->p_memsz)
panic("load_icode() error: file length > memory length.\n");
region_alloc(e, ph->p_va, ph->p_memsz);
// 将elf檔案的段資料直接move到相應的虛拟位址空間
memmove(ph->p_va, binary+ph->p_offset, ph->p_filesz);
// 由于filesz<=memsz, 将多出來的memz置0
memset(ph->p_va+ph->p_filesz, 0, ph->p_memsz-ph->p_filesz);
}
}
// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
region_alloc(e, USTACKTOP-PGSIZE, PGSIZE);
// LAB 3: Your code here.
// 恢複核心的頁表目錄
lcr3(PADDR(kern_pgdir));
}
env_create()
函數功能:調用
env_alloc()
建立一個新的環境,調用
load_icode()
向環境中載入ELF檔案
代碼實作如下:
void
env_create(uint8_t *binary, enum EnvType type)
{
// LAB 3: Your code here.
struct Env *env = NULL;
int err = env_alloc(&env, 0);
if (err == -E_NO_FREE_ENV)
panic("env_create() error: no free environment.\n");
if (err == -E_NO_MEM)
panic("env_create() error: out of memory.\n");
env->env_type = type;
load_icode(env, binary);
}
env_run()
函數功能:運作一個給定的環境
- 如果是環境切換(即有環境正在運作):
- 如果目前環境
的curenv
為env_status
,設定它為ENV_RUNNING
ENV_RUNNABLE
- 設定
新的環境curenv為
- 設定新的環境的
為env_status
ENV_RUNNING
- 更新
計數env_runs
- 利用
切換位址空間lcr3()
- 如果目前環境
- 利用
恢複環境的重要寄存器并進入使用者模式env_pop_tf()
代碼實作如下:
void
env_run(struct Env *e)
{
// Step 1: If this is a context switch (a new environment is running):
// 1. Set the current environment (if any) back to
// ENV_RUNNABLE if it is ENV_RUNNING (think about
// what other states it can be in),
// 2. Set 'curenv' to the new environment,
// 3. Set its status to ENV_RUNNING,
// 4. Update its 'env_runs' counter,
// 5. Use lcr3() to switch to its address space.
// Step 2: Use env_pop_tf() to restore the environment's
// registers and drop into user mode in the
// environment.
// Hint: This function loads the new environment's state from
// e->env_tf. Go back through the code you wrote above
// and make sure you have set the relevant parts of
// e->env_tf to sensible values.
// LAB 3: Your code here.
if (curenv != NULL && curenv->env_status == ENV_RUNNING) {
curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
curenv->env_status = ENV_RUNNING;
curenv->env_runs++;
// 設定cr3中的頁表目錄位址:位址為實體位址
lcr3(PADDR(curenv->env_pgdir));
env_pop_tf(&curenv->env_tf);
// panic("env_run not yet implemented");
}
env_run()
這個函數是永不傳回的(不是指沒有傳回值,而是指它跳轉去執行别的代碼了,而且永遠不會回到這裡了),它利用
env_pop_tf()
函數設定新環境的重要寄存器,然後進入使用者環境執行代碼,是以
env_pop_tf()
必須放在
env_run()
的最後一行執行。
env_pop_tf()
的代碼如下:
// 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 */
}
env_pop_tf()
将
tf
的内容視為棧,目前棧頂設定為
tf
的第一個字段,pop過程能順序通路
tf
中的每個資料,從逐個出棧
tf
裡面的資料到相應的寄存器
-
将目前棧指針指向輸入變量movl %0,%%esp
tf
-
恢複所有popal
寄存器,即r32
裡的東西tf.PushRegs
- pop出
指派到tf_es
es
- pop出
指派到tf_ds
ds
- 跳過
和tf_trapno
,使tf_err
指向esp
tf_eip
- 調用
iret
,跳轉到使用者程序執行代碼
調用
時:從iret
指向的棧中順序出棧esp
指派到相應寄存器,然後程式跳轉到eip, cs, eflags(标志寄存器), esp, ss
處繼續執行,是以這個函數正常狀态下不會傳回cs:eip
使用者環境小結
至此為止,進入運作一個新環境的函數調用如下:
-
(start
)kern/entry.S
-
(i386_init
)kern/init.c
-
cons_init
-
mem_init
-
env_init
-
(還未實作)trap_init
-
env_create
-
env_run
-
env_pop_tf
-
-
啟動核心,建立并進入一個環境的過程大約如下:
- 啟動核心,開啟分頁,設定棧區(
,之前lab1做的)kern/entry.S
- 初始化.bss段,初始化一系列硬體(
,之前lab1做的)cons_init
- 虛拟記憶體初始化(
,之前lab2做的,lab3剛剛對其進行補充:增加初始化envs數組)mem_init
- 用CMOS檢測可用的實體記憶體
- 為kernel的頁表目錄配置設定記憶體,将頁表目錄
作為頁表插入到頁表項kern_pgdir
處(以便核心以外的環境在UVPT
處能夠查找到自己的頁表目錄,在lab4中會詳細講到)UVPT
- 初始化
數組,将實體記憶體以頁為機關記錄到pages
數組中并利用pages
管理空閑頁面,編寫頁表配置設定、釋放、映射的相關函數page_free_list
- 完成實體記憶體前256M的映射,與此同時填寫了頁表目錄和二級頁表,并賦予頁面相應權限
- 設定
為cr3
的實體位址,并設定kern_pgdir
的标志cr0
- 為所有可能的環境進行初始化(
)env_init
- 初始化
數組中的每一項狀态為envs
,并放入未使用的環境ENV_FREE
連結清單中env_free_list
- 加載GDT表和初始化段描述子
- 初始化
- 中斷設定和異常處理(
,還沒做)trap_init
- 建立一個新環境(
)env_create
- 調用
初始化一個環境:這個函數不是由我們動手編寫的,但調用了我們編寫的一些函數env_alloc()
- 從
拿出一個未被使用的env_free_list
結構Env
- 利用
初始化新環境的虛拟位址空間:env_setup_vm()
- 為新環境配置設定一頁記憶體作為頁表目錄
- 将其位址填寫到新環境的
域中env_pgdir
- 拷貝核心頁表目錄中
以上部分(核心位址空間映射情況)到新環境的頁表目錄UTOP
- 将新的頁表目錄作為頁表插入到新環境目錄頁表項的
處UVPT
- 為新環境生成一個唯一辨別
env_id
- 初始化新環境的其他域:
,env_parent_id
,env_type
,env_status
env_runs
- 初始化新環境的段寄存器
關聯目前GDT表,初始化棧指針env_tf.tf_ds/es/ss/cs
指向env_tf.tf_esp
USTACKTOP
- 修改
指向下一個未被使用的env_free_list
結構Env
- 從
- 調用
為新環境加載可執行二進制檔案,這個二進制是進入新環境後執行的程式:load_icode()
- 以elf格式讀取二進制的程式執行入口
,并用這個入口設定新環境的e_entry
,讓新環境能夠從二進制的入口開始執行程式env_tf.tf_eip
- 利用
為類型為region_alloc()
的節配置設定記憶體并映射到ELF_PROG_LOAD
訓示的位置p_va
- 利用
将這些節長度為memmove()
的檔案内容移動到p_filesz
訓示的位置p_va
- 由于
,利用p_filesz<=p_memsz
填充兩者之間的空缺memset()
- 注意:此處操作的
是新環境的位址空間,有别于kernel的位址空間,是以需要在操作之前先臨時把p_va
設定為新環境的頁表目錄位址cr3
,操作結束後再恢複到核心的頁表目錄位址e->env_pgdir
- 以elf格式讀取二進制的程式執行入口
- 為新環境配置設定一頁作為其棧區,映射到虛拟位址
處USTACKTOP-PGSIZE
- 這裡我尋思着
是在USTACKTOP-PGSIZE
底下,也屬于使用者記憶體區,也得在使用者位址空間下執行?UTOP
- 這裡我尋思着
- 調用
- 進入一個新環境,執行其程式(
)env_run
- 若目前有環境在運作,設定該環境的
為env_status
ENV_RUNNABLE
- 設定目前環境指針
指向新環境curenv
- 修改新環境的運作狀态為
,運作次數ENV_RUNNING
env_runs++
- 修改
為新環境的頁表目錄位址cr3
- 調用
進入新環境env_pop_tf()
- 若目前有環境在運作,設定該環境的
代碼完成到這裡,編譯并啟動qemu運作後,系統就應該能夠順利進入使用者環境并執行
hello
這個二進制檔案。由于目前還沒實作對中斷和異常的處理,是以它将在
hello
進行系統調用
int $0x30
時出錯(這行能在
hello.asm
中找到)。JOS此時尚未設定硬體允許從使用者空間轉換到核心态,當CPU發現這個它沒辦法處理這個中斷時,它引發一個異常;當它又發現它沒辦法處理這個異常時,它又引發了一個異常;但它發現它還是沒辦法處理這個異常,隻好放棄,是以最終引發了“三重異常”,在qemu中得到
Triple fault
的輸出。
檢視
hello.asm
,我們可以在中斷處設定斷點
b *0x800b44
,如果程式能夠執行到這個位置沒有異常,就說明之前編寫的代碼是正确的:
gdb:
The target architecture is assumed to be i8086
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb) b *0x800b44
Breakpoint 1 at 0x800b44
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x800b44: int $0x30
Breakpoint 1, 0x00800b44 in ?? ()
(gdb)
qemu:
[email protected]:~/1work/MIT-JOS/lab$ make qemu-nox-gdb
***
*** Now run 'make gdb'.
***
qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::26000 -D qemu.log -S
6828 decimal is 15254 octal!
Physical memory: 131072K available, base = 640K, extended = 130432K
check_page_alloc() succeeded!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_installed_pgdir() succeeded!
[00000000] new env 00001000
接下來,在gdb中輸入
si
再往下執行一步,将能在qemu中收到
Triple fault
:
gdb:
Breakpoint 1, 0x00800b44 in ?? ()
(gdb) si
=> 0x800b44: int $0x30
Breakpoint 1, 0x00800b44 in ?? ()
(gdb)
qemu:
[00000000] new env 00001000
EAX=00000000 EBX=00000000 ECX=0000000d EDX=eebfde88
ESI=00000000 EDI=00000000 EBP=eebfde60 ESP=eebfde54
EIP=00800b44 EFL=00000092 [--S-A--] CPL=3 II=0 A20=1 SMM=0 HLT=0
ES =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
CS =001b 00000000 ffffffff 00cffa00 DPL=3 CS32 [-R-]
SS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
DS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
FS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
GS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
LDT=0000 00000000 00000000 00008200 DPL=0 LDT
TR =0028 f018eb60 00000067 00408900 DPL=0 TSS32-avl
GDT= f011b300 0000002f
IDT= f018e340 000007ff
CR0=80050033 CR2=00000000 CR3=003bc000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
DR6=ffff0ff0 DR7=00000400
EFER=0000000000000000
Triple fault. Halting for inspection via QEMU monitor.
進行中斷和異常
見下一篇部落格MIT-JOS系列6:使用者環境(二)