2.6 Code: starting xv6, the first process and system call
To make xv6 more concrete, we’ll outline how the kernel starts and runs the first process. The subsequent chapters will describe the mechanisms that show up in this overview in more detail.
When the RISC-V computer powers on, it initializes
itself and runs a boot loader which is stored in read-only
memory. The boot loader loads the xv6 kernel into memory. Then, in
machine mode, the CPU executes xv6 starting at
_entry
(kernel/entry.S:7).
The RISC-V starts with paging hardware disabled:
virtual addresses map directly to physical addresses.
The loader loads the xv6 kernel into memory at physical address 0x80000000. The reason it places the kernel at 0x80000000 rather than 0x0 is because the address range 0x0:0x80000000 contains I/O devices.
The instructions at
_entry
set up a stack so that xv6 can run C code.
Xv6 declares space for an initial stack,
stack0
,
in the file
start.c
(kernel/start.c:11).
The code at
_entry
loads the stack pointer register
sp
with the address
stack0+4096
,
the top of the stack, because the stack
on RISC-V grows down.
Now that the kernel has a stack,
_entry
calls into C code at
start
(kernel/start.c:15).
The function
start
performs some configuration that is only allowed in
machine mode, and then switches to supervisor mode.
To enter supervisor mode, RISC-V
provides the instruction
mret
.
This instruction is most often used to return from
a previous call from supervisor mode to machine mode.
start
isn’t returning from such a call, but
sets things up as if it were:
it sets the previous privilege mode to
supervisor in the register
mstatus
,
it sets the return address to
main
by writing
main
’s
address into
the register
mepc
,
disables virtual address translation in supervisor mode
by writing
0
into the page-table register
satp
,
and delegates all interrupts and exceptions
to supervisor mode.
Before jumping into supervisor mode,
start
performs one more task: it programs the clock
chip to generate timer interrupts.
With this housekeeping out of the way,
start
“returns” to supervisor
mode by calling
mret
.
This causes the program counter to change
to
main
(kernel/main.c:11),
the address previously stored in mepc
.
After
main
(kernel/main.c:11)
initializes several devices and subsystems,
it creates the first process by calling
userinit
(kernel/proc.c:233).
The first process executes a small program written in RISC-V assembly,
which makes the first system call in xv6.
initcode.S
(user/initcode.S:3) loads the number for the exec
system call, SYS_EXEC
(kernel/syscall.h:8),
into
register a7,
and then calls ecall
to re-enter the kernel.
The kernel uses the number in register a7 in syscall
(kernel/syscall.c:132) to call the desired system call.
The system call table (kernel/syscall.c:107) maps
SYS_EXEC
to the function sys_exec
, which the kernel
invokes. As we saw in Chapter 1, exec
replaces the memory and registers of the current process with a new
program (in this case, /init
).
Once the kernel has completed
exec
,
it returns to user space in
the /init
process.
init
(user/init.c:15)
creates a new console device file
if needed
and then opens it as file descriptors 0, 1, and 2.
Then it starts a shell on the console.
The system is up.