发布于 

MIT 6.828 Lab3

Notes of MIT 6.828 Lab3

Part A: User Environments and Exception Handling

Environment State

In JOS, Environment is a similar concept to process, which couples the concepts of “thread” and “address space”. The thread is defined primarily by the saved registers (the env_tf field), and the address space is defined by the page directory and page tables pointed to by env_pgdir.

We use Env structure to hold each environment.

1
2
3
4
5
6
7
8
9
10
11
12
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
};

The kernel maintains three main global variables pertaining to environments:

1
2
3
struct Env *envs = NULL;        // All environments
struct Env *curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list

Allocating the Environments Array

Modify mem_init() further to allocate a similar array of Env structures, called envs.

Exercise 1

Like above, we use boot_alloc() to allocate space for envs.

1
2
envs = (struct Env *) boot_alloc(NENV * sizeof(struct Env));
memset(envs, 0, NENV * sizeof(struct Env));

Creating and Running Environments

Implement the following functions to create a user environment, load a static binary image that is embedded within the kernel and run the environment.

Exercise 2

env_init() initializes all of the Env structures in the env array and add them to the env_free_list. Also calls env_init_percpu, which configures the segmentation hardware with separate segments for privilege level 0 (kernel) and privilege level 3 (user).

We need to make sure the environments are in the free list in the same order they are in the envs array, so the order of loop is reverse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
env_init(void)
{
// Set up envs array
for (int i = NENV - 1; i >= 0; --i) {
envs[i].env_id = 0;
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_setup_vm() allocates a page directory for a new environment and initialize the kernel portion of the new environment’s address space.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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.

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;

return 0;
}

region_alloc() allocates and maps physical memory for an environment.
This function will be used in load_icode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void
region_alloc(struct Env *e, void *va, size_t len)
{
// 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!)
uint32_t beginva = ROUNDDOWN((uint32_t)va, PGSIZE);
uint32_t endva = ROUNDUP((uint32_t)va + len, PGSIZE);
struct PageInfo *pp;

while(beginva < endva) {
pp = page_alloc(0);
if (pp == NULL) {
panic("region_alloc: %e", -E_NO_MEM);
break;
}
if (page_insert(e->env_pgdir, pp, (void *)beginva, (PTE_U | PTE_W)) == -E_NO_MEM) {
panic("region_alloc: %e", -E_NO_MEM);
break;
}
beginva += PGSIZE;
}
}

load_icode() parses an ELF binary image, much like the boot loader already does, and load its contents into the user address space of a new environment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void
load_icode(struct Env *e, uint8_t *binary)
{
// parse elf header
struct Elf *elfhdr = (struct Elf *)binary;
if (elfhdr->e_magic != ELF_MAGIC)
panic("invalid ELF header");

// parse program header
struct Proghdr *ph, *eph;
ph = (struct Proghdr *) ((uint8_t *) elfhdr + elfhdr->e_phoff);
eph = ph + elfhdr->e_phnum;

// load env pgdir to copy data
lcr3(PADDR(e->env_pgdir));

// load each segment
for (; ph < eph; ph++) {
region_alloc(e, (void *)ph->p_va, ph->p_memsz);
memset((void *)ph->p_va, 0, ph->p_memsz);
if (ph->p_type == ELF_PROG_LOAD)
memcpy((void *)ph->p_va, (void *)binary + ph->p_offset, ph->p_filesz);
}

// load kernel pgdir back
lcr3(PADDR(kern_pgdir));

// set eip to program entry point
e->env_tf.tf_eip = elfhdr->e_entry;

// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
region_alloc(e, (void *)(USTACKTOP - PGSIZE), PGSIZE);
}

env_create() allocates an environment with env_alloc and call load_icode to load an ELF binary into it.

1
2
3
4
5
6
7
8
9
void
env_create(uint8_t *binary, enum EnvType type)
{
struct Env *e;

env_alloc(&e, 0);
load_icode(e, binary);
e->env_type = type;
}

Setting Up the IDT

Exercise 4 & Challenge

Question1

  1. The purpose of having an individual handler function for each exception/interrupt is to set a unique interrupt number for each exception/interrupt. So different exception/interrupt can be identified later.

  2. The result is caused by the DPL bit in the interrupt descriptor. In trap_init(), we set the DPLs of all exceptions except T_BRKPT and T_SYSCALL to 0.

    When a user program invokes int $14, the cpu detects that it tries to violate the privilege level. Therefore, general protect exception is produced.

    If the kernel actually allows softint’s int $14 instruction to invoke the kernel’s page fault handler, there will be security issues.

Challenge

Part B: Page Faults, Breakpoints Exceptions, and System Calls

Handling Page Faults

Exercise 5

The Breakpoint Exception

Exercise 6

Question2

  1. In trap_init(), we set the DPL of T_BRKPT to 3, so that the user can invoke the breakpoint exception and won’t trigger a general protection exception.

  2. The point of these mechanisms is to isolate user from invoking system exceptions by specifying the privilege bit in IDT.