MIT 6.828 学习笔记5 Lab3实验报告

Lab3 实验报告

Exercise 1

  • Modify mem_init() in kern/pmap.c to allocate and map the envs array.
// mem_int()
// 第一处
envs = (struct Env *) boot_alloc(NENV * sizeof(struct Env));
memset(pages, 0, NENV * sizeof(struct Env));

// 第二处
boot_map_region(kern_pgdir, UENVS, NENV * sizeof(struct Env), PADDR(envs), PTE_U | PTE_P);

这里仿照上一个实验可以比较轻松的写出来,注意,由于内核已用内存多出了 NENV 这一段,所以还需要修改 page_init 函数内的上限,参照 LAB 2 实验报告的代码,修改如下

// page_init()
size_t right_i = PGNUM(PADDR(envs + NENV));

Exercise 2

  • In the file env.c, finish coding the following functions.
// env_init()
env_free_list = envs;
struct Env *pre = envs;
for (int i = 1; i != NENV; ++i) {
    pre -> env_link = envs + i;
    pre = envs + i;
}

这里是初始化 envs 数组,然后插入到空闲链表中,由注释可知,需要确保分配 env 的时候是从 env[0] 开始分配的,也就是使用尾接法建链表,这和之前的 pages 不同

// env_setup_vm()
p->pp_ref++;
e->env_pgdir = page2kva(p);
e->env_pgdir[PDX(UENVS)] = kern_pgdir[PDX(UENVS)];
e->env_pgdir[PDX(UPAGES)] = kern_pgdir[PDX(UPAGES)];
e->env_pgdir[PDX(KSTACKTOP-KSTKSIZE)] = kern_pgdir[PDX(KSTACKTOP-KSTKSIZE)];
for (size_t i = PDX(KERNBASE); i != 1024; ++i) {
    e->env_pgdir[i] = kern_pgdir[i];
}

初始化用户地址空间中内核部分的虚拟内存并设置一级页表,可以利用之前的 kern_pgdir 将相同的部分复制过去,这里应该也可以直接使用 menset 函数,注意需要增加页面的引用

// region_alloc()
uintptr_t low = ROUNDDOWN((uintptr_t) va, PGSIZE);
uintptr_t high = ROUNDUP((uintptr_t) va + len, PGSIZE);
if (high > UTOP) {
    panic("allocation fails");
}
while (low < high) {
    struct PageInfo *pp = page_alloc(ALLOC_ZERO);
    if (pp == NULL) {
        panic("allocation falis");
    }
    pp->pp_ref++;
    int r = page_insert(e->env_pgdir, pp, (void *) low, PTE_P | PTE_U | PTE_W);
    if (r != 0) {
        panic("region_alloc: %e", r);
    }
    low += PGSIZE;
}

env 分配物理内存,接着映射到虚拟内存,注意需要对齐以及考虑边界情况

// load_icode()
// 第一处
struct Elf *elf = (struct Elf *) binary;
struct Proghdr *ph = (struct Proghdr *) (binary + elf->e_phoff);
struct Proghdr *eph = ph + elf->e_phnum;
lcr3(PADDR(e->env_pgdir));
while (ph < eph) {
    if (ph->p_type == ELF_PROG_LOAD) {
        region_alloc(e, (void *) ph->p_va, ph->p_memsz);
        memcpy((void *) ph->p_va, binary + ph->p_offset, ph->p_filesz);
    }
    ph++;
}
lcr3(PADDR(kern_pgdir));
e->env_tf.tf_eip = elf->e_entry;

// 第二处
region_alloc(e, (void *) (USTACKTOP - PGSIZE), PGSIZE);

elf 二进制文件读入用户地址空间中,仿照 bootloadermain.c 的做法,需要了解 elf 文件的结构,这个之前学习过,需要注意的是,为了读入用户地址空间,需要切换到用户的页表,还有记得设置 eip 指向入口处

// env_create()
struct Env *e;
int r = env_alloc(&e, 0);
if (r < 0) {
    panic("env_alloc: %e", r);
}
load_icode(e, binary);
e->env_type = type;

利用之前的函数创建 env ,对于 parent_id 由于分配页面的时候,把页面都清 0 了,所以这里不显式设置也行

