Part A
Exercise 1
练习 1. 修改 `kern/pmap.c` 中的 `mem_init()` ,分配并映射 `envs` 数组。该数组由 `Env` 结构的 `NENV` 实例组成,分配方式与分配页面数组类似。与页面数组一样,支持 `envs` 的内存也应在 `UENVS`(定义于 `inc/mlayout.h` )处映射为用户只读,这样用户进程才能读取该数组。
你应该运行代码并确保 `check_kern_pgdir()` 成功。
[[lab3 - 翻译#^988084]]
解答
题目要求修改 mem_init
:
- 初始化
envs
- 映射
envs
初始化:pmap.c : 163行
映射 : pmap.c : 196行,
运行 make qemu
进行检查:
Excercise 2
env_init
// 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.
for(int 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
// 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.
e->env_pgdir = page2kva(p);
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);
// UVPT maps the env's own page table read-only.
p->pp_ref ++;
// Permissions: kernel R, user R
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
return 0;
}
不是很明白,为什么说,这个时候的pageinfo需要维护pp_ref?
区分好pgdir_walk和page_insert的功能区别:
pgdir_walk:访问页表,更为基础,只是为了修改页表
page_insert:将一个内存页映射到指定va,中间用到前者,因为要修改页表
env_create()
//
// 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 *newenv_store;
envid_t parent_id = 0;
int r = env_alloc(&newenv_store, parent_id);
if (r <= 0)
{
panic("env_create error: %e ", r);
}
// 加载二进制文件
load_icode(newenv_store, binary);
// 设置环境类型
newenv_store->env_type = type;
}
load_icode
注释翻译:
为用户进程设置初始程序二进制文件、堆栈和处理器标志。
只有在运行第一个用户模式环境之前的内核初始化过程中才会调用该函数。
该函数将 ELF 二进制映像中的所有可加载段加载到环境的用户内存中,从 ELF 程序头中指示的相应虚拟地址开始。
与此同时,它还会将这些段中在程序头中标记为映射但实际上并不存在于 ELF 文件中的部分(即程序的 bss 部分)清零。
除了 Boot Loader 还需要从磁盘读取代码外,所有这些都与我们的 Boot Loader 非常相似。
看看 boot/main.c 就会明白。
最后,该函数为程序的初始堆栈映射了一个页面。
load_icode 在遇到问题时会惊慌失措。
- load_icode 如何会失败? 给定的输入可能有什么问题?
提示 按照 ELF 程序段头指定的地址将每个程序段加载到虚拟内存中。
- 只能加载 ph->p_type == ELF_PROG_LOAD 的程序段。
- 每个程序段的虚拟地址可在 ph->p_va 中找到,其在内存中的大小可在 ph->p_memsz 中找到。
- 应将 ELF 二进制文件中从 "binary + ph->p_offset "开始的 ph->p_filesz 字节复制到虚拟地址 ph->p_va。
- 剩余的内存字节应清零。(ELF 头应该是 ph->p_filesz <= ph->p_memsz。)
使用上一个实验室的函数分配和映射页面。//
目前所有页面保护位都应为用户读/写。
ELF 程序段不一定是页面对齐的,但在本函数中可以假定没有两个程序段会接触同一个虚拟页面。//
你可能会发现 region_alloc 这样的函数很有用。//
如果能直接将数据移入 ELF 二进制文件中存储的虚拟地址,加载段就会简单得多。
那么,在执行该函数时,哪个页面目录应该有效呢?//
- 你还必须对程序的入口点做一些处理,以确保环境在那里开始执行。
请参阅下面的 env_run() 和 env_pop_tf())。
env_create()
-->env_alloc():分配env结构
-->env_setup_vm():创建映射,将栈映射至USTACKTOP
-->load_icode():加载程序,申请栈的空间
-->region_alloc()
运行:
从lab/kern/init.c来看,
在执行i386_init时,最会会通过env_run进入用户态,运行lab/user/hello.c中的umain
但是确发生了三重错误。
我们用 GDB 在 env_pop_tf() 函数设置断点,然后通过指令 si,单步调试,观察 iret 指令前后寄存器的变化。
一个疑问
int $0x30
究竟是谁调用的?
hello.c中没有这个代码啊?
Exercise 3
练习 3. 如果还没有,请阅读《80386 程序员手册》第 9 章 "异常和中断"[Chapter 9, Exceptions and Interrupts](https://pdos.csail.mit.edu/6.828/2018/readings/i386/c09.htm)(或《IA-32 开发人员手册》第 5 章 [IA-32 Developer's Manual](https://pdos.csail.mit.edu/6.828/2018/readings/ia32/IA32-3A.pdf))。
观察下TSS
Exercise 6
Exercise 7
疑问的答案
来看看hello.c的代码
hello.c调用了lib/print.c/cprintf,
- lib/print.c/cprintf调用了 lib/syscall.c/sys_cputs()
- lib/syscall.c/sys_cputs()调用了lib/syscall.c/syscall()
- lib/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
我们的任务是补充libmain函数,填充this指针。
by the way:了解下libmain和用户程序的关系
再次仔细地观察下用户地函数,hello.c中的主函数,是"main"吗?
是umain,他还引入了头文件inc/lib.h,里面有umain的声明。
然后,操作系统这边的lib/libmain.c中,也引入了这个头文件,这样一来,操作系统的libmain函数就可以调用用户的main函数了。
Exercise 9 页错误
任务内容:
1. 修改`kern/trap.c`,当页错误发生在内核态时panic。
~~~ad-note
检查`tf_cs`的低位字节可以判断fault发生在**用户态**还是**内核态**
~~~
2. 读`kern/pmap.c` 中的 `user_mem_assert` 并实现 `user_mem_check`
3. 修改 `kern/syscall.c` 对系统调用的参数进行正确性检查
4. 启动你的kernel,运行 `user/buggyhello`。 environment应当被销毁, kernel 不应当 'panic'。你应该会看到:
~~~txt
[00001000] user_mem_check assertion failure for va 00000001
[00001000] free env 00001000
Destroyed the only environment - nothing more to do!
~~~
5. 最终,修改 `kern/kdebug.c` 中的 `debuginfo_eip`,在 `usd, stabs, stabstr` 上调用 'user_mem_check'。如果你现在运行 `user/breakpoint` ,你应能够从 kernel monitor 运行 `backtrace`,并看见backtrace 在kernel panics 之前,随着一个page fault回溯到 `lib/libmain.c` 。
是什么导致了page fualt?
你不需要修复他,但是你要明白它为何发生。
页错误处理函数
补充页错误处理函数
实现user_mem_check
先看看 user_mem_assert
是怎么用 user_mem_check
的:
然后实现 user_mem_check
:
- 检查
va
开始之后大小为len
的内存空间范围,确认其权限是否为perm
- 除此之外的限制:
- 低于
ULIM
- 该页具备权限
- 低于
- 如果发生错误,则将
user_mem_check_addr
设置为第一个有问题的页 - 如果没有问题,则返回 0, 否则返回
-E_FAULT
修改 syscall.c
对 syscall 的输入做检查:
目前 syscall 只有四种调用:
只有 sys_cputs
中有指针,有访问内存的需求。因此只需要检查 sys_cputs
.
最后 make grade
运行breakpoint
发生了什么?
首先看看 user/breakpoint.c
的代码:
内容就是直接int3中断
那么,cpu 会将控制流跳转到中断向量表,经过 kern/trapentry.S
中的 trap_dispatch
到达 kern/monitor.c/monitor
,最后调用 print_trapframe
将 trapframe
的内容打印了除了