
MIPS system calls

Now that the code has already been written and debugged, I decided to write an article that would reveal in more detail how the system call mechanism in MIPS works. You can consider it as an addition to that article on assembler.
Introduction
First of all, you need to understand what system calls are, and why they are needed.
Wikipedia gives the following definition:
A system call in programming and computer engineering is the appeal of an application program to the kernel of an operating system to perform an operation.
From a programmer's point of view, a system call usually looks like a subroutine or function call from a system library. However, a system call as a special case of calling such a function or subroutine should be distinguished from a more general call to the system library, since the latter may not require privileged operations.

In other words, a system call is a function call with a predetermined address and simultaneously transferring the processor from privileged mode (kernel mode). Switching to kernel mode allows you to execute privileged commands, for example, managing virtual memory tables, prohibiting / allowing interrupts, and accessing data stored in the kernel.
A pre-known address means that all the processing functions can be represented by an array of pointers, and the index in this array will correspond to this handler. The difference between a system call and a function call is that control to the base address + offset is transmitted by hardware, by the processor itself.
That is, the processor, having met the instruction generating the system call, interrupts the sequential execution of the user program commands and transfers control to the desired address with the necessary information saved to return to the main program. This is very similar to the behavior of the processor when an exception or external interruption occurs, therefore, usually these subsystems are implemented in a similar way and are considered together.
MIPS Architecture
Let's move on to a specific implementation of these subsystems in the MIPS architecture.
There are several modes of operation for interrupts in the MIPS version 2 architecture. They differ in the base addresses and structures of the interrupt tables themselves.
There are two modes for the base address:
- In the first case, the processor, faced with any type of exception, transfers control to the address fixed address (0x80000180), the size of the processor is 128 bytes.
- In the second, the processor transfers control to the address specified in the CP0_EBASE register, the size of the processor is 256 bytes.
There are two modes of operation for the structure of the interrupt table: normal, when one handler is called in response to all exceptional situations, and vector, in which each interrupt number has its own space for the handler.
These modes are set in special registers of the MIPS processor. Special registers, unlike general purpose registers, are used by the program to control the processor itself.
In MIPS, such registers are taken out in coprocessor 0. And they are accessed by special assembler commands: mfc0 - for reading registers, and mtc0 - for writing to the register.
The registers are addressed by the index and coprocessor selector. Here are some important registers for handling system calls:
Title | Index | Selector | Description |
---|---|---|---|
CP0_STATUS | 12 | 0 | control flags for the processor |
CP0_CAUSE | thirteen | 0 | information about the cause of the interruption |
CP0_EPC | 14 | 0 | address of the command that was executing at the time of interruption |
CP0_EBASE | fifteen | 1 | base address of the exception handling procedure |
Returning to the specification of exception handling modes, they are set in two registers: CP0_STATUS and CP0_CAUSE, which have the following format.
CP0_STATUS
31-28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18-16 | 15-8 | 7 | 6 | 5 | 4-3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
CU3..CU0 | RP | FR | RE | MX | Px | Bev | TS | Sr | Nmi | Impl | IM7..IM0 | Kx | Sx | Ux | KSU | Erl | EXL | IE |
CP0_CAUSE
31 | thirty | 29-28 | 27 | 26 | 25-24 | 23 | 22 | 21-16 | 15-10 | 9-8 | 7 | 6-2 | 1-0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bd | Ti | CE | DC | PCI | 0 | IV | WP | 0 | IP | IP | 0 | excode | 0 |
CPU initialization
I will consider only the first mode of operation as the simplest and compatible with all MIPS processors. All other modes are done in a similar way.
To put the processor into this mode, you need to reset the BEV bit in the status register and bit IV in the reason register.
C code from the project
/* Setup a proper exception table and enable exceptions. */
static int mips_exception_init(void) {
unsigned int reg;
/* clear BEV bit */
reg = mips_read_c0_status();
reg &= ~(ST0_BEV);
mips_write_c0_status(reg);
/* clear CauseIV bit */
reg = mips_read_c0_cause();
reg &= ~(CAUSE_IV);
mips_write_c0_cause(reg);
/* copy the first exception handler */
memcpy((void *)(EBASE + 0x180), &mips_first_exception_handler, 0x80);
mips_setup_exc_table();
/* clear EXL bit */
reg = mips_read_c0_status();
reg &= ~(ST0_ERL);
mips_write_c0_status(reg);
return 0;
}
After clearing these bits when an interrupt, exception or system call occurs, the processor interrupts the sequential execution of instructions and transfers control to the address 0x80000180, where the primary processing code that we copied to this address is located. At the same time, the processor goes into privileged mode, saves the return address in the CP0_EPC register, and writes the reason (type) of the exception in the CP0_CAUSE register (in the exception code field).
About the exception code field is worth a little more. As mentioned above, in MIPS, as well as in other architectures, interrupts, system calls and hardware exceptions are usually implemented in a similar way, in one subsystem. That is, the first thing the handler code should do is to save information about what happened. It is this information that is entered in the exception code field. In MIPS, this field can take the following values:
The code | Designation | Description |
---|---|---|
0 | INT | External interrupt |
1-3 | Working with virtual memory | |
4 | ADDRL | Read from Unaligned Address |
5 | ADDRS | Unbalanced Address Recording |
6 | IBUS | Error reading instructions |
7 | DBUS | Error on data bus |
8 | SYSCALL | System call |
9 | Bkpt | Breakpoint |
10 | Ri | Reserved Instruction |
eleven | Coprocessor error | |
12 | Ovf | Arithmetic overflow |
13 and above | Floating point operations |
Handling System Calls
First level handler
The primary handler is written in assembler.
NESTED(mips_first_exception_handler, 0, $sp)
.set push /* save the current status of flags */
mfc0 $k1, $CP0_CAUSE
andi $k1, $k1, 0x7c /* read exception number */
j mips_second_exception_handler /* jump to real exception handler */
nop
.set pop /* restore the previous status of flags */
END(mips_first_exception_handler)
It just stores the type of exception in the register $ k1 and calls the second-level handler, which is no longer limited in size. The call is made by the command “j”, not “jar”, because the handler code is placed during the program operation (we copied it into the initialization functions), and we need to have an absolute rather than a relative address of the called procedure.
Another feature worth mentioning here is the k1 register.
In MIPS architecture, there are 32 general-purpose registers r0 - r31. And by convention, some registers are used in a special way, for example, the r31 register is used as a pointer to the stack, and it can be accessed by the special name sp. The same is with registers k0 (r26) and k1 (r27), the compiler does not use them, they are reserved for use in the kernel of the OS, and interrupt handling is just such a case of special use.
Second level handler
Let's move on to the second-level handler. Its main purpose is to prepare for calling the C-function, that is, first of all, to save the rest of the registers that can be used in this function itself. It is also written in assembler.
LEAF(mips_second_exception_handler)
SAVE_ALL /* save all needed registers */
PTR_L $k0, exception_handlers($k1) /* exception number is an offset in array */
PTR_LA $ra, restore_from_exception /* return address for exit from exception */
move $a0, $sp /* Arg 0: saved regs. */
jr $k0 /* Call C code. */
nop
restore_from_exception: /* label for exception return address */
RESTORE_ALL /* restore all registers and return from exception */
END(mips_second_exception_handler)
SAVE_ALL is an assembler macro. It looks as follows.
.macro SAVE_ALL
LONG_ADDI $sp, -PT_SIZE
SAVE_SOME
SAVE_AT
SAVE_TEMP
SAVE_STATIC
.endm
I will not cite the source code of all nested macros. I can only say that in the first line the stack frame is reserved for interruption, where all the necessary registers are sequentially saved.
SAVE_AT - the register at (r1) is reserved for use by assembler and work with it must be separated by the directives ".set noat" and ".set at" (so that there are no compiler warnings)
SAVE_TEMP - saves temporary registers (r8-r15) and (r24- r25)
SAVE_STATIC - registers s0-s7
SAVE_SOME - necessary service registers, for example, a pointer to the stack and special registers of the coprocessor (for example, status register), therefore this macro should be the first.
Then the right third-level handler is selected. Pointers to third-level handlers in our project are stored in a regular array, the type of exception sets the offset. It’s an offset, not an index, since the MIPS creators put an exception number in the CAUSE register with a shift of two bits to the left, so we can directly call a function from an array of pointers without additional arithmetic.
Then, before calling the function, we want to write the return address (ra). And finally, we pass information about the state in which we entered the interrupt to the handler function, for this we pass a pointer to the stack, and in the signature of the C-function we specify the description (structure) of this frame.
Here is a description of this structure
typedef struct pt_regs {
unsigned int reg[25];
unsigned int gp; /* global pointer r28 */
unsigned int sp; /* stack pointer r29 */
unsigned int fp; /* frame pointer r30 */
unsigned int ra; /* return address 31*/
unsigned int lo;
unsigned int hi;
unsigned int cp0_status;
unsigned int pc;
}pt_regs_t;
Third-level handler (C code)
The code for the processor is as follows
void mips_c_syscall_handler(pt_regs_t *regs) {
uint32_t result;
/* v0 contains syscall number */
uint32_t (*sys_func)(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) =
SYSCALL_TABLE[regs->reg[1]];
/* a0, a1, a2, a3, s0 contain arguments */
result = sys_func(regs->reg[3], regs->reg[4], regs->reg[5],
regs->reg[6], regs->reg[15]);
/* v0 set equal to result */
regs->reg[1] = result;
regs->pc += 4; /* skip comand generated syscall */
}
Hope everything is clear from the code:
- First, we get the system call handler number with the desired number.
- Then we call this handler, passing there all the parameters that may be during a system call.
- In the register v0 we put the result of the call.
- And finally, we skip the command that generated the system call, otherwise we will return to the same address where it happened, and the call will happen again.
Receiving a system call
Now we need to talk about how to make a system call.
A system call is generated by a special assembler command: for example, in x86 it is int , in SPARC it is ta , and in MIPS it is syscall .
As it probably became clear from the previous section, at the time of the system call, the call number should be stored in the register v0, and the parameters transferred in the registers a0, a1, a2, a3. Here, for example, is the code for a function that puts one argument in the register a0 and makes a system call 0x11. I assume the reader is familiar with gcc inline assembler
static inline int syscall_demo(int arg1) {
long __res;
__asm__ volatile (
"move $a0, %2\n\t"
"li $v0, %1\n\t"
"syscall\n\t"
"move %0, $v0"
: "=r" (__res)
: "I" (0x11),
"r" ((long)(arg1)));
return __res;
}
Of course, it’s not convenient to write functions for each type, which is why macros are used. The following is a macro code that declares a system call function with one parameter.
#define __SYSCALL1(NR,type,name,type1,arg1) \
static inline type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ( \
"move $a0, %2\n\t" \
"li $v0, %1\n\t" \
"syscall\n\t" \
"move %0, $v0" \
\
: "=r" (__res) \
: "I" (NR), \
"r" ((long)(arg1))); \
return __res; \
}
The code for system calls with a different number of parameters is similar to the one given.
Putting it all together. We have used several tests in the project, including the one below.
SYSCALL1(1,int,syscall_1,int,arg1);
TEST_CASE("calling syscall with one argument") {
test_assert_equal(syscall_1(1), 1);
}
The SYSCALL macro is expanded into the above code with an inline assembler, it is substituted with 1 (the first argument of the macro) as the call name syscall_1 (third parameter), the return type is int (the second macro parameter), and the type of the variable is also int (fourth macro parameter )
The test itself checks that the result of the call to syscall_1 (1) will be equal to one.
Related Links
- Implementing Interrupts for MIPS in the Das U-boot Loader
- Implementing interrupts for MIPS in the Linux kernel
- Code of our project
Conclusion
In conclusion, for those who are interested in more detailed understanding of this topic, I recommend taking the project code and playing qemu (the wiki pages describe how to start). Understanding how it works is much easier if you walk around breakpoints with all the Eclipse amenities.
Thanks to everyone who read to the end! I will be glad to hear comments, recommendations and suggestions.