Note
For GCC 7 or later, after switching to lab3
branch an error like kernel panic at kern/pmap.c:147: PADDR called with invalid kva 00000000
will occur. This is a bug caused by the linker script, modify kern/kernel.ld
as follow will fix it.
--- a/kern/kernel.ld
+++ b/kern/kernel.ld
@@ -50,6 +50,7 @@ SECTIONS
.bss : {
PROVIDE(edata = .);
*(.bss)
+ *(COMMON)
PROVIDE(end = .);
BYTE(0)
}
The details are here. If you are not interested, just skip this part.
What leads to the bug is the magic symbol end
is not set properly. The .bss
section, as the output of objdump -h obj/kern/kernel
, starts from 0xf018e100
and its size is 0x00000f14
, ranges from 0xf018e100
to 0xf018f014
. But the symbol end
has been set to 0xf018f000
, which is inside the .bss
section. As an uninitialized global variable, kern_pgdir
will be linked to the .bss
section. And the output of objdump -t obj/kern/kernel | grep kern_pgdir
indicates the address of kern_pgdir
is 0xf018f00c
, right after end
. What makes it even worse is that 0xf018f000
happens to be an address aligned to PGSIZE
. Unlike lab2, now the ROUNDUP()
won't save the kernel anymore. In this coincidence, the bug occurs silently.
Here is a step-by-step code analyze:
// kern_pgdir == NULL, &kern_pgdir == 0xf018f00c
// in mem_init()
kern_pgdir = (pde_t *) boot_alloc(PGSIZE); // call boot_alloc(4096)
// in boot_alloc()
if (!nextfree) {
extern char end[]; // end == 0xf018f000
nextfree = ROUNDUP((char *) end, PGSIZE); // nextfree = 0xf018f000
}
// ...
// 0xf018f000 is returned to to mem_init()
kern_pgdir = (pde_t *) boot_alloc(PGSIZE); // kern_pgdir = 0xf018f000
memset(kern_pgdir, 0, PGSIZE); // Damn! 0xf018f000 to 0xf0190000 are set to 0, including 0xf018f00c
// Now kern_pgdir == 0
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P; // call PADDR(0), panic!
Here comes the question, why end
is not set properly? The linker script kern/kernel.ld
shows how end
is set:
.bss : {
PROVIDE(edata = .);
*(.bss)
PROVIDE(end = .);
BYTE(0)
}
The procedure is to link all .bss
section of kernel objects to kernel, then set the end
symbol. However, the output objdump -t obj/kern/pmap.o | grep kern_pgdir
shows kern_pgdir
is actually in COMMON
section. In my personal opinion, the COMMON
will be linked implicitly to .bss
after end
is set. To solve the problem, COMMON
should be linked explicitly to .bss
before end
is set. That's why we need to modify the linker script as above.
Addresses above may vary from environments, but why the bug occurs should be the same reason.
Exercise 1
Pretty simple, just allocate space for envs
and map it as we did for pages
in Lab 2.
// Make 'envs' point to an array of size 'NENV' of 'struct Env'.
envs = (struct Env *)boot_alloc(NENV * sizeof(struct Env));
memset(envs, 0, NENV * sizeof(struct Env));
// ...
// Map the 'envs' array read-only by the user at linear address UENVS
boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U | PTE_P);
Now check_kern_pgdir() succeeded!
should be printed.
Exercise 2
This exercise may be a bit difficult, but the first one env_init()
is relatively easy. To ensure environments in env_free_list
are in the same order as they are in the envs
array, they are inserted to the free list from tail to head here.
void
env_init(void)
{
int i;
// Set up envs array
for (i = NENV - 1; i >= 0; i--) {
envs[i].env_status = ENV_FREE;
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();
}
env_setup_vm()
provides many hints. These can be summarized into 3 points.
-
kern_pgdir
can be used as a template here since the VA space of all envs is identical aboveUTOP
exceptUVPT
. What we need to do is to copy it toenv_pgdir
. - The
pp_ref
ofenv_pgdir
needs to be increased. - Since we allocate a new page for the page directory and
env_pgdir
should represent the kernel virtual address of it, thepage2kva()
function inkern/pmap.h
is handy.
Here is my code:
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.
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()
requires us to allocate pages for env
and map them in the environment's address space. The page_alloc()
and page_insert()
we implemented in Lab 2 are helpful here. Follow the comment we can write some code like this. And don't forget to handle errors.
static void
region_alloc(struct Env *e, void *va, size_t len)
{
void *begin = ROUNDDOWN(va, PGSIZE);
void *end = ROUNDUP(va + len, PGSIZE);
if ((uint32_t)end > UTOP) {
panic("region_alloc: cannot allocate pages over UTOP");
}
while (begin < end) {
struct PageInfo *pp;
if ((pp = page_alloc(0)) == NULL) {
panic("region_alloc: out of free memory");
}
int r = page_insert(e->env_pgdir, pp, begin, PTE_U | PTE_W);
if (r != 0) {
panic("region_alloc: %e", r);
}
begin += PGSIZE;
}
}
Now it comes to load_icode()
, the most difficult one in this exercise. This function requires us to read the ELF header and all program headers, then map the right section to the right place in the environment's address space. The Executable and Linkable Format page on Wikipedia will be helpful in processing the ELF file.
The comment says it is much simpler to move data directly into the virtual addresses stored in the ELF binary. So we should switch to the environment's address space before loading the program, and this can be done with lcr3()
. It also says we need to set the EIP points to the entry point. The env_tf
saves the register values, and EIP can be set by setting env_tf.tf_eip
.
The procedure of loading an ELF file is very similar to loading the kernel in bootloader. bootmain()
in boot/main.c
is a good reference. Here is my code:
static void
load_icode(struct Env *e, uint8_t *binary)
{
struct Elf *elfhdr = (struct Elf *)binary;
if (elfhdr->e_magic != ELF_MAGIC) {
panic("load_icode: invalid elf header");
}
// switch to env's address space
lcr3(PADDR(e->env_pgdir));
// load each program segment
struct Proghdr *ph = (struct Proghdr *)(binary + elfhdr->e_phoff);
struct Proghdr *eph = ph + elfhdr->e_phnum;
for (; ph < eph; ph++) {
if (ph->p_type == ELF_PROG_LOAD) {
if (ph->p_filesz > ph->p_memsz) {
panic("load_icode: invalid program header (p_filesz > p_memsz)");
}
region_alloc(e, (uint8_t *)ph->p_va, ph->p_memsz);
memcpy((uint8_t *)ph->p_va, binary + ph->p_offset, ph->p_filesz);
memset((uint8_t *)ph->p_va + ph->p_filesz, 0, ph->p_memsz - ph->p_filesz);
}
}
// make eip points to the 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);
// switch back to kernel address space
lcr3(PADDR(kern_pgdir));
}
env_create()
is quite easy, just follow what the comment askes to do.
void
env_create(uint8_t *binary, enum EnvType type)
{
struct Env* env;
int r;
if ((r = env_alloc(&env, 0)) != 0) {
panic("env_create: %e", r);
}
load_icode(env, binary);
env->env_type = type;
}
The last one, env_run()
is also an easy one. The comment even gives detailed steps and don't forget to check if curenv
is NULL
.
void
env_run(struct Env *e)
{
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);
}
Now you may think QEMU will print something is successfully executed. But it only gives a triple fault. Continue reading the lab instruction you will find what really indicates the success was that GDB shows the triple fault is caused by int $0x30
instruction.
Exercise 4
This exercise is the hardest one so far. Before doing this exercise, we need to know how x86 processors handle exceptions and interrupts. That's why Exercise 3 asks us to read Chapter 9 of Intel 80386 Reference Manual.
To handle exceptions and interrupts, we need some interrupt handlers, and set up an Interrupt Descriptor Table, also known as IDT. IDT associates each interrupt vector with its handler. The entries are called gates and their format is shown in Figure 9-3.
As we can see in Figure 9-4, the OFFSET
of a gate is the address of the interrupt handler and the SELECTOR
is the index (aka segment selector) of an entry in Global Descriptor Table.
When the processor performs a call to the interrupt handler, it will save the current state of specific registers as well as an optional error code on the new stack like this: (Here only the situation with privilege transition is considered)
15 0
+---------------------+
| 0 | SS |
| ESP |
| EFLAGS |
| 0 | CS |
| EIP |
| Error Code(Optional)| <--- ESP after transfer to handler
+---------------------+
Back to the exercise itself, the first thing we need to do is to write some interrupt handlers. The macros TRAPHANDLER
and TRAPHANDLER_NOEC
in kern/trapentry.S
can be used to generate these handlers. The former is for interrupts with an error code, the latter is for those without an error code. Both of them will push the interrupt number onto the stack and jump to _alltraps
. Also, TRAPHANDLER_NOEC
pushes an extra 0 onto the position of error code to fit the stack to struct TrapFrame
.
The interrupt numbers are defined in inc/trap.h
. Section 9.10 of Intel 80386 Reference Manual can be a reference to determine whether an interrupt causes an error code, but it is incomplete. The following is my summary of x86 exceptions and interrupts used here according to Section 6.15 of Intel® 64 and IA-32 Architectures Software Developer's Manual: Volume 3.
Interrupt No. | Name | Error Code? |
---|---|---|
0 | Divide Error Exception (#DE) | No |
1 | Debug Exception (#DB) | No |
2 | NMI Interrupt | No |
3 | Breakpoint Exception (#BP) | No |
4 | Overflow Exception (#OF) | No |
5 | BOUND Range Exceeded Exception (#BR) | No |
6 | Invalid Opcode Exception (#UD) | No |
7 | Device Not Available Exception (#NM) | No |
8 | Double Fault Exception (#DF) | Yes |
10 | Invalid TSS Exception (#TS) | Yes |
11 | Segment Not Present (#NP) | Yes |
12 | Stack Fault Exception (#SS) | Yes |
13 | General Protection Exception (#GP) | Yes |
14 | Page-Fault Exception (#PF) | Yes |
16 | x87 FPU Floating-Point Error (#MF) | No |
17 | Alignment Check Exception (#AC) | Yes |
18 | Machine-Check Exception (#MC) | No |
19 | SIMD Floating-Point Exception (#XM) | No |
Now we can define interrupt handlers like this in kern/trapentry.S
.
/*
* Lab 3: Your code here for generating entry points for the different traps.
*/
TRAPHANDLER_NOEC(th_divide, T_DIVIDE)
TRAPHANDLER_NOEC(th_debug, T_DEBUG)
TRAPHANDLER_NOEC(th_nmi, T_NMI)
TRAPHANDLER_NOEC(th_brkpt, T_BRKPT)
TRAPHANDLER_NOEC(th_oflow, T_OFLOW)
TRAPHANDLER_NOEC(th_bound, T_BOUND)
TRAPHANDLER_NOEC(th_illop, T_ILLOP)
TRAPHANDLER_NOEC(th_device, T_DEVICE)
TRAPHANDLER(th_dblflt, T_DBLFLT)
TRAPHANDLER(th_tss, T_TSS)
TRAPHANDLER(th_segnp, T_SEGNP)
TRAPHANDLER(th_stack, T_STACK)
TRAPHANDLER(th_gpflt, T_GPFLT)
TRAPHANDLER(th_pgflt, T_PGFLT)
TRAPHANDLER_NOEC(th_fperr, T_FPERR)
TRAPHANDLER(th_align, T_ALIGN)
TRAPHANDLER_NOEC(th_mchk, T_MCHK)
TRAPHANDLER_NOEC(th_simderr, T_SIMDERR)
Proceed to _alltraps
,it wasn't hard as detailed instructions are given. Beware that stack grows from high address to low address, but a struct is from low to high. So we need to push registers in reverse order as they are in the struct TrapFrame
. Remember some of the values have already been pushed.
Another need to notice here is an immediate value cannot be moved directly to a segment register like ds
or es
. It should be moved to a general-purpose register like ax
first, then be moved to the segment register. Here is my code:
/*
* Lab 3: Your code here for _alltraps
*/
_alltraps:
pushl %ds
pushl %es
pushal
movw $GD_KD, %ax
movw %ax, %ds
movw %ax, %es
pushl %esp
call trap
The interrupt handlers in trapentry.S
are implemented now, next we are going to set up the IDT in trap_init()
in kern/trap.c
. As it says, macro function SETGATE()
defined in inc/mmu.h
is helpful here to set up the IDT. Maybe offset
is the only confusing argument here, as we analyzed above, it is the address of interrupt handler.
To get the address of these interrupt handlers, we need to declare these functions with void NAME()
as the comment of trapentry.S
indicates.
The kernel code segment selector in GDT is GD_KT
defined in inc/memlayout.h
. Here all gates are interrupt gates and we assume all gates can only be invoked in Ring 0.
void
trap_init(void)
{
extern struct Segdesc gdt[];
void th_divide();
void th_debug();
void th_nmi();
void th_brkpt();
void th_oflow();
void th_bound();
void th_illop();
void th_device();
void th_dblflt();
void th_tss();
void th_segnp();
void th_stack();
void th_gpflt();
void th_pgflt();
void th_fperr();
void th_align();
void th_mchk();
void th_simderr();
SETGATE(idt[T_DIVIDE], 0, GD_KT, &th_divide, 0);
SETGATE(idt[T_DEBUG], 0, GD_KT, &th_debug, 0);
SETGATE(idt[T_NMI], 0, GD_KT, &th_nmi, 0);
SETGATE(idt[T_BRKPT], 0, GD_KT, &th_brkpt, 0);
SETGATE(idt[T_OFLOW], 0, GD_KT, &th_oflow, 0);
SETGATE(idt[T_BOUND], 0, GD_KT, &th_bound, 0);
SETGATE(idt[T_ILLOP], 0, GD_KT, &th_illop, 0);
SETGATE(idt[T_DEVICE], 0, GD_KT, &th_device, 0);
SETGATE(idt[T_DBLFLT], 0, GD_KT, &th_dblflt, 0);
SETGATE(idt[T_TSS], 0, GD_KT, &th_tss, 0);
SETGATE(idt[T_SEGNP], 0, GD_KT, &th_segnp, 0);
SETGATE(idt[T_STACK], 0, GD_KT, &th_stack, 0);
SETGATE(idt[T_GPFLT], 0, GD_KT, &th_gpflt, 0);
SETGATE(idt[T_PGFLT], 0, GD_KT, &th_pgflt, 0);
SETGATE(idt[T_FPERR], 0, GD_KT, &th_fperr, 0);
SETGATE(idt[T_ALIGN], 0, GD_KT, &th_align, 0);
SETGATE(idt[T_MCHK], 0, GD_KT, &th_mchk, 0);
SETGATE(idt[T_SIMDERR], 0, GD_KT, &th_simderr, 0);
// Per-CPU setup
trap_init_percpu();
}
Finally, we completed this exercise. Now run make grade
we shall pass all tests in Part A like this:
divzero: OK (1.4s)
softint: OK (1.2s)
badsegment: OK (1.2s)
Part A score: 30/30
Question 1
There are 2 purposes:
-
To push the corresponding error code onto the stack. This is used for the codes going to handle it further like
trap_dispatch()
to distinguish the interrupts. -
To provide permission control or isolation. For each standalone interrupt handler, we can define it whether can be triggered by a user program or not. By putting such limits on interrupt handlers, we can ensure user programs would not interfere with the kernel, corrupt the kernel or even take control of the whole computer.
Question 2
I didn't have to do anything extra to do. It triggers an Interrupt 13 because only the kernel running in Ring 0 can trigger the handler of page fault as we defined above. This meets the "Executing the INT n instruction when the CPL is greater than the DPL of the referenced interrupt, trap, or task gate." condition, so the processor triggers a General Protection Exception (Interrupt 13)
If we allow a page fault to be triggered by a user program like softint
. It can manipulate virtual memory and may cause serious security issues.
Exercise 5
Pretty easy, simply adding a switch statement will complete this.
static void
trap_dispatch(struct Trapframe *tf)
{
// Handle processor exceptions.
switch (tf->tf_trapno) {
case T_PGFLT:
page_fault_handler(tf);
return;
default:
// Unexpected trap: The user process or the kernel has a bug.
print_trapframe(tf);
if (tf->tf_cs == GD_KT)
panic("unhandled trap in kernel");
else {
env_destroy(curenv);
return;
}
}
}
Now faultread
, faultreadkernel
, faultwrite
, and faultwritekernel
tests in make grade
will be passed.
Exercise 6
It's another easy one. The kernel monitor can be invoked by calling monitor()
in kern/monitor.c
. So we just need to add one case to trap_dispatch()
.
case T_BRKPT:
monitor(tf);
return;
But it cannot pass the breakpoint
test because we only allow a breakpoint exception to be triggered by the kernel. To allow being triggered by a user program, we need to change this line in trap_init()
in kern/trap.c
SETGATE(idt[T_BRKPT], 0, GD_KT, &th_brkpt, 3);
to:
SETGATE(idt[T_BRKPT], 0, GD_KT, &th_brkpt, 3);
Now breakpoint
test in make grade
will be passed.
Question 3
This is a similar question to previous ones. When we set up the IDT, or, to be more specific, the gates in IDT, there is a DPL parameter indicates the maximum privilege level can the interrupt be triggered. According to Page 6-37 of Intel® 64 and IA-32 Architectures Software Developer's Manual: Volume 3, these incorrect setups would cause the processor to trigger a general protection fault:
- Accessing a gate that contains a null segment selector.
- Executing the INT n instruction when the CPL is greater than the DPL of the referenced interrupt, trap, or task gate.
- The segment selector in a call, interrupt, or trap gate does not point to a code segment.
Question 4
These mechanisms restrict what user programs can do, therefore, they protect the kernel from being interfered or corrupted. They also isolate the kernel from user programs and it enhances the system's robustness.
Exercise 7
There are two syscall.c
, kern/syscall.c
and lib/syscall.c
. The former is the kernel syscall trap handler,and the latter is the syscall interface for user programs. What we are going to implement here is the former.
But first we need to take a look at the lib/syscall.c
to understand how a syscall is called. Here the syscall()
function uses inline assembly. GCC-Inline-Assembly-HOWTO is a good reference for understanding inline assembly.
Briefly, the syscall number will be moved to %eax
and the 5 parameters will be moved to %edx
, %ecx
, %ebx
, %edi
, and %esi
in sequence. Then it executes an int $0x30
instruction to trigger the trap handler, the handler will handle it and put the return value in %eax
and give control back to the user program.
Now let's finish the code. First, we need to add some code to set up handler and IDT for the syscall trap.
in kern/trapentry.S
@@ -64,3 +64,4 @@ TRAPHANDLER_NOEC(th_fperr, T_FPERR)
TRAPHANDLER(th_align, T_ALIGN)
TRAPHANDLER_NOEC(th_mchk, T_MCHK)
TRAPHANDLER_NOEC(th_simderr, T_SIMDERR)
+TRAPHANDLER_NOEC(th_syscall, T_SYSCALL)
in kern/trap.c:trap_init()
@@ -82,3 +82,4 @@ trap_init(void)
void th_align();
void th_mchk();
void th_simderr();
+ void th_syscall();
@@ -101,3 +102,4 @@ trap_init(void)
SETGATE(idt[T_ALIGN], 0, GD_KT, &th_align, 0);
SETGATE(idt[T_MCHK], 0, GD_KT, &th_mchk, 0);
SETGATE(idt[T_SIMDERR], 0, GD_KT, &th_simderr, 0);
+ SETGATE(idt[T_SYSCALL], 0, GD_KT, &th_syscall, 3);
Then dispatch syscall traps in trap_dispatch()
to syscall()
. Here, it is the one in kern/syscall.c
. It should take register values which have been pushed on to the stack from TrapFrame
as parameters and write the return value to the eax
in TrapFrame
.
case T_SYSCALL:
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;
Finally, we need to implement syscall()
in kern/syscall.c
. This isn't a hard one.
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.
switch (syscallno) {
case SYS_cputs:
sys_cputs((const char *)a1, a2);
return 0;
case SYS_cgetc:
return sys_cgetc();
case SYS_getenvid:
return sys_getenvid();
case SYS_env_destroy:
return sys_env_destroy(a1);
default:
return -E_INVAL;
}
}
Now the user/hello
program under your kernel will print "hello, world" on the console and then cause a page fault in user mode and testbss
test in make grade
will be passed.
Exercise 8
Another easy one. Here we need to set thisenv
points to current Env
structure in envs[]
, so we need to know the index. The hint on lab page asks us to look in inc/env.h
and use sys_getenvid()
. Take a look at ENVX()
in inc/env.h
and you will find only a simple line of code will solve this exercise.
thisenv = &envs[ENVX(sys_getenvid())];
Now hello
test in make grade
will be passed.
Exercise 9 & 10
According to the instruction, first, kern/trap.c
should be changed to panic if a page fault happens in kernel mode.
And it hints that we can determine what mode it is by the low bits of tf_cs
. As the figure below shows, it was the lowest 2 bits indicates the privilege level.
So something like this should be added to page_fault_handler()
in kern/trap.c
.
// Handle kernel-mode page faults.
if ((tf->tf_cs & 0x3) == 0) {
panic("page_fault_handler: page fault in kernel mode");
}
Next, we are going to implement user_mem_check()
in kern/pmap.c
. To handle non-page-aligned va
and len
, we can round them in the same way as region_alloc()
in kern/env.c
we implemented in Exercise 2. And pgdir_walk()
implemented in Lab 2 can be used to get the PTE for va
.
There is a corner case often be neglected. (I neglected this, too). As we are asked to set user_mem_check_addr
to the first erroneous virtual address, in most cases, it is the starting address of a page, except the first page since va
may not be a starting address of a page. This case should be handled properly or we will fail the buggyhello
test. Here is my code:
int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
// LAB 3: Your code here.
uint32_t addr = (uint32_t)va;
uint32_t begin = ROUNDDOWN(addr, PGSIZE);
uint32_t end = ROUNDUP(addr + len, PGSIZE);
while (begin < end) {
pte_t *pte = pgdir_walk(env->env_pgdir, (void *)begin, 0);
// Thanks @trace-andreason for telling me the mistake on next line
if (begin >= ULIM || pte == NULL || !(*pte & PTE_P) || (*pte & perm) != perm) {
user_mem_check_addr = (begin < addr) ? addr : begin;
return -E_FAULT;
}
begin += PGSIZE;
}
return 0;
}
And we need to add memory check in sys_cputs()
in kern/syscall.c
.
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.
user_mem_assert(curenv, s, len, PTE_U);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}
Finally, we are asked to add memory check on usd
, stabs
, and stabstr
in debuginfo_eip
in kern/kdebug.c
. Read the definition of struct UserStabData
will give you the idea of how to perform the check.
const struct UserStabData *usd = (const struct UserStabData *) USTABDATA;
// Make sure this memory is valid.
// Return -1 if it is not. Hint: Call user_mem_check.
if (user_mem_check(curenv, usd, sizeof(struct UserStabData), PTE_U) < 0) {
return -1;
}
stabs = usd->stabs;
stab_end = usd->stab_end;
stabstr = usd->stabstr;
stabstr_end = usd->stabstr_end;
// Make sure the STABS and string table memory is valid.
if (user_mem_check(curenv, stabs, stab_end - stabs, PTE_U) < 0) {
return -1;
}
if (user_mem_check(curenv, stabstr, stabstr_end - stabstr, PTE_U) < 0) {
return -1;
}
Now run make grade
we shall pass all the tests, including the evilhello
in Exercise 10 like this.
divzero: OK (1.5s)
softint: OK (1.2s)
badsegment: OK (1.2s)
Part A score: 30/30
faultread: OK (1.4s)
faultreadkernel: OK (1.2s)
faultwrite: OK (1.8s)
faultwritekernel: OK (1.2s)
breakpoint: OK (1.9s)
testbss: OK (2.2s)
hello: OK (1.2s)
buggyhello: OK (1.9s)
buggyhello2: OK (1.2s)
evilhello: OK (1.8s)
Part B score: 50/50
Score: 80/80
But Exercise 9 is not over yet. We need to run user/breakpoint
then run backtrace
in kernel monitor to answer the last question.
I examined what the saved %ebp
at 0xeebfdfc0
points to when it reaches lib/libmain.c
with GDB. It points to 0xeebfdff0
. Only 3 dwords after this address are below USTACKTOP
, but mon_backtrace()
will print 6 dwords. And according to inc/memlayout.h
, the page above USTACKTOP
is an empty page. When it trying to access the 4th one at 0xeebfe000
, which is above USTACKTOP
, a page fault happens.
To find out why the stack frame at 0xeebfdff4
is so unusual, I looked through lib/entry.S
and found the answer. The _start
function here will create this stack, as it only pushes 2 dwords for argc
and argv
onto the stack. So it only has 2 arguments, not 5 as normal. A user stack structure is like this:
4 0
+--------------+ USTACKTOP
_start | arg 1 (argv) |
| arg 0 (argc) |
| saved %eip |
+--------------+
| saved %ebp | <---+
| saved %esi | |
stack frames | arg 4 | |
like libmain | arg 3 | |
| arg 2 | |
| arg 1 | |
| arg 0 | |
| saved %eip | |
+--------------+ |
%ebp ---> | saved %ebp | ----+
| something |
%esp ---> +--------------+
To fix this isn't difficult although we are not asked to do so. Simply modify mon_backtrace()
in kern/monitor.c
like this will fix it.
@@ -63,8 +63,15 @@ mon_backtrace(int argc, char **argv, struct Trapframe *tf)
cprintf("Stack backtrace:\n");
while (ebp != 0){
uint32_t eip = ebp[1];
- cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n", ebp, eip, ebp[2], ebp[3], ebp[4], ebp[5], ebp[6]);
+
+ if ((uint32_t)ebp == 0xeebfdff0) { // handle the stack created by _start
+ cprintf(" ebp %08x eip %08x args %08x %08x\n", ebp, eip, ebp[2], ebp[3]);
+ } else {
+ cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n", ebp, eip, ebp[2], ebp[3], ebp[4], ebp[5], ebp[6]);
+ }
+
struct Eipdebuginfo info;
+
if (debuginfo_eip(eip, &info) == 0) {
cprintf(" %s:%d: %.*s+%d\n", info.eip_file, info.eip_line, info.eip_fn_namelen, info.eip_fn_name, eip - info.eip_fn_addr);
}