This is the fourth chapter of lab code notes for the course of OPERATING system MIT6.S081 taught by myself: Traps. This lab approximately takes 8 hours.

Courses address: pdos.csail.mit.edu/6.S081/2020… Lab address: pdos.csail.mit.edu/6.S081/2020… My code address: github.com/Miigon/my-x… Commits: github.com/Miigon/my-x…

The code comments in this article were added while writing the blog, and the code in the original repository may be missing comments or not identical.

Lab 4: Traps

This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.

Explore how traps implement system calls.

Note that the main content of this part is actually in the lecture (Lecture 5, lecture 6). The experiment is not very complicated but focuses on understanding concepts, such as trap mechanism, trampoline function, function calling convention, call stack, privilege mode, RISCV assembly. These may still be able to complete the LAB even without knowing. But this does not mean that these are not important, on the contrary, these are the main content, otherwise, even if lab runs, it is just blitzer, not really achieve the learning effect.

RISC-V assembly (easy)

It will be important to understand a bit of RISC-V assembly, which you were exposed to in 6.004. There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program in user/call.asm.

Read the code in call.asm for the functions g, f, and main. The instruction manual for RISC-V is on the reference page. Here are some questions that you should answer (store the answers in a file answers-traps.txt):

Read call.asm and the RISC-V instruction set tutorial to answer questions. (Learn risC-V assembly)

Q: Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
A: a0-a7; a2;

Q: Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)
A: There is none. g(x) is inlined within f(x) and f(x) is further inlined into main()

Q: At what address is the function printf located?
A: 0x0000000000000628, main calls it with pc-relative addressing.

Q: What value is in the register ra just after the jalr to printf in main?
A: 0x0000000000000038, next line of assembly right after the jalr

Q: Run the following code.

	unsigned int i = 0x00646c72;
	printf("H%x Wo%s", 57616, &i);      

What is the output?
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 change 57616 to a different value?
A: "He110 World"; 0x726c6400; no, 57616 is 110 in hex regardless of endianness.

Q: 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);

A: A random value depending on what codes there are right before the call.Because printf tried to read more arguments than supplied.
The second argument `3` is passed in a1, and the register for the third argument, a2, is not set to any specific value before the
call, and contains whatever there is before the call.
Copy the code

Simple translation:

Q: Which registers store the parameters of the function call? For example, when main calls printf, which register is 13 stored in? A: a0-a7; a2; Q: Where is the assembly code for calling f in main? What about the call to g? A: There is no such code. G (x) is internally linked to f(x), and then f(x) is further internally linked to main() Q: what is the address of the printf function? A: 0x0000000000000628, main uses PC relative addressing to calculate this address. Q: What is the value of ra after jalr jumps to printf in main? A: 0x0000000000000038, the address of the next assembly instruction of the JALR instruction. Q: Run the following code unsigned int I = 0x00646C72; printf("H%x Wo%s", 57616, &i); What is the output? If RISC-V is big-endian, why do I need to be set to achieve the same effect? Do I need to change 57616 to something else? A: "He110 World"; 0x726c6400; Q: In the following code, what is promised after 'y='? (Note: The answer is not a specific value.) Why? printf("x=%d y=%d", 3); A: The output is A "random" value that is affected by the code before the call. Because printf tries to read more arguments than it provides. The second parameter '3' is passed through A1, and the register A2 corresponding to the third parameter is not set to any specific value before the call, but will contain any values that were already in before the call occurred.Copy the code

Backtrace (moderate)

For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.

Implement a backtrace() function in kernel/printf.c. Insert a call to this function in sys_sleep, and then run bttest, which calls sys_sleep. Your output should be as follows:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
Copy the code

After bttest exit qemu. In your terminal: the addresses may be slightly different but if you run addr2line -e kernel/kernel (or riscv64-unknown-elf-addr2line -e kernel/kernel) and cut-and-paste the above addresses as follows:

$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D
Copy the code

You should see something like this:

kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85
Copy the code

Add backtrace function to print out call stack for debugging.

Add a declaration in defs.h

// defs.h
void            printf(char*,...).;
void            panic(char*) __attribute__((noreturn));
void            printfinit(void);
void            backtrace(void); // new
Copy the code

Add risCv.h to get the current FP (frame Pointer) register:

// riscv.h
static inline uint64
r_fp(a)
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x));
  return x;
}
Copy the code

Fp refers to the start address of the current stack frame, sp refers to the end address of the current stack frame. (The stack grows from high address to low address, so FP is the starting address of the frame, but the address is higher than SP.) The first 8-byte fP-8 in the stack frame from high to low is the return address that the current calling layer should return to. The second 8-byte FP-16 in the stack frame is the previous address, which points to the start address of the FP in the previous stack frame. The rest are saved registers, local variables, and so on. The size of a stack frame is not fixed, but at least 16 bytes. In Xv6, a page is used to store the stack, and if FP has reached the upper bound of the stack page, it has reached the bottom of the stack.

If you look at call.asm, you can see that the function body of a function initially extends a stack frame to be used by the layer call, and then is reclaimed after the function completes execution. Example:

Int g(int x) {0: 1141 addi sp,sp,-16 // expand stack frame 2: e422 sd s0,8(sp) 0800 addi s0,sp,16 return x+3; } 6: 250d addiw A0,a0,3 8: 6422 ld s0,8(sp) // from stack frame read return address a: 0141 addi sp,sp,16 // reclaim stack frame C: 8082 ret // returnCopy the code

Note that the stack grows from high addresses to low addresses, so expansion is -16 and reclamation is +16.

For more details on registers, stack frames, and memory calls, check out Lecture 5, or the useful slides.

Backtrace backtrace backtrace

// printf.c

void backtrace(a) {
  uint64 fp = r_fp();
  while(fp ! = PGROUNDUP(fp)) {// If you have reached the bottom of the stack
    uint64 ra = *(uint64*)(fp - 8); // return address
    printf("%p\n", ra);
    fp = *(uint64*)(fp - 16); // previous fp}}Copy the code

Call backtrace() once at the beginning of sys_sleep

// sysproc.c
uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  backtrace(); // print stack backtrace.

  if(argint(0, &n) < 0)
    return - 1;
  
  / /...

  return 0;
}
Copy the code

Compile and run:

$ bttest
0x0000000080002dea
0x0000000080002cc4
0x00000000800028d0
Copy the code

Alarm (hard)

In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.

Add system calls sigalarm and SIGReturn as follows:

int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
Copy the code

First, add an alarm-related field to the proc struct definition:

  • Alarm_interval: clock interval. 0 is disabled
  • Alarm_handler: clock callback handler
  • Alarm_ticks: indicates the number of ticks left before the next clock starts
  • Alarm_trapframe: Indicates the trapframe at the interrupt time of the clock, which is used to restore the original program after the interrupt processing is complete
  • Alarm_goingoff: Whether a clock callback has been executed and has not been returned (used to prevent alarm_handler from being called again when the alarm_handler alarm expires, causing alarm_trapFrame to be overwritten)
struct proc {
  / /...
  int alarm_interval;          // Alarm interval (0 for disabled)
  void(*alarm_handler)();      // Alarm handler
  int alarm_ticks;             // How many ticks left before next alarm goes off
  struct trapframe *alarm_trapframe;  // A copy of trapframe right before running alarm_handler
  int alarm_goingoff;          // Is an alarm currently going off and hasn't not yet returned? (prevent re-entrance of alarm_handler)
};
Copy the code

Sigalarm and SIGReturn

// sysproc.c
uint64 sys_sigalarm(void) {
  int n;
  uint64 fn;
  if(argint(0, &n) < 0)
    return - 1;
  if(argaddr(1, &fn) < 0)
    return - 1;
  
  return sigalarm(n, (void(*)())(fn));
}

uint64 sys_sigreturn(void) {
	return sigreturn();
}
Copy the code
// trap.c
int sigalarm(int ticks, void(*handler)()) {
  // Sets the related properties in myProc
  struct proc *p = myproc();
  p->alarm_interval = ticks;
  p->alarm_handler = handler;
  p->alarm_ticks = ticks;
  return 0;
}

int sigreturn(a) {
  // Return the trapFrame to the state before the clock was interrupted
  struct proc *p = myproc();
  *p->trapframe = *p->alarm_trapframe;
  p->alarm_goingoff = 0;
  return 0;
}
Copy the code

Add initialization and release code to proc.c:

// proc.c
static struct proc*
allocproc(void)
{
  / /...

found:
  p->pid = allocpid();

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  // Allocate a trapframe page for alarm_trapframe.
  if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_ticks = 0;
  p->alarm_goingoff = 0;

  / /...

  return p;
}

static void
freeproc(struct proc *p)
{
  / /...

  if(p->alarm_trapframe)
    kfree((void*)p->alarm_trapframe);
  p->alarm_trapframe = 0;
  
  / /...
  
  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_ticks = 0;
  p->alarm_goingoff = 0;
  p->state = UNUSED;
}
Copy the code

In the usertrap() function, the specific code to implement the clock mechanism:

void
usertrap(void)
{
  int which_dev = 0;

  / /...

  if(p->killed)
    exit(- 1);

  // give up the CPU if this is a timer interrupt.
  // if(which_dev == 2) {
  // yield();
  // }

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if(p->alarm_interval ! =0) { // If the clock event is set
      if(--p->alarm_ticks <= 0) { // The clock counts down to -1 tick. If the tick count has reached or exceeded the specified tick count, the clock counts down to -1 tick
        if(! p->alarm_goingoff) {// Make sure no clock is running
          p->alarm_ticks = p->alarm_interval;
          // jump to execute alarm_handler
          *p->alarm_trapframe = *p->trapframe; // backup trapframe
          p->trapframe->epc = (uint64)p->alarm_handler;
          p->alarm_goingoff = 1;
        }
        // If there is already a clock handler running when a clock expires, it will delay the ticking until the next tick after the original handler completes
      }
    }
    yield();
  }

  usertrapret();
}
Copy the code

This way, each time the clock is interrupted, if the process has a clock that has been set (alarm_interval! = 0), and the reciprocal of alarm_ticks is performed. When alarm_ticks countdown to less than or equal to 0, if there is no clock being processed, try to trigger the clock to save the original program flow (* alarm_trapFrame = * trapFrame), then modify the VALUE of the PC register, Jump the program flow to alarm_handler, and then restore the original execution flow (* trapFrame = * alarm_trapFrame) after the alarm_handler is executed. This is an imperceptible interruption from the point of view of the original program execution flow.

Compile and run:

$ alarmtest test0 start ............. alarm! test0 passed test1 start .. alarm! . alarm! . alarm! . alarm! . alarm! . alarm! . alarm! .alarm! . alarm! . alarm! test1 passed test2 start .............. alarm! test2 passedCopy the code

Optional challenge exercises

Print the names of the functions and line numbers in backtrace() instead of numerical addresses (hard).

The default compilation mode for Xv6 contains debugging information in the generated executable file, including the names of all symbols and their corresponding addresses. In theory, BackTrace can do something similar to addr2Line, parsing the debugging information attached to the executable itself to get the source file and line number corresponding to the address. Skip the challenge here.