📜 ⬆️ ⬇️

MIPS system calls

This summer appplemac has published an article dedicated to the study of the MIPS assembler . In it, in particular, the syscall command generating a system call was considered. The author focused on explaining the MIPS assembler, and in my opinion, he didn’t say in sufficient detail what a system call is. At that moment I was engaged in the transfer of the project under the MIPS architecture, I dealt with interrupts, exceptions and 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 calls mechanism works in MIPS. You can consider it as a supplement to that article about the assembler.

Introduction

First of all, you need to understand what system calls are and why they are needed.
Wikipedia gives the following definition:
System Call (English system call) in programming and computing - the application program to the core of the operating system to perform any operation.
From the programmer’s point of view, a system call usually looks like a subroutine or function call from the system library. However, a system call as a special case of calling such a function or subroutine should be distinguished from a more general reference to the system library, since the latter may not require performing privileged operations.


In other words, a system call is a function call with a pre-known address and simultaneous transfer of the processor from the privileged mode (kernel mode). Switching to kernel mode allows you to execute privileged commands, such as managing virtual memory tables, disabling / enabling interrupts, and accessing data stored in the kernel.
A previously known address means that all processing functions can be represented by an array of pointers, and this handler will correspond to an index in this array. The difference between the system call and the function call is that the control to the base address + offset is transferred by the hardware, by the processor itself.

That is, the processor, having encountered an instruction that generates a system call, interrupts the sequential execution of user program commands and transfers control to the desired address while preserving the necessary information to return to the main program. This is very similar to the behavior of the processor when an exception or an external interrupt occurs, therefore, usually these subsystems are implemented in a similar way and are considered together.
')
MIPS Architecture

Let us turn to a specific implementation of these subsystems in the MIPS architecture.

In the MIPS version 2 architecture, there are several modes of operation for interrupts. They differ in the base addresses and the structures of the interrupt tables themselves.
For the base address there are two modes:
  1. In the first processor, faced with any type of exceptions, transfers control to the address a fixed address (0x80000180), the size of the handler is 128 bytes.
  2. In the second, the processor transfers control to the address specified in the CP0_EBASE register, the handler size is 256 bytes.

There are also two operation modes for the structure of the interrupt table: normal, when one handler is called in response to all exceptions, and vector mode, in which each interrupt number is assigned its own space for the handler.

These modes are set in special registers of the MIPS processor. Special registers, unlike general registers, are used by the program to control the processor itself.

In MIPS, such registers are moved to coprocessor 0. And they are accessed by special assembler commands: mfc0 - to read registers, and mtc0 - to write to the register.
Registers are addressed by the index and selector of the coprocessor. Here are some important registers for handling system calls:
TitleIndexSelectorDescription
CP0_STATUS120control flags for the processor
CP0_CAUSE130interrupt reason information
CP0_EPC140address of the command that was executed at the time of the interruption
CP0_EBASE15onebase address of the exception handling procedure

Returning to setting the exception handling modes, they are set in two registers: CP0_STATUS and P0_CAUSE, having the following format.

P0_STATUS
31-282726252423222120nineteen18-1615-876five4-32one0
CU3..CU0RPFRREMXPXBEVTSSRNMIImplIM7..IM0KXSXUxKSUERLEXLIE

CP0_CAUSE
31thirty29-28272625-24232221-1615-109-876-21-0
BdTiCEDCPCI0IVWP0IPIP0exCode0

CPU Initialization

I will consider only the first mode of operation, as the most simple and compatible with all MIPS processors. All other modes are done in a similar way.

To transfer the processor to this mode, you need to reset the BEV bit in the status register and the IV bit in the cause register.

C-shny 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 switches to the privileged mode, stores the return address in the CP0_EPC register, and writes the reason (type) for the exception to the CP0_CAUSE register (in the exception code field).

About the exception code field is to tell a little more. As mentioned above, in MIPS, as, indeed, in other architectures, interrupts, system calls and hardware exceptions are usually implemented in a similar way, in the same subsystem. That is, the first thing that 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:
CodeDesignationDescription
0IntExternal interrupt
1-3Work with virtual memory
fourADDRLReading from an unaligned address
fiveADDRSRecord at unaligned address
6IBUSError reading instructions
7DBUSError on the data bus
eightSyscallSystem call
9BKPTBreakpoint
tenRIReserved instruction
elevenCoprocessor error
12OvfArithmetic overflow
13 and aboveFloating 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 only remembers the type of the exception in the register $ k1 and calls the second-level handler, which is no longer limited in size. The call is made with the command “j”, not “jar”, ​​because the handler code is placed during the program operation (we copied it in the initialization function), and we need to have the absolute, not the relative address of the called procedure.

Another feature worth mentioning here is the k1 register.
In the MIPS architecture, there are 32 general purpose registers r0 - r31. And by convention, some registers are used in a special way, for example, the register r31 is used as a pointer to the stack, and it can be accessed by the special name sp. The same with the registers k0 (r26) and k1 (r27), the compiler does not use them, they are reserved for use in the OS kernel, and interrupt handling is just such a case of special use.

Second level handler

Let us turn to the second level handler. Its main purpose is to prepare for calling a S-function, that is, first of all, to save the remaining registers that can be used in this function itself. It is also written in assembly language.
  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 like this.
  .macro SAVE_ALL LONG_ADDI $sp, -PT_SIZE SAVE_SOME SAVE_AT SAVE_TEMP SAVE_STATIC .endm 

I will not give the source code of all nested macros. Let me just say that in the first line the stack of the interrupt frame is reserved, where all the necessary registers are saved sequentially.
SAVE_AT - the register at (r1) is reserved for use by the 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 - the necessary service registers, for example, a pointer to the stack and special registers of the coprocessor (for example, the status register), so this macro should be the first.

Then there is a choice of the correct third level handler. Pointers to third-level handlers in our project are stored in a regular array, the type of exception sets the offset. It is an offset, not an index, since the MIPS creators place in the CAUSE register an exception number with a shift of two bits to the left, so we can directly call a function from the array of pointers without performing additional arithmetic.
Then, before calling the function, we want to write the return address (ra). Finally, we pass to the handler function information about the state in which we entered the interrupt, for this we pass a pointer to the stack, and in the signature of the C-function we will specify the description (structure) of the 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; 

Level 3 handler (C code)

The code of the s-shnogo handler is the following
 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 */ } 

I hope that everything is clear from the code:


Receive a system call

Now you need to talk about how to make a system call.
The 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 in the register v0 the call number should be stored, and in the registers a0, a1, a2, a3 the transmitted parameters. Here, for example, the code of 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 is not convenient to write functions for each type, therefore macros are used. Below is the macro code that declares a single-parameter system call function.
 #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 use several tests in the project, among which is the one located 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, its number is 1 (the first macro argument) is substituted for the syscall_1 call name (the third parameter), the return type is int (the second macro parameter), and the type of the variable is also int (the fourth macro parameter ).
In the test itself, it is checked that the result of the syscall_1 (1) call will be equal to one.

Related Links

  1. Implementing interrupts for MIPS in the Das U-boot bootloader
  2. Implementing interrupts for MIPS in the Linux kernel
  3. The code of our project


Conclusion

In conclusion, for those who are interested in a more detailed understanding of this topic, I recommend taking the project code and playing qemu (the wiki pages describe how to run it). Understanding how things work is much easier if you walk along breakpoints with all the amenities of Eclipse.

Thanks to everyone who read to the end! I will be glad to hear comments, recommendations and suggestions.

Source: https://habr.com/ru/post/162229/


All Articles