// env_run()
if (curenv != NULL) {
    curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
curenv->env_status = ENV_RUNNING;
lcr3(PADDR(curenv->env_pgdir));
env_pop_tf(&(curenv->env_tf));

运行 env ,需要设置状态并切换页表,这里起到关键作用的函数是 env_pop_tf ,通过查看代码可以知道,它将 trapframe 里保存的信息 pop 到了对应的寄存器上,注意在 load_icode 函数的最后一句把 trapframe 中的 eip 设置到了二进制文件的入口,所以在执行完 env_pop_tf 后,下一条指令的地址将会是二进制文件的入口,因此达到了切换的目的

Exercise 3

  • Read Chapter 9, Exceptions and Interrupts in the 80386 Programmer’s Manual.

这个需要读一读,不然后面可能会遇到一些困难,不过我也没认真读完,主要是英文看起来好累……过程中到网上查一些相关的中文资料,收获还是挺大的

Exercise 4

  • Edit trapentry.S and trap.c and implement the features described above.
// trapentry.S
// 第一处
TRAPHANDLER_NOEC(divide_error, T_DIVIDE)
TRAPHANDLER_NOEC(debug_exception, T_DEBUG)
TRAPHANDLER_NOEC(non_maskable_interrupt, T_NMI)
TRAPHANDLER_NOEC(break_point, T_BRKPT)  // 注意这个地方千万不能用 breakpoint 作为函数名
TRAPHANDLER_NOEC(overflow, T_OFLOW)
TRAPHANDLER_NOEC(bounds_check, T_BOUND)
TRAPHANDLER_NOEC(illegal_opcode, T_ILLOP)
TRAPHANDLER_NOEC(device_not_available, T_DEVICE)
TRAPHANDLER(double_fault, T_DBLFLT)
TRAPHANDLER(invalid_task_switch_segment, T_TSS)
TRAPHANDLER(segment_not_present, T_SEGNP)
TRAPHANDLER(stack_fault, T_STACK)
TRAPHANDLER(general_protection_fault, T_GPFLT)
TRAPHANDLER(page_fault, T_PGFLT)
TRAPHANDLER_NOEC(floating_point_error, T_FPERR)
TRAPHANDLER(alignment_check, T_ALIGN)
TRAPHANDLER_NOEC(machine_check, T_MCHK)
TRAPHANDLER_NOEC(SIMD_floating_point_exception, T_SIMDERR)

使用上面给的宏定义来设置处理 trap 的函数,这里的函数名可以自己取,但是要注意, T_BRKPT 的函数名不能是 breakpoint ,因为 inc/x86.h 中含有同名函数,系统在调用时会调用 inc/x86.h 里的那个函数,关于是否需要 errorcode 可以查看 LEC 8handouts ,里面含有相关信息

// trapentry.S
// 第二处
_alltraps:
  pushl %ds
  pushl %es
  pushal
  movl $GD_KD, %eax
  movw %ax, %ds
  movw %ax, %es
  pushl %esp
  call trap
  popal
  popl %es
  popl %ds
  addl $8, %esp
  iret

根据之前所说,在引发异常时 CPU 会把 SS 寄存器到 EIP 寄存器压入栈中,如果需要 error code 的话也会压入,而在上面宏定义的函数中,trapno 也被压入了,所以这里只需要 push 余下的寄存器,注意需要根据 trapframe 的结构倒序压入, pushal 指令会按顺序将 eaxedi 压入栈中,call 之后的指令是当 call trap 失败时可以还原相关寄存器

// trap.c
// trap_init()
extern void divide_error();
extern void debug_exception();
extern void non_maskable_interrupt();
extern void break_point();
extern void overflow();
extern void bounds_check();
extern void illegal_opcode();
extern void device_not_available();
extern void double_fault();
extern void invalid_task_switch_segment();
extern void segment_not_present();
extern void stack_fault();
extern void general_protection_fault();
extern void page_fault();
extern void floating_point_error();
extern void alignment_check();
extern void machine_check();
extern void SIMD_floating_point_exception();
SETGATE(idt[T_DIVIDE], 0, GD_KT, divide_error, 0);
SETGATE(idt[T_DEBUG], 0, GD_KT, debug_exception, 0);
SETGATE(idt[T_NMI], 0, GD_KT, non_maskable_interrupt, 0);
SETGATE(idt[T_BRKPT], 0, GD_KT, break_point, 3);
SETGATE(idt[T_OFLOW], 0, GD_KT, overflow, 0);
SETGATE(idt[T_BOUND], 0, GD_KT, bounds_check, 0);
SETGATE(idt[T_ILLOP], 0, GD_KT, illegal_opcode, 0);
SETGATE(idt[T_DEVICE], 0, GD_KT, device_not_available, 0);
SETGATE(idt[T_DBLFLT], 0, GD_KT, double_fault, 0);
SETGATE(idt[T_TSS], 0, GD_KT, invalid_task_switch_segment, 0);
SETGATE(idt[T_SEGNP], 0, GD_KT, segment_not_present, 0);
SETGATE(idt[T_STACK], 0, GD_KT, stack_fault, 0);
SETGATE(idt[T_GPFLT], 0, GD_KT, general_protection_fault, 0);
SETGATE(idt[T_PGFLT], 0, GD_KT, page_fault, 0);
SETGATE(idt[T_FPERR], 0, GD_KT, floating_point_error, 0);
SETGATE(idt[T_ALIGN], 0, GD_KT, alignment_check, 0);
SETGATE(idt[T_MCHK], 0, GD_KT, machine_check, 0);
SETGATE(idt[T_SIMDERR], 0, GD_KT, SIMD_floating_point_exception, 0);

设置 IDT ,需要先声明函数,需要注意,由于 break_point 普通用户也可以使用,所以 DPL = 3SETGATE 的定义在 inc/mmu.h 之中

Questions

  • What is the purpose of having an individual handler function for each exception/interrupt? (i.e., if all exceptions/interrupts were delivered to the same handler, what feature that exists in the current implementation could not be provided?)

不同异常或中断的处理方式与结果不相同,例如是否可以恢复或从哪里恢复,条件也不一定相同,例如对权限等级与 errorcode 等参数的要求不同,所以需要拥有不同的处理函数

  • Did you have to do anything to make the user/softint program behave correctly? The grade script expects it to produce a general protection fault (trap 13), but softint’s code says int $14. Why should this produce interrupt vector 13? What happens if the kernel actually allows softint’s int $14 instruction to invoke the kernel’s page fault handler (which is interrupt vector 14)?

由于 trap 14IDT 内描述符的 DPL = 0 ,而此时 CPL = 3 即权限不足,所以执行这条指令会引发 trap 13

Exercise 5

  • Modify trap_dispatch() to dispatch page fault exceptions to page_fault_handler().
// trap.c
// trap_init()
switch (tf->tf_trapno) {
    case T_PGFLT:
        if (tf->tf_cs == 0) {
            panic("page fault in kernel");
        }
        page_fault_handler(tf);
        break;
    default:
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
}

根据 trapno 判断异常的类型,然后分配给相应函数

Exercise 6

  • Modify trap_dispatch() to make breakpoint exceptions invoke the kernel monitor.
// trap_init()
switch (tf->tf_trapno) {
    case T_PGFLT:
        if (tf->tf_cs == 0) {
            panic("page fault in kernel");
        }
        page_fault_handler(tf);
        break;
    case T_BRKPT:
        monitor(tf);
        break;
    default:
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
}

简单加上相应分支即可

Questions

  • The break point test case will either generate a break point exception or a general protection fault depending on how you initialized the break point entry in the IDT (i.e., your call to SETGATE from trap_init). Why? How do you need to set it up in order to get the breakpoint exception to work as specified above and what incorrect setup would cause it to trigger a general protection fault?

这个和上一个问题类似,如果设置 break pointDPL = 0 则会引发权限错误,由于这里设置的 DPL = 3 ,所以会引发断点

  • What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?

这个机制有效地防止了一些程序恶意任意调用指令,引发一些危险的错误,所以我认为这个粒度的权限机制时十分必要的

Exercise 7

  • Add a handler in the kernel for interrupt vector T_SYSCALL.
// trap.c
// trap_dispatch()
struct PushRegs regs = tf->tf_regs;
switch (tf->tf_trapno) {
    case T_PGFLT:
        if (tf->tf_cs == 0) {
            panic("page fault in kernel");
        }
        page_fault_handler(tf);
        break;
    case T_BRKPT:
        monitor(tf);
        break;
    case T_SYSCALL:
        tf->tf_regs.reg_eax = syscall(regs.reg_eax, regs.reg_edx, regs.reg_ecx, regs.reg_ebx, regs.reg_edi, regs.reg_esi);
        break;
    default:
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
}

还是和之前一样,分配相关的异常处理函数,这里需要将结果保存到 eax 寄存器中,记得在 IDT 中增加相应表项

// trapentry.S
TRAPHANDLER_NOEC(system_call, T_SYSCALL)

// trap.c
// trap_init()
extern void system_call();
SETGATE(idt[T_SYSCALL], 0, GD_KT, system_call, 3);

这里需要设置 syscallDPL = 3 ,接下来是 syscall.c 的部分

// syscall.c
// syscall()
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_NO_SYS;
}

