[Lab Report] MIT 6.S081 Lab: traps (v2022)
RISC-V assembly
Which registers contain arguments to functions? For example, which register holds 13 in main’s call to
printf
?
Here are the contents of main
and its corresponding assembly code:
// call.c
void main(void) {
printf("%d %d\n", f(8)+1, 13);
exit(0);
}
# call.asm
void main(void) {
1c: 1141 addi sp,sp,-16
1e: e406 sd ra,8(sp)
20: e022 sd s0,0(sp)
22: 0800 addi s0,sp,16
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0
2c: 7d850513 addi a0,a0,2008 # 800 <malloc+0xe8>
30: 00000097 auipc ra,0x0
34: 62a080e7 jalr 1578(ra) # 65a <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 298080e7 jalr 664(ra) # 2d2 <exit>
a0
, a1
…, a7
contain arguments to functions. In this case, it is a2
that holds 13.
Where is the call to function
f
in the assembly code for main? Where is the call tog
? (Hint: the compiler may inline functions.)
Here are the contents of f
, g
and their corresponding assembly code:
// call.c
int g(int x) {
return x+3;
}
int f(int x) {
return g(x);
}
# call.asm
int g(int x) {
0: 1141 addi sp,sp,-16
2: e422 sd s0,8(sp)
4: 0800 addi s0,sp,16
return x+3;
}
6: 250d addiw a0,a0,3
8: 6422 ld s0,8(sp)
a: 0141 addi sp,sp,16
c: 8082 ret
int f(int x) {
e: 1141 addi sp,sp,-16
10: e422 sd s0,8(sp)
12: 0800 addi s0,sp,16
return g(x);
}
14: 250d addiw a0,a0,3
16: 6422 ld s0,8(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret
From above we can tell that the compiler directly calculates the result of “f(8)+1=12” and stores it in a1
.
At what address is the function
printf
located?
000000000000065a <printf>:
What value is in the register
ra
just after thejalr
toprintf
inmain
?
According to the RISC-V manual, the indirect jump instruction jalr
(jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1
, then setting the least-significant bit of the result to zero. The address of the instruction following the jump (pc+4
) is written to register rd
. Register x0 can be used as the destination if the result is not required.
Specifically, the instruction jalr rd, offset(rs1)
will store the value of pc+4
in rd
. If rd
is ra
instead, the instruction essentially becomes a function call. Otherwise, it is just a simple jump. The supported range is within ±2KB (-2048 ~ 2047) based on rs1
. By combining the high 20 bits of auipc
with the low 12 bits of jalr
, all functions within the 32-bit range of pc
can be called.
34: 62a080e7 jalr 1578(ra) # 65a <printf>
exit(0);
38: 4501 li a0,0
So in this case the value
in ra
after jalr
to printf
is 0x38.
Run the following code.
unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, &i);
What is the output?
The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set
i
to in order to yield the same output? Would you need to change57616
to a different value?
The output is: He110 World
. If the RISC-V is big-endian, setting i
as dlr
would yield the same output. And since 57616 will always be 0xe110 no matter the RISC-V is little-endian or big-endian, we don’t need to change 57616
to a different value.
In the following code, what is going to be printed after
'y='
? (note: the answer is not a specific value.) Why does this happen?printf("x=%d y=%d", 3);
The printed value for y
is undefined. Before calling printf
, the compiler will place the input parameters into registers a0-a7
. When jumping to printf
, the values are then read from the registers. However, since we did not specify a value for the second placeholder, a2
was not modified. Therefore, the value of y
printed out is an arbitrary value that was present in a2
before the function call.
Backtrace
The task becomes trivial following the hints:
-
Add the prototype for
backtrace()
tokernel/defs.h
.// printf.c void printf(char*, ...); void panic(char*) __attribute__((noreturn)); void printfinit(void); void backtrace(void);
-
The GCC compiler stores the frame pointer of the currently executing function in the register
s0
. Addr_fp()
tokernel/riscv.h
:... static inline uint64 r_ra() { uint64 x; asm volatile("mv %0, ra" : "=r" (x) ); return x; } static inline uint64 r_fp() { uint64 x; asm volatile("mv %0, s0" : "=r" (x) ); return x; } // flush the TLB. ...
-
Layout of stack frames . . +-> . | +-----------------+ | | | return address | | | | previous fp ------+ | | saved registers | | | local variables | | | ... | <-+ | +-----------------+ | | | return address | | +------ previous fp | | | saved registers | | | local variables | | +-> | ... | | | +-----------------+ | | | return address | | | | previous fp ------+ | | saved registers | | | local variables | | | ... | <-+ | +-----------------+ | | | return address | | +------ previous fp | | | saved registers | | | local variables | | $fp --> | ... | | +-----------------+ | | return address | | | previous fp ------+ | saved registers | $sp --> | local variables | +-----------------+
$sp
defines the end of the current stack frame, while$fp
defines the beginning of the current stack frame (which is also the end of the last stack frame). Specifically,$fp
contains the value of$sp
just before the current function was called.- The stack grows downward from high addresses to low addresses, so although
$fp
is the starting address of the frame, its address is higher than that of$sp
.
Implement
backtrace()
inkernel/printf.c
:... void backtrace(void) { uint64 fp = r_fp(); while (fp != PGROUNDDOWN(fp)) { // PGROUNDDOWN(fp) always indicates the starting position of the page where fp is located uint64 r_addr = *(uint64*)(fp - 8); // the return address lives at a fixed offset (-8) from the frame pointer of a stackframe printf("%p\n", r_addr); fp = *(uint64*)(fp - 16); // the saved frame pointer lives at fixed offset (-16) from the frame pointer } }
Alarm
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
We start with registering the system calls sigalarm
and sigreturn
following the prescribed steps, which will not be elaborated further.
We will need the following entries in the proc
structure (in kernel/proc.h
):
struct proc {
...
// store the alarm interval and the pointer to the handler function
int alarminterval;
void (*alarmhandler)();
// keep track of how many ticks have passed since the last call
int passedticks;
// save registers so that we can restore the states upon return
struct trapframe *savedtrapframe;
// prevent re-entrant calls to the handler
int caninvoke;
};
Initialize and free these entries in kernel/proc.c
accordingly:
...
static struct proc*
allocproc(void)
{
...
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Allocate a savedtrapframe page.
if((p->savedtrapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->alarminterval = 0;
p->alarmhandler = 0;
p->passedticks = 0;
p->caninvoke = 1;
// An empty user page table.
...
}
...
static void
freeproc(struct proc *p)
{
...
p->trapframe = 0;
if(p->savedtrapframe)
kfree((void*)p->savedtrapframe);
p->savedtrapframe = 0;
if(p->pagetable)
...
p->alarminterval = 0;
p->alarmhandler = 0;
p->passedticks = 0;
p->caninvoke = 1;
}
...
Now we can implement sys_sigalarm
and sys_sigreturn
in kernel/sysproc.c
:
...
uint64
sys_sigalarm(void)
{
int interval;
uint64 handler;
argint(0, &interval);
argaddr(1, &handler);
struct proc *p = myproc();
p->alarminterval = interval;
p->alarmhandler = (void(*)())(handler);
p->passedticks = 0;
return 0;
}
uint64
sys_sigreturn(void)
{
struct proc *p = myproc();
*p->trapframe = *p->savedtrapframe;
p->caninvoke = 1;
return p->trapframe->a0; // sigreturn is a system call, and its return value is stored in a0
}
Then, modify the usertrap
function in kernel/trap.c
and we are done:
void
usertrap(void)
{
...
if(killed(p))
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->alarminterval > 0 && p->caninvoke) {
if(++p->passedticks == p->alarminterval) {
p->passedticks = 0;
*p->savedtrapframe = *p->trapframe; // save all the registers before jumping to the handler
p->trapframe->epc = (uint64)p->alarmhandler;
p->caninvoke = 0;
}
}
}
usertrapret();
}
For some reason, usertests -q
will hang on test preempt
on my machine. The source code for this test is provided below:
// meant to be run w/ at most two CPUs
void
preempt(void)
{
int pid1, pid2, pid3;
int pfds[2];
printf(1, "preempt: ");
pid1 = fork();
if(pid1 == 0)
for(;;)
;
pid2 = fork();
if(pid2 == 0)
for(;;)
;
pipe(pfds);
pid3 = fork();
if(pid3 == 0){
close(pfds[0]);
if(write(pfds[1], "x", 1) != 1)
printf(1, "preempt write error");
close(pfds[1]);
for(;;)
;
}
close(pfds[1]);
if(read(pfds[0], buf, sizeof(buf)) != 1){
printf(1, "preempt read error");
return;
}
close(pfds[0]);
printf(1, "kill... ");
kill(pid1);
kill(pid2);
kill(pid3);
printf(1, "wait... ");
wait();
wait();
wait();
printf(1, "preempt ok\n");
}
I’m uncertain whether this issue is due to a kernel malfunction or simply a result of the system running too slowly. If you have experience with this particular problem as well and have been able to identify its cause, I would greatly appreciate hearing about your findings.