LoginSignup
3
1

More than 3 years have passed since last update.

MIT 6.828 labs walkthroughs: Lab 3 User Environments

Last updated at Posted at 2019-08-16

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.

  1. kern_pgdir can be used as a template here since the VA space of all envs is identical above UTOP except UVPT. What we need to do is to copy it to env_pgdir.
  2. The pp_ref of env_pgdir needs to be increased.
  3. Since we allocate a new page for the page directory and env_pgdir should represent the kernel virtual address of it, the page2kva() function in kern/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.

fig9-3.gif

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.

fig9-4.gif

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:

  1. 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.

  2. 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.

i77PJ.png

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);
                }
3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1