只需简单地根据 syscallno 调用不同的函数即可

Exercise 8

  • Add the required code to the user library, then boot your kernel.
// libmain.c
// libmain()
thisenv = &envs[ENVX(sys_getenvid())];

由于之前没有设置 thisenv 的值,所以运行到 hello 的第二句时会出现错误,这里根据 id 取出索引,然后找到相应 env

Exercise 9

  • Change kern/trap.c to panic if a page fault happens in kernel mode. Implement user_mem_check in that same file.Change kern/syscall.c to sanity check arguments to system calls.
// trap.c
// page_fault_handler()
if ((tf->tf_cs & 3) == 0) {
    panic("page fault in kern");
}

由于 cs 寄存器的低 2 位的值与 CPL 相等,所以可以根据 cs 寄存器判断是否在内核态

// pmap.c
// user_mem_check()
pde_t *pgdir = env->env_pgdir;
uintptr_t high = ROUNDUP((uintptr_t) va + len, PGSIZE);
for (uintptr_t low = (uintptr_t) va; low < high; low = ROUNDUP(low + 1, PGSIZE)) {
    pte_t pte = *pgdir_walk(pgdir, (void *) low, false);
    if ((~pte & perm) || low >= ULIM) {
        user_mem_check_addr = low;
        return -E_FAULT;
    }
}
return 0;

