Function call is running in user mode, using the user source, and there is no need for privilege switching and context restoring.
System call is the request from user mode to higher privilege mode, for getting into system to use kernel source such as I/O device, network interface.
System call
System call is the ABI(Application Binary Interface), which provide interface for U mode to use OS source. The follow is the process of system call:
The user program prepares for system call by loading the system call number and the arguments that needed in predefined registers(a7: system call number, a0~a6: Hold arguments of system call, a0 also hold the return value from the system call)
After that, user program issue the ecall instruction. After the ecall executed, a trap occurs, and the trap context switch performs. The CPU transfers control to the operating system’s trap handler. The trap handler use the sscause and system call number in a7 to execute requested system call. Once the system call is completed, the return value (if any) is placed in register a0.
Finally, restore user context to return user mode.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
.section .data message: .asciz "Hello, World!\n"
.section .text .globl _start _start: # System call to write li a7, 64 # Load system call number for 'write' (64) into a7 li a0, 1 # File descriptor 1 (stdout) into a0 la a1, message # Load address of message into a1 li a2, 14 # Message length (14 bytes) into a2 ecall # Issue the system call
# System call to exit li a7, 93 # Load system call number for 'exit' (93) into a7 li a0, 0 # Exit code 0 into a0 ecall # Issue the system call
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(), looks exactly like a typical procedure call in C; that is, if it looks just like a procedure call, how does the system know it’s a system call, and do all the right stuff? The simple reason: it is a procedure call, but hidden inside that procedure call is the famous trap instruction. More specifically, when you call open() (for example), you are executing a procedure call into the C library. Therein, whether for open() or any of the other system calls provided, the library uses an agreed-upon calling convention with the kernel to put the arguments to open in well-known locations (e.g., on the stack, or in specific registers), puts the system-call number into a well-known location as well (again, onto the stack or a register), and then executes the aforementioned trap instruction. The code in the library after the trap unpacks return values and returns control to the program that issued the system call. Thus, the parts of the C library that make system calls are hand-coded in assembly, as they need to carefully follow convention in order to process arguments and return values correctly, as well as execute the hardware-specific trap instruction. And now you know why you personally don’t have to write assembly code to trap into an OS; somebody has already written that assembly for you.
Trap Management
When user program issues the ecall instruction, it trigger the trap execution to switch the processor from user mode to supervisor mode. Then the first step is to find the trap handler entry address to jump to. To do this, the kernel set the trap table at boot time.
In rCore, at boot time, trap entry address is set by
1 2 3 4 5 6 7 8 9
/// initialize CSR `stvec` as the entry of `__alltraps` pubfninit() { extern"C" { fn__alltraps(); } unsafe { stvec::write(__alltraps asusize, TrapMode::Direct); } }
There is only a single trap entry _alltraps. All traps get into _alltraps to do trap context save and load and run different functionality base on what exception was(sscause).
*sstatus* used to back to the mode before trap.
*spec* save the last instruction before trap, which used to continue user program(PC is set sepc) after issued *sret* return user mode.
.align 2 __alltraps: csrrw sp, sscratch, sp # now sp->kernel stack, sscratch->user stack # allocate a TrapContext on kernel stack addi sp, sp, -34*8 # save general-purpose registers sd x1, 1*8(sp) # skip sp(x2), we will save it later sd x3, 3*8(sp) # skip tp(x4), application does not use it # save x5~x31 .set n, 5 .rept 27 SAVE_GP %n .set n, n+1 .endr # we can use t0/t1/t2 freely, because they were saved on kernel stack csrr t0, sstatus csrr t1, sepc sd t0, 32*8(sp) sd t1, 33*8(sp) # read user stack from sscratch and save it on the kernel stack csrr t2, sscratch sd t2, 2*8(sp) # set input argument of trap_handler(cx: &mut TrapContext) mv a0, sp call trap_handler