RISC-V assembly
ra
is ABI name for register x1
. It is to store return address.ret
: Return from subroutine. The actual instructions are:jalr x0, x1, 0
auipc x6, offset[31:12]
void g(int x) {
printf("%d", x);
}
void main(void) {
g(3);
exit(0);
}
Assembly version
void g(int x) {
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
8: 85aa mv a1,a0
printf(“%d”, x)*;*
a: 00000517 auipc a0,0x0
e: 7ce50513 addi a0,a0,1998*# 7d8 <malloc+0xe6>*
12: 00000097 auipc ra,0x0
16: 622080e7 jalr 1570(ra)*# 634 <printf>*
}
1a: 60a2 ld ra,8(sp)
1c: 6402 ld s0,0(sp)
1e: 0141 addi sp,sp,16
20: 8082 ret
0000000000000022 <main>:
void main(void) {
22: 1141 addi sp,sp,-16
24: e406 sd ra,8(sp)
26: e022 sd s0,0(sp)
28: 0800 addi s0,sp,16
g(3)*;*
2a: 450d li a0,3
2c: 00000097 auipc ra,0x0
30: fd4080e7 jalr -44(ra)*# 0 <g>*
exit(0)*;*
34: 4501 li a0,0
36: 00000097 auipc ra,0x0
3a: 27e080e7 jalr 638(ra)*# 2b4 <exit>*
main
uses jalr
to call g().What does jar do?
RISC-V’s subroutine call jal (jump and link) places its return address in a register. This is faster in many computer designs, because it saves a memory access compared to systems that push a return address directly on a stack in memory.
In our example, the return address is saved in
ra
. Then PC jumps to g() and continue executing. In beginning of g(), you can see it saves return address to stack: sd ra,8(sp)
At end of g(), it retrieves ra
back: ld ra,8(sp)
What is return address?
Assembly snippet
30: fd4080e7 jalr -44(ra)*# 0 <g>*
exit(0)*;*
34: 4501 li a0,0
Gdb breakpoints and exam content in
ra
Thread 3 hit Breakpoint 1, g (x=x@entry=3) at user/call.c:6
6 void g(int x) {
(gdb) info reg
ra 0x34 0x34 <main+18>
Clearly, the return address is the address of the next instruction after JAL, which is 0x34.
We found the proof in RISCV-SPEC JAL stores the address of the instruction following the jump (pc+4) into register rd.
So before we calling function, we set return address first.
what does ret do?
Let’s see spec first:

jalr
saves return address to first param. This part we are clear. It then sets PC to rs1 + offset. So PC will continue from the new address. Now, let’s look at our ret instruction’s meaningjalr x0, x1, 0
auipc x6, offset[31:12]
Save return address to x0, which does nothing in our code example. It then set PC to
ra
+ 0, which means let program counter continues from the return address we saved before!When your program is calling a function, it prepares the return address first before jump to the function. So that when the function about to return, it knows where it should return to.
jalr
saves return address for current PC+4, jump to the calling function. ra
contains the return address.
Proposal 1
Start from max VA - trampoline - trap frame. Every mmap, find current max end VA we use, deduct size, and round down to find start address. Save start addr, and end address.
Keep track of current max end virtual address. So every mmap knows where to start.
In munmap, we need to reset current max end if needed.
For searching, it is bad. Since it requires loop the entire list to find coverage.
The system call pipe creates a pipe object, and two files. The two files link to the pipe, one for write, one for read. The system calls write back the FD of two files to user program.
Shell calls fork to run write, and another fork to run read. In this way, 2 different process are running write and read work, so they can wait and wakeup each other if needed. The detail implementation of how pipe write and read work is in pipe.c. But the key take-away is since they are handled by different process, so they can sleep on chan, and got waken up by scheduler.
A file can be a pipe type. A pipe is:
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
uint nread; // number of bytes read*
uint nwrite; // number of bytes written*
int readopen; // read fd is still open*
int writeopen; // write fd is still open*
};
Two file write and read from the same pipe’s data array.
broadcast feature
We could implement a broadcast system call similar to pipe. A write could be broadcast to multiple listeners. Once the writer is done, wakeup all sleeping processes, and let them read, and do something. Each listener updates how much they have read, last read position etc. How to run this new call?
int fds[5]
broadcast(fds)
write(fds[0]
…
read(fds[1])
…
read(fds[2])
…
IPC
Can we make a ‘pipe' talking to each other? We might need 2 data arrary in struct IPC_PIPE. One for fd1 write, one for fd2 write. Each pipe type file needs to remember it reads from which data content, and write to which one.
Process calls
yield
to give up CPU, and let CPU scheduler to run. It calls the context switch assembly to save its registers, for later restoring purpose.The assembly for calling the big
swtch
is at 800020b6
:
switch
is at:0000000080002618 <swtch>:
80002618: 00153023 sd ra,0(a0)
8000261c: 00253423 sd sp,8(a0)
80002620: e900 sd s0,16(a0)
80002622: ed04 sd s1,24(a0)
80002624: 03253023 sd s2,32(a0)
80002628: 03353423 sd s3,40(a0)
8000262c: 03453823 sd s4,48(a0)
80002630: 03553c23 sd s5,56(a0)
80002634: 05653023 sd s6,64(a0)
…
Key code is here:
800020ae: addi a0,s1,104
800020b2: auipc ra,0x0
800020b6: jalr 1382(ra) # 80002618 <swtch>
800020ba: mv a5,tp
auipc ra,0x0
prepares ra
for the jump. ra
is set to 800020b2
.jalr 1382(ra)
adds 0x1382 to 800020b2
, which is jumping to the swtch
context swtich assembly (at 80002618
).After the jump, at this point,
ra
is set to PC + 4, which is 800020ba
.
Stepi
shows the next instruction to execute.Swtch
does not save the program counter. Instead, swtch
saves the ra
register, which holds the return address from which swtch
was called (PC + 4).