通过页表找到相应的 pte ,然后判断是否具有权限,这里需要记录第一个出错的虚拟地址,所以一开始不能将 va 对齐,还有这种写法的 high 不能向下对齐,因为结果可能会比 low 还小

// syscall.c
// sys_cputs()
user_mem_assert(curenv, s, len, 0);

利用刚才的函数检查这一段地址

// kdebug.c
// debuginfo_eip()
// 第一处
if (user_mem_check(curenv, (void *) USTABDATA, sizeof(struct UserStabData), 0) < 0) {
    return -1;
}

// 第二处
if (user_mem_check(curenv, (void *) stabs, stab_end - stabs, 0) < 0 || user_mem_check(curenv, (void *) stabstr, stabstr_end - stabstr, 0) < 0) {
    return -1;
}

同理,检查相应的地址

关于最后一个问题,在调用 backtrace 后显示如下

K> backtrace
Stack backtrace:
 ebp efffff20 eip f0100c79 args 00000001 efffff38 f0198000 00000000 f0175840
     kern/monitor.c:217: monitor+276
 ebp efffff90 eip f01039a0 args f0198000 efffffbc 00000000 00000082 00000000
     kern/trap.c:192: trap+199
 ebp efffffb0 eip f0103aa8 args efffffbc 00000000 00000000 eebfdfd0 efffffdc
     kern/trapentry.S:80: <unknown>+0
 ebp eebfdfd0 eip 00800073 args 00000000 00000000 eebfdff0 00800049 00000000
     lib/libmain.c:26: libmain+58
 ebp eebfdff0 eip 00800031 args 00000000 00000000Incoming TRAP frame at 0xeffffea4
kernel panic at kern/trap.c:261: page fault in kern

这里引发页错误的原因是访问到了用户栈顶以上,可以看到, libmain 的两个参数都是 0 ,回想一下,在 lib/entry.S 中,系统在 USTACKTOP 执行了两次 pushl $0 ,所以当往上找第三个参数时就到达了上面的空内存,所以引发了页错误

Exercise 10

  • Boot your kernel, running user/evilhello. The environment should be destroyed, and the kernel should not panic.

这里只要完成了 Exercise 9 就可以通过这题,即 make grade 全部通过

时间: 2024-08-10 09:49:23

MIT 6.828 学习笔记5 Lab3实验报告的相关文章

MIT 6.828 学习笔记6 Lab4实验报告

