MIT-6.828 Lab3實驗報
Lab 3: User Environments實驗報告
tags:mit-6.828 os
概述:
本文是lab3的實驗報告,主要介紹JOS中的程序,異常處理,系統呼叫。內容上分為三部分:
- 使用者環境建立,可以載入使用者ELF檔案並執行。
- 建立異常處理機制。
- 提供系統呼叫的能力。
Part A: User Environments and Exception Handling
本實驗指的使用者環境和UNIX中的程序是一個概念,之所有沒有使用程序是強調JOS的使用者環境和UNIX程序將提供不同的介面。 JOS使用ENV資料結構記錄使用者環境,本實驗只會建立一個使用者環境,lab4將會支援多使用者環境。核心維護了三個全域性變數,
struct Env *envs = NULL
struct Env *curenv = NULL
static struct Env *env_free_list
和lab2管理物理頁的思路一樣,envs指向一個ENV結構的陣列,curenv指向當前正在執行的環境,env_free_list指向一個ENV結構的連結串列,儲存未在執行的環境。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 };
各個欄位解釋如下:
- env_tf:Trapframe結構定義在inc/trap.h中,相當於暫存器的一個快照,當前使用者環境重新執行時,該結構中儲存的暫存器資訊將被重新載入到暫存器執行。
- env_link:指向下一個ENV結構,用於構建連結串列使用。
- env_id:使用者環境的id
- env_parent_id:當前使用者環境父節點的id
- env_type:對於大部分使用者環境是ENV_TYPE_USER,後面將會介紹特殊的系統服務環境
- env_status:當前使用者環境狀態
- env_pgdir:頁目錄地址
Exercise 1
實驗要求修改kern/mpap.c中的mem_init()函式,在其中分配一個ENV結構的陣列給全域性變數,並將線性地址UENVS對映到envs起始處。
思路和lab2中的pages陣列的分配一樣: 在mem_init()分配完pages陣列後,新增如下語句:
envs = (struct Env*)boot_alloc(sizeof(struct Env) * NENV);
memset(envs, 0, sizeof(struct Env) * NENV);
這樣就完成了envs的初始化。 同樣在mem_init()中對映完UPAGES後,對映UENVS:
boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U);
這樣執行完mem_init()後核心線性地址空間到實體地址空間的對映關係可用下圖表示:
由於現在還沒有檔案系統,我們將直接把使用者二進位制程式直接嵌入到核心中。obj/kern/kernel.sym中類似_binary_obj_user_hello_start,_binary_obj_user_hello_end,_binary_obj_user_hello_size這種符號就是使用者程式的起始線性地址,終止線性地址。
觀察kern/init.c中的i386_init()函式會發現多瞭如下語句:
env_init();
#if defined(TEST)
// Don't touch -- used by grading script!
ENV_CREATE(TEST, ENV_TYPE_USER);
#else
// Touch all you want.
ENV_CREATE(user_hello, ENV_TYPE_USER); //會呼叫env_create(_binary_obj_user_hello_start, ENV_TYPE_USER)
#endif // TEST*
env_run(&envs[0]); //envs[0]已經在env_create的時候初始化過了
ENV_CREATE(user_hello, ENV_TYPE_USER);
這個巨集相當於呼叫env_create(_binary_obj_user_hello_start, ENV_TYPE_USER)
env_init(), env_create(), env_run()這三個函式都沒有實現,需要在Exercise2中完成。
Exercise 2:
完成kern/evn.c中的如下函式,使mem_init()的env_run(&envs[0])能正常執行。
- env_init()
- env_setup_vm()
- region_alloc()
- load_icode()
- env_create()
- env_run()
env_init():
作用:初始化envs陣列,構建env_free_list連結串列,注意順序,envs[0]應該在連結串列頭部位置。實現如下:
// Mark all environments in 'envs' as free, set their env_ids to 0,
// and insert them into the env_free_list.
// Make sure the environments are in the free list in the same order
// they are in the envs array (i.e., so that the first call to
// env_alloc() returns envs[0]).
//
void
env_init(void)
{
// Set up envs array
// LAB 3: Your code here.
env_free_list = NULL;
for (int i = NENV - 1; i >= 0; i--) { //前插法構建連結串列
envs[i].env_id = 0;
envs[i].env_link = env_free_list;
env_free_list = &envs[i];
}
// Per-CPU part of the initialization
env_init_percpu(); //載入全域性描述符表(GDT)
}
env_init_percpu()載入全域性描述符表並且初始化段暫存器gs, fs, es, ds, ss。GDT定義在kern/env.c中:
struct Segdesc gdt[] =
{
// 0x0 - unused (always faults -- for trapping NULL far pointers)
SEG_NULL,
// 0x8 - kernel code segment
[GD_KT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 0),
// 0x10 - kernel data segment
[GD_KD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 0),
// 0x18 - user code segment
[GD_UT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 3),
// 0x20 - user data segment
[GD_UD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 3),
// 0x28 - tss, initialized in trap_init_percpu()
[GD_TSS0 >> 3] = SEG_NULL
};
struct Pseudodesc gdt_pd = {
sizeof(gdt) - 1, (unsigned long) gdt
};
env_setup_vm():
引數:
- struct Env *e:ENV結構指標
返回值:0表示成功,-E_NO_MEM表示失敗,沒有足夠實體地址。 作用:初始化e指向的Env結構代表的使用者環境的線性地址空間,設定e->env_pgdir欄位。
// Initialize the kernel virtual memory layout for environment e.
// Allocate a page directory, set e->env_pgdir accordingly,
// and initialize the kernel portion of the new environment's address space.
// Do NOT (yet) map anything into the user portion
// of the environment's virtual address space.
//
// Returns 0 on success, < 0 on error. Errors include:
// -E_NO_MEM if page directory or table could not be allocated.
//
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.
p->pp_ref++;
e->env_pgdir = (pde_t *) page2kva(p); //剛分配的物理頁作為頁目錄使用
memcpy(e->env_pgdir, kern_pgdir, PGSIZE); //繼承核心頁目錄
// 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; //唯一需要修改的是UVPT需要對映到當前環境的頁目錄實體地址e->env_pgdir處,而不是核心的頁目錄實體地址kern_pgdir處
return 0;
}
總的思路就是給e指向的Env結構分配頁目錄,並且繼承核心的頁目錄結構,唯一需要修改的是UVPT需要對映到當前環境的頁目錄實體地址e->env_pgdir處,而不是核心的頁目錄實體地址kern_pgdir處。設定完頁目錄也就確定了當前使用者環境線性地址空間到實體地址空間的對映。
region_alloc()
引數:
- struct Env *e:需要操作的使用者環境
- void *va:虛擬地址
- size_t len:長度
作用:操作e->env_pgdir,為[va, va+len)分配物理空間。
// Allocate len bytes of physical memory for environment env,
// and map it at virtual address va in the environment's address space.
// Does not zero or otherwise initialize the mapped pages in any way.
// Pages should be writable by user and kernel.
// Panic if any allocation attempt fails.
//
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!)
void *begin = ROUNDDOWN(va, PGSIZE), *end = ROUNDUP(va+len, PGSIZE);
while (begin < end) {
struct PageInfo *pg = page_alloc(0); //分配一個物理頁
if (!pg) {
panic("region_alloc failed\n");
}
page_insert(e->env_pgdir, pg, begin, PTE_W | PTE_U); //修改e->env_pgdir,建立線性地址begin到物理頁pg的對映關係
begin += PGSIZE; //更新線性地址
}
}
總的來說還是用lab2實現的函式操作e->env_pgdir結構。
load_icode()
引數:
- struct Env *e:需要操作的使用者環境
- uint8_t *binary:可執行使用者程式碼的起始地址
作用:載入binary地址開始處的ELF檔案。
// Set up the initial program binary, stack, and processor flags
// for a user process.
// This function is ONLY called during kernel initialization,
// before running the first user-mode environment.
//
// This function loads all loadable segments from the ELF binary image
// into the environment's user memory, starting at the appropriate
// virtual addresses indicated in the ELF program header.
// At the same time it clears to zero any portions of these segments
// that are marked in the program header as being mapped
// but not actually present in the ELF file - i.e., the program's bss section.
//
// All this is very similar to what our boot loader does, except the boot
// loader also needs to read the code from disk. Take a look at
// boot/main.c to get ideas.
//
// Finally, this function maps one page for the program's initial stack.
//
// load_icode panics if it encounters problems.
// - How might load_icode fail? What might be wrong with the given input?
//
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 segment 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 *ELFHDR = (struct Elf *) binary;
struct Proghdr *ph; //Program Header
int ph_num; //Program entry number
if (ELFHDR->e_magic != ELF_MAGIC) {
panic("binary is not ELF format\n");
}
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
ph_num = ELFHDR->e_phnum;
lcr3(PADDR(e->env_pgdir)); //這步別忘了,雖然到目前位置e->env_pgdir和kern_pgdir除了PDX(UVPT)這一項不同,其他都一樣。
//但是後面會給e->env_pgdir增加對映關係
for (int i = 0; i < ph_num; i++) {
if (ph[i].p_type == ELF_PROG_LOAD) { //只加載LOAD型別的Segment
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); //應該有如下關係:ph->p_filesz <= ph->p_memsz。搜尋BSS段
}
}
lcr3(PADDR(kern_pgdir));
e->env_tf.tf_eip = ELFHDR->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);
}
這裡相當於實現一個ELF可執行檔案載入器,不熟悉ELF檔案結構的同學可以參考我之前的筆記ELF格式。ELF檔案以一個ELF檔案頭開始,通過ELFHDR->e_magic欄位判斷該檔案是否是ELF格式的,然後通過ELFHDR->e_phoff獲取程式頭距離ELF檔案的偏移,ph指向的就是程式頭的起始位置,相當於一個數組,程式頭記錄了有哪些Segment需要載入,載入到線性地址的何處?ph_num儲存了總共有多少Segment。遍歷ph陣列,分配線性地址p_va開始的p_memsz大小的空間。並將ELF檔案中binary + ph[i].p_offset
偏移處的Segment拷貝到線性地址p_va處。
有一點需要注意,在執行for迴圈前,需要載入e->env_pgdir,也就是這句lcr3(PADDR(e->env_pgdir));
因為我們要將Segment拷貝到使用者的線性地址空間內,而不是核心的線性地址空間。
載入完Segment後需要設定e->env_tf.tf_eip = ELFHDR->e_entry;
也就是程式第一條指令的位置。
最後region_alloc(e, (void *) (USTACKTOP - PGSIZE), PGSIZE);
為使用者環境分配棧空間。
env_create()
引數:
- uint8_t *binary:將要載入的可執行檔案的起始位置
- enum EnvType type:使用者環境型別
作用:從env_free_list連結串列拿一個Env結構,載入從binary地址開始處的ELF可執行檔案到該Env結構。
// Allocates a new env with env_alloc, loads the named elf
// binary into it with load_icode, and sets its env_type.
// This function is ONLY called during kernel initialization,
// before running the first user-mode environment.
// The new env's parent ID is set to 0.
//
void
env_create(uint8_t *binary, enum EnvType type)
{
// LAB 3: Your code here.
struct Env *e;
int r;
if ((r = env_alloc(&e, 0) != 0)) {
panic("create env failed\n");
}
load_icode(e, binary);
e->env_type = type;
}
env_alloc(), load_icode()前面已經實現了,所以不難理解。
env_run(struct Env *e) 引數:
- struct Env *e:需要執行的使用者環境
作用:執行e指向的使用者環境
// Context switch from curenv to env e.
// Note: if this is the first call to env_run, curenv is NULL.
//
// This function does not return.
//
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;
e->env_status = ENV_RUNNING;
e->env_runs++;
lcr3(PADDR(e->env_pgdir)); //載入線性地址空間
env_pop_tf(&e->env_tf); //彈出env_tf結構到暫存器
}
該函式首先設定curenv,然後修改e->env_status,e->env_runs兩個欄位。 接著載入線性地址空間,最後將e->env_tf結構中的暫存器快照彈出到暫存器,這樣就會從新的%eip地址處讀取指令進行解析。 Trapframe結構和env_pop_tf()函式如下:
struct PushRegs {
/* registers as pushed by pusha */
uint32_t reg_edi;
uint32_t reg_esi;
uint32_t reg_ebp;
uint32_t reg_oesp; /* Useless */
uint32_t reg_ebx;
uint32_t reg_edx;
uint32_t reg_ecx;
uint32_t reg_eax;
} __attribute__((packed));
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" //將%esp指向tf地址處
"\tpopal\n" //彈出Trapframe結構中的tf_regs值到通用暫存器
"\tpopl %%es\n" //彈出Trapframe結構中的tf_es值到%es暫存器
"\tpopl %%ds\n" //彈出Trapframe結構中的tf_ds值到%ds暫存器
"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
"\tiret\n" //中斷返回指令,具體動作如下:從Trapframe結構中依次彈出tf_eip,tf_cs,tf_eflags,tf_esp,tf_ss到相應暫存器
: : "g" (tf) : "memory");
panic("iret failed"); /* mostly to placate the compiler */
}
PushRegs
結構儲存的正是通用暫存器的值,env_pop_tf()第一條指令,將將%esp指向tf地址處,也就是將棧頂指向Trapframe
結構開始處,Trapframe
結構開始處正是一個PushRegs
結構,popal
將PushRegs
結構中儲存的通用暫存器值彈出到暫存器中,接著按順序彈出暫存器%es, %ds。最後執行iret
指令,該指令是中斷返回指令,具體動作如下:從Trapframe結構中依次彈出tf_eip,tf_cs,tf_eflags,tf_esp,tf_ss到相應暫存器。你會發現和Trapframe
結構從上往下是完全一致的。
最後總結下這些函式的呼叫關係:
env_create()
-->env_alloc()
-->env_setup_vm()
-->load_icode()
-->region_alloc()
現在i386_init()函式中的env_run(&envs[0]);
呼叫應該能正常執行,並且將控制轉移到hello(user/hello.c)程式中。我們用GDB在env_pop_tf()函式設定斷點,然後通過指令si,單步除錯,觀察iret指令前後暫存器的變化。iret指令後執行的第一條指令應該是cmpl指令(lib/entry.S中的start label處)然後進入hello中執行(可以檢視hello的反彙編obj/user/hello.asm),如果順利將會執行到一條int指令,這是一個系統呼叫,將字元顯示到控制檯,但是現在還不起作用。
The target architecture is assumed to be i8086
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb) b env_pop_tf //設定斷點
Breakpoint 1 at 0xf0102d5f: file kern/env.c, line 470.
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0xf0102d5f <env_pop_tf>: push %ebp
Breakpoint 1, env_pop_tf (tf=0xf01b2000) at kern/env.c:470
470 {
(gdb) si //單步
=> 0xf0102d60 <env_pop_tf+1>: mov %esp,%ebp
0xf0102d60 470 {
(gdb) //單步
=> 0xf0102d62 <env_pop_tf+3>: sub $0xc,%esp
0xf0102d62 470 {
(gdb) //單步
=> 0xf0102d65 <env_pop_tf+6>: mov 0x8(%ebp),%esp
471 asm volatile(
(gdb) //單步
=> 0xf0102d68 <env_pop_tf+9>: popa
0xf0102d68 471 asm volatile(
(gdb) //單步
=> 0xf0102d69 <env_pop_tf+10>: pop %es
0xf0102d69 in env_pop_tf (tf=<error reading variable: Unknown argument list address for `tf'.>)
at kern/env.c:471
471 asm volatile(
(gdb) //單步
=> 0xf0102d6a <env_pop_tf+11>: pop %ds
0xf0102d6a 471 asm volatile(
(gdb) //單步
=> 0xf0102d6b <env_pop_tf+12>: add $0x8,%esp
0xf0102d6b 471 asm volatile(
(gdb) //單步
=> 0xf0102d6e <env_pop_tf+15>: iret
0xf0102d6e 471 asm volatile(
(gdb) info registers //在執行iret前,檢視暫存器資訊
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xf01b2030 0xf01b2030
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0xf0102d6e 0xf0102d6e <env_pop_tf+15>
eflags 0x96 [ PF AF SF ]
cs 0x8 8 //0x8正是核心程式碼段的段選擇子
ss 0x10 16
ds 0x23 35
es 0x23 35
fs 0x23 35
gs 0x23 35
(gdb) si //單步執行,指令應該執行iret指令
=> 0x800020: cmp $0xeebfe000,%esp
0x00800020 in ?? ()
(gdb) info registers //執行iret指令後,差看暫存器
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xeebfe000 0xeebfe000
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x800020 0x800020
eflags 0x2 [ ]
cs 0x1b 27 //0x18是使用者程式碼段的在GDT中的偏移,使用者許可權是0x3,所以選擇子正好是0x1b
ss 0x23 35 //這些暫存器值都是在env_alloc()中被設定好的
ds 0x23 35
es 0x23 35
fs 0x23 35
gs 0x23 35
(gdb) b *0x800a1c //通過檢視obj/user/hello.asm找到斷點位置
Breakpoint 2 at 0x800a1c
(gdb) c
Continuing.
=> 0x800a1c: int $0x30 //系統呼叫指令,現在還不起作用
Breakpoint 2, 0x00800a1c in ?? ()
(gdb)
觀察執行iret前後的cs段暫存器的值,執行iret前cs的值0x8正是核心程式碼段的段選擇子(GD_KT定義在inc/memlayout.h中),執行後cs的值0x1b,0x18是使用者程式碼段的在GDT中的偏移(GD_UT定義在inc/memlayout.h中),使用者許可權是0x3,所以選擇子正好是0x1b。 現在來看env_alloc()函式:
// Allocates and initializes a new environment.
// On success, the new environment is stored in *newenv_store.
//
// Returns 0 on success, < 0 on failure. Errors include:
// -E_NO_FREE_ENV if all NENV environments are allocated
// -E_NO_MEM on memory exhaustion
//
int
env_alloc(struct Env **newenv_store, envid_t parent_id)
{
int32_t generation;
int r;
struct Env *e;
if (!(e = env_free_list))
return -E_NO_FREE_ENV;
// Allocate and set up the page directory for this environment.
if ((r = env_setup_vm(e)) < 0)
return r;
// Generate an env_id for this environment.
generation = (e->env_id + (1 << ENVGENSHIFT)) & ~(NENV - 1);
if (generation <= 0) // Don't create a negative env_id.
generation = 1 << ENVGENSHIFT;
e->env_id = generation | (e - envs);
// Set the basic status variables.
e->env_parent_id = parent_id;
e->env_type = ENV_TYPE_USER;
e->env_status = ENV_RUNNABLE;
e->env_runs = 0;
// Clear out all the saved register state,
// to prevent the register values
// of a prior environment inhabiting this Env structure
// from "leaking" into our new environment.
memset(&e->env_tf, 0, sizeof(e->env_tf));
// Set up appropriate initial values for the segment registers.
// GD_UD is the user data segment selector in the GDT, and
// GD_UT is the user text segment selector (see inc/memlayout.h).
// The low 2 bits of each segment register contains the
// Requestor Privilege Level (RPL); 3 means user mode. When
// we switch privilege levels, the hardware does various
// checks involving the RPL and the Descriptor Privilege Level
// (DPL) stored in the descriptors themselves.
e->env_tf.tf_ds = GD_UD | 3; //設定ds
e->env_tf.tf_es = GD_UD | 3; //設定es
e->env_tf.tf_ss = GD_UD | 3; //設定ss
e->env_tf.tf_esp = USTACKTOP; //設定esp
e->env_tf.tf_cs = GD_UT | 3; //設定cs
// You will set e->env_tf.tf_eip later.
// commit the allocation
env_free_list = e->env_link;
*newenv_store = e;
cprintf("[%08x] new env %08x\n", curenv ? curenv->env_id : 0, e->env_id);
return 0;
}
這裡就是設定e->env_tf結構的地方,設定完後再執行iret指令,暫存器就會載入這些設定了的值。豁然開朗了。
Handling Interrupts and Exceptions
Basics of Protected Control Transfer
閱讀Chapter 9, Exceptions and Interrupts熟悉x86中斷和異常機制。 中斷和異常都是保護控制轉換。在Intel體系語義下,中斷是一種由處理器之外的非同步事件引起的保護控制轉換,比如外部裝置的通知。異常是由正在執行的程式碼引起的同步的保護控制轉換,比如訪問無效記憶體,或者除以0。 為了防止中斷髮生時,當前執行的程式碼不會跳轉到核心的任意位置執行,x86提供了兩種機制:
- 中斷描述符表:處理器確保異常或中斷髮生時,只會跳轉到由核心定義的程式碼點處執行。x86允許256種不同的中斷或異常進入點,每一個都有一個向量號,從0到255。CPU使用向量號作為IDT的索引,取出一個IDT描述符,根據IDT描述符可以獲取中斷處理函式cs和eip的值,從而進入中斷處理函式執行。
- 任務狀態段(TSS):當x86異常發生,並且發生了從使用者模式到核心模式的轉換時,處理器也會進行棧切換。一個叫做task state segment (TSS)的結構指定了棧的位置。TSS是一個很大的資料結構,由於JOS中核心模式就是指許可權0,所以處理器只使用TSS結構的ESP0和SS0兩個欄位來定義核心棧,其它欄位不使用。
Types of Exceptions and Interrupts
0-31號中斷都是同步中斷,缺頁中斷就是14號,31號以上的中斷可以由int指令,或者外部裝置觸發。在JOS中,將用48號中斷作為系統呼叫中斷。
An Example
假設處理器正在執行程式碼,這時遇到一條除法指令嘗試除以0,處理器將會做如下動作:
- 將棧切換到TSS的SS0和ESP0欄位定義的核心棧中,在JOS中兩個值分別是GD_KD和KSTACKTOP。
- 處理器在核心棧中壓入如下引數:
+--------------------+ KSTACKTOP
| 0x00000 | old SS | " - 4
| old ESP | " - 8
| old EFLAGS | " - 12
| 0x00000 | old CS | " - 16
| old EIP | " - 20 <---- ESP
+--------------------+
- 除以0的異常中斷號是0,處理器讀取IDT的第0項,從中解析出CS:EIP。
- CS:EIP處的異常處理函式執行。 對於一些異常來說,除了壓入上圖五個word,還會壓入錯誤程式碼,如下所示:
+--------------------+ KSTACKTOP
| 0x00000 | old SS | " - 4
| old ESP | " - 8
| old EFLAGS | " - 12
| 0x00000 | old CS | " - 16
| old EIP | " - 20
| error code | " - 24 <---- ESP
+--------------------+
仔細觀察壓入的資料和Trapframe
結構,你會發現是一致的。
Exercise 4
需要我們修改trapentry.S和trap.c建立異常處理函式,在trap_init()中建立並且載入IDT。
在trapentry.S中加入如下程式碼:
/*
* Lab 3: Your code here for generating entry points for the different traps.
*/
TRAPHANDLER_NOEC(th0, 0)
TRAPHANDLER_NOEC(th1, 1)
TRAPHANDLER_NOEC(th3, 3)
TRAPHANDLER_NOEC(th4, 4)
TRAPHANDLER_NOEC(th5, 5)
TRAPHANDLER_NOEC(th6, 6)
TRAPHANDLER_NOEC(th7, 7)
TRAPHANDLER(th8, 8)
TRAPHANDLER_NOEC(th9, 9)
TRAPHANDLER(th10, 10)
TRAPHANDLER(th11, 11)
TRAPHANDLER(th12, 12)
TRAPHANDLER(th13, 13)
TRAPHANDLER(th14, 14)
TRAPHANDLER_NOEC(th16, 16)
TRAPHANDLER_NOEC(th_syscall, T_SYSCALL)
/*
* Lab 3: Your code here for _alltraps
*/
//參考inc/trap.h中的Trapframe結構。tf_ss,tf_esp,tf_eflags,tf_cs,tf_eip,tf_err在中斷髮生時由處理器壓入,所以現在只需要壓入剩下暫存器(%ds,%es,通用暫存器)
//切換到核心資料段
_alltraps:
pushl %ds
pushl %es
pushal
pushl $GD_KD
popl %ds
pushl $GD_KD
popl %es
pushl %esp //壓入trap()的引數tf,%esp指向Trapframe結構的起始地址
call trap //呼叫trap()函式
我們使用TRAPHANDLER和TRAPHANDLER_NOEC巨集建立0~16號中斷的中斷處理函式。TRAPHANDLER和TRAPHANDLER_NOEC建立的函式都會跳轉到_alltraps處,這裡參考inc/trap.h中的Trapframe結構,tf_ss,tf_esp,tf_eflags,tf_cs,tf_eip,tf_err在中斷髮生時由處理器壓入,所以現在只需要壓入剩下暫存器(%ds,%es,通用暫存器)。然後將%esp壓入棧中(也就是壓入trap()的引數tf),這裡不明白的同學回顧下lab1函式呼叫的過程。最後跳轉到trap()函式執行。 現在異常處理函式有了,還沒有建立IDT,下面修改trap_init():
void
trap_init(void)
{
extern struct Segdesc gdt[];
// LAB 3: Your code here.
void th0();
void th1();
void th3();
void th4();
void th5();
void th6();
void th7();
void th8();
void th9();
void th10();
void th11();
void th12();
void th13();
void th14();
void th16();
void th_syscall();
SETGATE(idt[0], 0, GD_KT, th0, 0); //格式如下:SETGATE(gate, istrap, sel, off, dpl),定義在inc/mmu.h中
SETGATE(idt[1], 0, GD_KT, th1, 0);
SETGATE(idt[3], 0, GD_KT, th3, 3);
SETGATE(idt[4], 0, GD_KT, th4, 0);
SETGATE(idt[5], 0, GD_KT, th5, 0);
SETGATE(idt[6], 0, GD_KT, th6, 0);
SETGATE(idt[7], 0, GD_KT, th7, 0);
SETGATE(idt[8], 0, GD_KT, th8, 0);
SETGATE(idt[9], 0, GD_KT, th9, 0);
SETGATE(idt[10], 0, GD_KT, th10, 0);
SETGATE(idt[11], 0, GD_KT, th11, 0);
SETGATE(idt[12], 0, GD_KT, th12, 0);
SETGATE(idt[13], 0, GD_KT, th13, 0);
SETGATE(idt[14], 0, GD_KT, th14, 0);
SETGATE(idt[16], 0, GD_KT, th16, 0);
SETGATE(idt[T_SYSCALL], 0, GD_KT, th_syscall, 3); //為什麼門的DPL要定義為3,參考《x86組合語言-從真實模式到保護模式》p345
// Per-CPU setup
trap_init_percpu();
}
該函式會在進入核心時由i386_init()呼叫。我們新增的程式碼就是建立IDT,trap_init_percpu()中的lidt(&idt_pd);
正式載入IDT。
Part B: Page Faults, Breakpoints Exceptions, and System Calls
Handling Page Faults
缺頁中斷中斷號是14,發生時引發缺頁中斷的線性地址將會被儲存到CR2暫存器中。
Exercise 5
修改trap_dispatch(),將頁錯誤分配給page_fault_handler()處理。在trap_dispatch()新增如下程式碼:
// LAB 3: Your code here.
if (tf->tf_trapno == T_PGFLT) {
page_fault_handler(tf);
return;
}
The Breakpoint Exception
斷點異常中斷號是3,偵錯程式常常插入一位元組的int3指令臨時替代某條指令,從而引發斷點異常。
Exercise 6
修改trap_dispatch(),使得當斷點異常發生時呼叫核心的monitor。在trap_dispatch()繼續新增如下程式碼:
if (tf->tf_trapno == T_BRKPT) {
monitor(tf);
return;
}
System calls
JOS使用int指令實現系統呼叫,使用0x30作為中斷號。應用使用暫存器傳遞系統呼叫號和引數。系統呼叫號儲存在%eax,五個引數依次儲存在%edx, %ecx, %ebx, %edi, %esi中。返回值儲存在%eax中。
Exercise 7
需要我們做如下幾件事:
- 為中斷號T_SYSCALL新增一箇中斷處理函式
- 在trap_dispatch()中判斷中斷號如果是T_SYSCALL,呼叫定義在kern/syscall.c中的syscall()函式,並將syscall()儲存的返回值儲存到tf->tf_regs.reg_eax等將來恢復到eax暫存器中。
- 修改kern/syscall.c中的syscall()函式,使能處理定義在inc/syscall.h中的所有系統呼叫。
步驟一如下:分別在trapentry.S和trap.c的trap_init()函式中新增如下程式碼:
TRAPHANDLER_NOEC(th_syscall, T_SYSCALL)
SETGATE(idt[T_SYSCALL], 0, GD_KT, th_syscall, 3); //為什麼門的DPL要定義為3,參考《x86組合語言-從真實模式到保護模式》p345
步驟二:在trap.c的trap_dispatch()中新增如下程式碼:
if (tf->tf_trapno == T_SYSCALL) { //如果是系統呼叫,按照前文說的規則,從暫存器中取出系統呼叫號和五個引數,傳給kern/syscall.c中的syscall(),並將返回值儲存到tf->tf_regs.reg_eax
tf->tf_regs.reg_eax = syscall(tf->tf_regs.reg_eax, tf->tf_regs.reg_edx, tf->tf_regs.reg_ecx,
tf->tf_regs.reg_ebx, tf->tf_regs.reg_edi, tf->tf_regs.reg_esi);
return;
}
步驟三:修改kern/syscall.c中的syscall()
// Dispatches to the correct kernel function, passing the arguments.
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
// Call the function corresponding to the 'syscallno' parameter.
// Return any appropriate return value.
// LAB 3: Your code here.
int32_t ret;
switch (syscallno) { //根據系統呼叫號呼叫相應函式
case SYS_cputs:
sys_cputs((char *)a1, (size_t)a2);
ret = 0;
break;
case SYS_cgetc:
ret = sys_cgetc();
break;
case SYS_getenvid:
ret = sys_getenvid();
break;
case SYS_env_destroy:
ret = sys_env_destroy((envid_t)a1);
break;
default:
return -E_INVAL;
}
return ret;
}
現在回顧一下系統呼叫的完成流程:以user/hello.c為例,其中呼叫了cprintf(),注意這是lib/print.c中的cprintf,該cprintf()最終會呼叫lib/syscall.c中的sys_cputs(),sys_cputs()又會呼叫lib/syscall.c中的syscall(),該函式將系統呼叫號放入%eax暫存器,五個引數依次放入in DX, CX, BX, DI, SI,然後執行指令int 0x30,發生中斷後,去IDT中查詢中斷處理函式,最終會走到kern/trap.c的trap_dispatch()中,我們根據中斷號0x30,又會呼叫kern/syscall.c中的syscall()函式(注意這時候我們已經進入了核心模式CPL=0),在該函式中根據系統呼叫號呼叫kern/print.c中的cprintf()函式,該函式最終呼叫kern/console.c中的cputchar()將字串列印到控制檯。當trap_dispatch()返回後,trap()會呼叫env_run(curenv);
,該函式前面講過,會將curenv->env_tf結構中儲存的暫存器快照重新恢復到暫存器中,這樣又會回到使用者程式系統呼叫之後的那條指令執行,只是這時候已經執行了系統呼叫並且暫存器eax中儲存著系統呼叫的返回值。任務完成重新回到使用者模式CPL=3。
Exercise 8
使用者程式執行後都會走到lib/libmain.c中的libmain(),需要修改該函式初始化其中的const volatile struct Env *thisenv;
變數。
void
libmain(int argc, char **argv)
{
// set thisenv to point at our Env structure in envs[].
// LAB 3: Your code here.
envid_t envid = sys_getenvid(); //系統呼叫,我們已經在Exercise 7中實現了
thisenv = envs + ENVX(envid); //獲取Env結構指標
// save the name of the program so that panic() can use it
if (argc > 0)
binaryname = argv[0];
// call user main routine
umain(argc, argv);
// exit gracefully
exit();
}
如果一切順利:user/hello.c:
void
umain(int argc, char **argv)
{
cprintf("hello, world\n");
cprintf("i am environment %08x\n", thisenv->env_id); //現在我們已經初始化了thisenv變量了,所以可以列印處來了O(∩_∩)O
}
將先打印出'hello, world',然後列印'i am environment 00001000'。
Page faults and memory protection
作業系統依賴處理器的來實現記憶體保護。當程式試圖訪問無效地址或沒有訪問許可權時,處理器在當前指令停住,引發中斷進入核心。如果核心能夠修復,則在剛才的指令處繼續執行,否則程式將無法接著執行。系統呼叫也為記憶體保護帶來了問題。大部分系統呼叫介面讓使用者程式傳遞一個指標引數給核心。這些指標指向的是使用者緩衝區。通過這種方式,系統呼叫在執行時就可以解引用這些指標。但是這裡有兩個問題:
- 在核心中的page fault要比在使用者程式中的page fault更嚴重。如果核心在操作自己的資料結構時出現 page faults,這是一個核心的bug,而且異常處理程式會中斷整個核心。但是當核心在解引用由使用者程式傳遞來的指標時,它需要一種方法去記錄此時出現的任何page faults都是由使用者程式帶來的。
- 核心通常比使用者程式有著更高的記憶體訪問許可權。使用者程式很有可能要傳遞一個指標給系統呼叫,這個指標指向的記憶體區域是核心可以進行讀寫的,但是使用者程式不能。此時核心必須小心的去解析這個指標,否則的話核心的重要資訊很有可能被洩露。
Exercise 9
需要我們做兩件事情:
- 首先如果頁錯誤發生在核心態時應該直接panic。
- 實現kern/pmap.c中的user_mem_check()工具函式,該函式檢測使用者環境是否有許可權訪問線性地址區域[va, va+len)。然後對在kern/syscall.c中的系統呼叫函式使用user_mem_check()工具函式進行記憶體訪問許可權檢查。
第一步:在page_fault_handler()中新增如下程式碼:
if ((tf->tf_cs & 3) == 0) //核心態發生缺頁中斷直接panic
panic("page_fault_handler():page fault in kernel mode!\n");
第二步:修改kern/pmap.c中的user_mem_check(),進行檢查
// Check that an environment is allowed to access the range of memory
// [va, va+len) with permissions 'perm | PTE_P'.
// Normally 'perm' will contain PTE_U at least, but this is not required.
// 'va' and 'len' need not be page-aligned; you must test every page that
// contains any of that range. You will test either 'len/PGSIZE',
// 'len/PGSIZE + 1', or 'len/PGSIZE + 2' pages.
//
// A user program can access a virtual address if (1) the address is below
// ULIM, and (2) the page table gives it permission. These are exactly
// the tests you should implement here.
//
// If there is an error, set the 'user_mem_check_addr' variable to the first
// erroneous virtual address.
//
// Returns 0 if the user program can access this range of addresses,
// and -E_FAULT otherwise.
//
int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
// LAB 3: Your code here.
cprintf("user_mem_check va: %x, len: %x\n", va, len);
uint32_t begin = (uint32_t) ROUNDDOWN(va, PGSIZE);
uint32_t end = (uint32_t) ROUNDUP(va+len, PGSIZE);
uint32_t i;
for (i = (uint32_t)begin; i < end; i += PGSIZE) {
pte_t *pte = pgdir_walk(env->env_pgdir, (void*)i, 0);
if ((i >= ULIM) || !pte || !(*pte & PTE_P) || ((*pte & perm) != perm)) { //具體檢測規則
user_mem_check_addr = (i < (uint32_t)va ? (uint32_t)va : i); //記錄無效的那個線性地址
return -E_FAULT;
}
}
cprintf("user_mem_check success va: %x, len: %x\n", va, len);
return 0;
}
//
// Checks that environment 'env' is allowed to access the range
// of memory [va, va+len) with permissions 'perm | PTE_U | PTE_P'.
// If it can, then the function simply returns.
// If it cannot, 'env' is destroyed and, if env is the current
// environment, this function will not return.
//
void
user_mem_assert(struct Env *env, const void *va, size_t len, int perm)
{
if (user_mem_check(env, va, len, perm | PTE_U) < 0) {
cprintf("[%08x] user_mem_check assertion failure for "
"va %08x\n", env->env_id, user_mem_check_addr);
env_destroy(env); // may not return
}
}
有了工具函式,我們看kern/syscall.c中的系統呼叫函式只有sys_cputs()引數中有指標,所以需要對其進行檢測:
// Print a string to the system console.
// The string is exactly 'len' characters long.
// Destroys the environment on memory errors.
static void
sys_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_assert(curenv, s, len, 0);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}
所有做完後,執行user/buggyhello,將會看到如下輸出:
[00001000] user_mem_check assertion failure for va 00000001
[00001000] free env 00001000
Destroyed the only environment - nothing more to do!
結束實驗9實際上實驗10也一併做完了。
總結
至此,lab3的所有實驗都已完成。如果順利執行./grade-lab3會看到: 回顧下,本實驗大致做了三件事:
- 使用者環境建立,可以載入使用者ELF檔案並執行。
- 建立異常處理函式,建立並載入IDT,使JOS能支援中斷處理。要能說出中斷髮生時的詳細步驟。
- 利用中斷機制,使JOS支援系統呼叫。要能說出遇到int 0x30這條系統呼叫指令時發生的詳細步驟。
如有錯誤,歡迎指正(*^_^*): 15313676365