First, we need to handle page faults. To do this, we have to detect the type of traps. Go to inc/trap.h
to find the attribute:
struct Trapframe {
...
uint32_t tf_trapno;
...
}
And also in kern/trap.c
:
void
print_trapframe(struct Trapframe *tf)
{
...
cprintf(" trap 0x%08x %s\n", tf->tf_trapno, trapname(tf->tf_trapno));
...
}
So we will use tf->tf_trapno
to identify the type of trap. This function will jump to different code blocks depending on tf->tf_trapno == T_PGFLT
or not. Here we use the switch block to be consistent with other codes such as cga_putc()
in kern/console.c
and check_kern_pgdir()
in kern/pmap.c
. Here is the code:
static void
trap_dispatch(struct Trapframe *tf)
{
int32_t ret;
// Handle processor exceptions.
// LAB 3: Your code here.
switch (tf->tf_trapno) {
case (T_PGFLT):
page_fault_handler(tf);
break;
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;
}
}
}
We have also to handle the breakpoint exception, and we will abuse this exception slightly by turning into a primitive pseudo-system call that any user environment can use to invoke the JOS kernel monitor (note that the function to invoke monitor is in kern/monitor.c
). This is quite similiar to what we did to set up the page fault:
static void
trap_dispatch(struct Trapframe *tf)
{
int32_t ret;
// Handle processor exceptions.
// LAB 3: Your code here.
switch (tf->tf_trapno) {
...
case (T_BRKPT):
monitor(tf);
break;
...
}
}
3. 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?
Because there is the Descriptor Privilege Level (DPL) to execute the instructions. The break point test case will genereate a break point exception when the DPL is 3 (user mode) and generate a general protection fault when the DPL is 0 (kernel mode).
4. What do you think is the point of these mechanisms, particularly in light of what the user/softint
test program does?
It depends on the DPL you have initialzed in the IDT. To execute a program without general protection fault, the CPL (Current Privilege Level) must be prior to or equal to DPL. If the user program tries to execute an instruction in the kernel mode, it will generate a general protection fault.
Since we have already implemented the handler for interrupt vector T_SYSCALL
in kern/trapentry.S
and kern/trap.c
(see Lab3, Part A), here we only change trap_dispatch()
.
Before changing syscall
, we have to set up another interrupt vector in kern/trap.c
:
extern void handler_syscall();
...
void
trap_init(void)
{
extern struct Segdesc gdt[];
...
SETGATE(idt[T_SYSCALL], false, GD_KT, handler_syscall, 3);
// Per-CPU setup
trap_init_percpu();
}
And in kern/trapentry.S
:
/*
* Lab 3: Your code here for generating entry points for the different traps.
*/
TRAPHANDLER_NOEC(handler_syscall, T_SYSCALL)
Note what has been declared in inc/syscall.h
:
/* system call numbers */
enum {
SYS_cputs = 0,
SYS_cgetc,
SYS_getenvid,
SYS_env_destroy,
NSYSCALLS
};
And then we call the function corresponding to the 'syscall' parameter according to these system call numbers. Note that sys_cputs
has parameters, so we need find out the way to handle them, take a look at lib/syscall.h
:
static inline int32_t
syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
int32_t ret;
// Generic system call: pass system call number in AX,
// up to five parameters in DX, CX, BX, DI, SI.
// Interrupt kernel with T_SYSCALL.
//
// The "volatile" tells the assembler not to optimize
// this instruction away just because we don't use the
// return value.
//
// The last clause tells the assembler that this can
// potentially change the condition codes and arbitrary
// memory locations.
asm volatile("int %1\n"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a1),
"c" (a2),
"b" (a3),
"D" (a4),
"S" (a5)
: "cc", "memory");
...
}
Since the ret
, T_SYSCALL
and num
are reserved parameters, the other five parameters will be in consideration. So we pass a1
and a2
as the first two parameters.
Here is the code:
// Dispatches to the correct kernel function, passing the arguments.
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.
// LAB 3: Your code here.
//panic("syscall not implemented");
switch (syscallno) {
case (SYS_cputs):
// pass first two parameters
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;
}
}
Note the comment of syscall
function in 3.1. and the explaination in the document:
The system call number will go in
%eax
, and the arguments (up to five of them) will go in%edx
,%ecx
,%ebx
,%edi
, and%esi
, respectively. The kernel passes the return value back in%eax
.
Here we have to bind the value of each register to each parameter. Also note that we have to handle return value of the syscall
function, in other words, pass the return value back in %eax
.
static void
trap_dispatch(struct Trapframe *tf)
{
int32_t ret;
// Handle processor exceptions.
// LAB 3: Your code here.
switch (tf->tf_trapno) {
...
case (T_SYSCALL):
ret = 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);
// Pass the return value back in %eax
tf->tf_regs.reg_eax = ret;
break;
...
}
}
What we have to do is to set thisenv
to point at our Env
structure in envs[]
. To fetch the index, there is a macro called ENVX(envid)
to call:
The environment index ENVX(eid) equals the environment's index in the 'envs[]' array.
#define ENVX(envid) ((envid) & (NENV - 1))
So the code is:
void
libmain(int argc, char **argv)
{
// set thisenv to point at our Env structure in envs[].
// LAB 3: Your code here.
thisenv = &envs[ENVX(sys_getenvid())];
// save the name of the program so that panic() can use it
if (argc > 0)
binaryname = argv[0];
// call user main routine
umain(argc, argv);
// exit gracefully
exit();
}
Here we have to check if the page fault hapens in kernel mode.
Hint: to determine whether a fault happened in user mode or in kernel mode, check the low bits of the
tf_cs
.
Note the %cs
has 4 bits, and we have to check the low 2 bits. Mind the comments in kern/env.c
:
In particular, the last argument to the SEG macro used in the definition of gdt specifies the Descriptor Privilege Level (DPL) of that descriptor: 0 for kernel and 3 for user.
The low 2 bits of each segment register contains the Requestor Privilege Level (RPL); 3 means user mode.
void
page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;
// Read processor's CR2 register to find the faulting address
fault_va = rcr2();
// Handle kernel-mode page faults.
// LAB 3: Your code here.
if ((tf->tf_cs & 3) == 0)) {
panic("page_fault_handler: page is in kernel mode, fault at %d", fault_va);
}
...
}
Things to do according to the comment above user_mem_check
:
- Page-align the start and the end memory address (i.e.
va
andva + len
); - Retrieve the pointer to the page table entry (PTE) and check if these pages can access a virtual address, including:
- The address is below
ULIM
; - The page exists;
- The page table gives it permission;
- The address is below
- If there is an error, set
user_mem_check_addr
to the first errorneous virtual address, and return-E_FAULT
; - If the user program can access this range of addresses, returns
0
.
So the code is:
int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
// LAB 3: Your code here.
uintptr_t start = (uintptr_t) ROUNDDOWN(va, PGSIZE);
uintptr_t end = (uintptr_t) ROUNDUP((va + len), PGSIZE);
pte_t *curr_pg = NULL;
// Find the permissions in the range of virtual address of the pages
for ( ; start < end; start += PGSIZE) {
curr_pg = pgdir_walk(env->env_pgdir, (void *) start, 0);
if (start > ULIM || curr_pg == NULL ||
((*curr_pg) & perm) != perm) {
user_mem_check_addr = (start == (uintptr_t) ROUNDDOWN(va, PGSIZE)) ?
(uintptr_t) va : (uintptr_t) start;
return -E_FAULT;
}
}
return 0;
}
Just add one line to call user_mem_assert
to implement sanity check:
static void
sys_cputs(const char *s, size_t len)
{
...
// LAB 3: Your code here.
user_mem_assert(curenv, s, len, 0);
...
}
Finally, change
debuginfo_eip
inkern/kdebug.c
to calluser_mem_check
onusd
,stabs
, andstabstr
. If you now runuser/breakpoint
, you should be able to run backtrace from the kernel monitor and see the backtrace traverse intolib/libmain.c
before the kernel panics with a page fault.
Here is the code. Mind the -E_FAULT
is 6
and user_mem_check
will return 0 when it is successful.
int
debuginfo_eip(uintptr_t addr, struct Eipdebuginfo *info)
{
...
// Find the relevant set of stabs
if (addr >= ULIM) {
...
} else {
// The user-application linker script, user/user.ld,
// puts information about the application's stabs (equivalent
// to __STAB_BEGIN__, __STAB_END__, __STABSTR_BEGIN__, and
// __STABSTR_END__) in a structure located at virtual address
// USTABDATA.
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.
// LAB 3: Your code here.
if (user_mem_check(curenv, usd, sizeof(const 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.
// LAB 3: Your code here.
if (user_mem_check(curenv, stabs, stab_end - stabs, PTE_U) != 0 ||
user_mem_check(curenv, stabstr, stabstr_end - stabstr, PTE_U) != 0) {
return -1;
}
}
}
This completes the lab. At this time, you will pass all of the make grade
tests.