Lab4实验报告 开始之前,为了弄懂 mpconfig 与 lapic,可能需要阅读 Intel processor manual 和 MP Specification,相关资源可以在课程中找到 Execrise 1 Implement mmio_map_region in kern/pmap.c. // mmio_map_region() uintptr_t ret = base; size = ROUNDUP(size, PGSIZE); base = base + size; if (ba

6.828学习笔记2 - QEMU和x86汇编语言

6.828的工具都装好了,在正式开始实验之前,要先熟悉每个工具的使用.对于我这种小白用户,需要熟悉的更多. 1.x86汇编语言 果然,课程首先让我熟悉汇编语言,并提供了两份参考文献.虽然我对汇编语言离熟悉还差十万八千里,不过好歹是用过的.我感到如果想要保持学习的兴趣,目前采取"如无必要,绝不深究"的态度非常重要.所以对汇编语言就先这样.我把参考文献下载到本地,然后跳到下一步. 2.模拟x86 课程给出了QEMU的作用.特性和辅助调试工具(GDB),不过这些细节我现在还统统不感兴趣.程序

CCNA学习笔记之一---VLAN实验组网

VLAN实验用的是Cisco Packet Tracer模拟器做的实验,实验拓扑如下图 1.Switch1配置如下 Switch>en                                    进入特权模式 Switch#conf t                                进入全局配置模式 Switch(config)#host Switch1                  给设备命名 Switch1(config)#no ip domain-lookup

[操作系统实验lab3]实验报告

[感受]: 这次操作系统实验感觉还是比较难的,除了因为助教老师笔误引发的2个错误外,还有一些关键性的理解的地方感觉还没有很到位,这些天一直在不断地消化.理解Lab3里的内容,到现在感觉比Lab2里面所蕴含的内容丰富很多,也算是有所收获,和大家分享一下我个人的一些看法与思路,如果有错误的话请指正. [关键函数理解]: 首先第一部分我觉得比较关键的是对于一些非常关键的函数的理解与把握,这些函数是我们本次实验的精华所在,虽然好几个实验都不需要我们自己实现,但是这些函数真的是非常厉害!有多厉害,呆会就知

lab3实验报告

1.实验的思路和具体过程 2.实验中关键代码 3.实验关键代码截图 4.实验运行结果截图 5.实验心得 C语言的重要地方就是结构体与指针应用,熟悉并精通指针,就能够写出许多结构化的简洁高效代码!随着实验的进行,我也越来越熟悉指针的运用了!

Linux服务器学习----tomcat 服务配置实验报告(一)

一.实验目的 1. 掌握 tomcat 服务的搭建 二.实验内容 1. 搭建一台缓存 tomcat 服务器 三.实验环境1. tomcat 服务器 centos7 对应主机 ip 为 10.10.64.1782. 客户机 win7 对应主机 ip 为 10.10.64.227 四.环境搭建在java官网下载Linux的java 1.配置Java的变量环境: tar -zxvf jdk-8u11-linux-x64.tar.gz #解压Java的包mv jdk1.8.0_11/ / #把java嗯

MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: The kernel

Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的:                           这张图仅仅展示了内存空间的一部分. 第一代PC处理器是16位字长的Intel 8088处理器,这类处理器只能访问1MB的地址空间,即0x00000000~0x000FFFFF.但是这1MB也不是用户都能利用到的,只有低640KB(0x00000000~0x00

《MIT 6.828 Lab1: Booting a PC》实验报告

实验内容 熟悉x86汇编语言.QEMU x86仿真器.PC开机引导流程 测试6.828 内核的启动加载器(boot loader) 研究6.828 内核的初始化模板(JOS) 实验题目 Exercise 1:阅读汇编语言资料 Exercise 1. Familiarize yourself with the assembly language materials available on the 6.828 reference page. You don't have to read them

MIT-6.828 Lab3实验报

Lab 3: User Environments实验报告 tags:mit-6.828 os 概述: 本文是lab3的实验报告,主要介绍JOS中的进程,异常处理,系统调用.内容上分为三部分: 用户环境建立,可以加载用户ELF文件并执行. 建立异常处理机制. 提供系统调用的能力. Part A: User Environments and Exception Handling 本实验指的用户环境和UNIX中的进程是一个概念,之所有没有使用进程是强调JOS的用户环境和UNIX进程将提供不同的接口.