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兩個字段來定義內核棧,其它字段不使用。那么內核如何找到這個TSS結構的呢?JOS內核維護了一個
static struct Taskstate ts;
的變量,然后在trap_init_percpu()函數中,設置TSS選擇子(使用ltr指令)。
void
trap_init_percpu(void)
{
// Setup a TSS so that we get the right stack
// when we trap to the kernel.
ts.ts_esp0 = KSTACKTOP;
ts.ts_ss0 = GD_KD;
ts.ts_iomb = sizeof(struct Taskstate);
// 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;
// Load the TSS selector (like other segment selectors, the
// bottom three bits are special; we leave them 0)
ltr(GD_TSS0); //設置TSS選擇子
// Load the IDT
lidt(&idt_pd);
}
到目前我們已經碰到很多除通用寄存器之外的寄存器了,下圖總結了各種寄存器:
- TSS選擇器就是剛才用ltr指令設置的。中斷發生時,自動通過該寄存器找到TSS結構(JOS中是ts這個變量),將棧寄存器SS和ESP分別設置為其中的SS0和ESP0兩個字段的值,這樣棧就切換到了內核棧。
- GDTR就是全局描述符表寄存器,之前已經設置過了。
- PDBR是頁目錄基址寄存器,通過該寄存器找到頁目錄和頁表,將虛擬地址映射為物理地址。
- IDTR是中斷描述符表寄存器,通過這個寄存器中的值可以找到中斷表。
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中加入如下代碼:
#define TRAPHANDLER(name, num) \
.globl name; /* define global symbol for 'name' */ \
.type name, @function; /* symbol type is function */ \
.align 2; /* align function definition */ \
name: /* function starts here */ \
pushl $(num); \
jmp _alltraps
/* Use TRAPHANDLER_NOEC for traps where the CPU doesn't push an error code.
* It pushes a 0 in place of the error code, so the trap frame has the same
* format in either case.
*/
#define TRAPHANDLER_NOEC(name, num) \
.globl name; \
.type name, @function; \
.align 2; \
name: \
pushl $0; \
pushl $(num); \
jmp _alltraps
.text
/*
* 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():
#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; \
}
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); //設置idt[1],段選擇子為內核代碼段,段內偏移為th1
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文件並執行。
- 內核維護一個名叫envs的Env數組,每個Env結構對應一個進程,Env結構最重要的字段有Trapframe env_tf(該字段中斷發生時可以保持寄存器的狀態),pde_t *env_pgdir(該進程的頁目錄地址)。進程對應的內核數據結構可以用下圖總結:
- 定義了env_init(),env_create()等函數,初始化Env結構,將Env結構Trapframe env_tf中的寄存器值設置到寄存器中,從而執行該Env。
- 內核維護一個名叫envs的Env數組,每個Env結構對應一個進程,Env結構最重要的字段有Trapframe env_tf(該字段中斷發生時可以保持寄存器的狀態),pde_t *env_pgdir(該進程的頁目錄地址)。進程對應的內核數據結構可以用下圖總結:
- 創建異常處理函數,建立並加載IDT,使JOS能支持中斷處理。要能說出中斷發生時的詳細步驟。需要搞清楚內核態和用戶態轉換方式:通過中斷機制可以從用戶環境進入內核態。使用iret指令從內核態回到用戶環境。中斷發生過程以及中斷返回過程和系統調用原理可以總結為下圖:
- 利用中斷機制,使JOS支持系統調用。要能說出遇到int 0x30這條系統調用指令時發生的詳細步驟。見上圖。
具體代碼在:https://github.com/gatsbyd/mit_6.828_jos
如有錯誤,歡迎指正(_):
15313676365