In this lecture, we will learn the system calls and see how to print message in the user programs using this mechanism.
Setting up the system calls is pretty much the same as we did with hardware interrupts
.
For example, we need to set the corresponding interrupt gate descriptor and process the request in the handler
.
The major difference is that we use instruction int to fire the software interrupt.
In this example, we want to print message. The print function
will execute int instruction and the vector number we use in this case is 80.
When we run int instruction, cpu will retrieve the corresponding descriptor in the idt just as we have learned
in the section exceptions and interrupts handling.
Then we enter kernel mode and this request is processed in the handler. After it performs some checks of the request
,
it then gets to system call part and the write screen function will finally be called.
At this point, we should see the message on the screen.
As you see, we need to prepare the kernel part and user part.
So let’s start off by implementing the kernel part.
Because we use vector number 80 in this example, we will initialize it in trap.c file.
In function initialize idt,
we add initialize idt entry.
The vector number is 80 in this example,
and this is a hexadecimal number. The entry point for this interrupt is
sysint
and the attribute of the idt entry is different from others.
Here we set it to
ee. The difference is that we set the dpl to 3 instead of 0. Because we will fire the interrupt in ring3,
the dpl should be set to 3,
otherwise we have no privilege to access this descriptor.
Ok we add the declaration of the function in the header file.
The form of this type of functions are the same, so we just copy and paste the declaration
and change the name to sysint.
Alright, the next thing we are going to do is we are going to add the entry point in the trap assembly file.
So we define it here.
sysint
and push 0 which means it includes no error code.
Then push trap number 80 so that we know this is the software interrupt in the handler function
.
After we push two values, we jump to trap.
OK.
Ok declare it global
and we are done.
In the trap.c file,
we add one case in handler function.
So in the switch statement, we write case
80
This is the trap number we pushed on the stack.
next we call system call function
and pass tf to this function where we will use tf to reference the data in the user stack.
Then we break.
Since this function is defined in syscall.c file, we include the header file.
Let's take a look at this file.
As you see, we defined a type system call. In the system, the type of system call functions is defined
with system call type.
So let's open the syscall.c file and see what's in this file.
The array system calls holds the system call function pointers. The number of the functions we can use is 10
which is enough for the moment.
The function initialize system call simply saves the system write function in the first element of the array
and this is the only function we implement here.
The system write function is the function we want to use in the user mode.
When we call print function in the user program, we just convert the message to the string, prepare the string as argument on the user stack
and execute int instruction. When we run system write function,
all we need to do is call function write screen and pass the string to it.
Here what the argptr actually referenced
is the data on the stack in user mode.
The attribute of the character is e. If there is no mistake in the string, we should see the message in yellow
printed on the screen.
Return the count of character being printed and we are done.
So how do we find the correct system call.
This is done by the function system call.
You can see, the trap frame is passed in the function and we copy the value of rax to i.
Here rax rdi rsi are the values we pushed on the stack in the trap when the interrupt is fired, here,
which means they are the value of registers when we execute int instruction in the user mode.
The rax is used to hold the index number of system call, 0 in this case. By specifying the index number,
we can easily find the correct system call.
The rdi holds the parameters count.
The rsi points to the arguments we passed to the function. We will see how to set them in the user applications in a moment
.
Then we perform some checks. Because the user programs are in the untrusted area,
we have to make sure the requests are valid.
Here we just do some simple checks.
For example, the number of parameters should be 0 or positive.
The index number must be 0, since we only have one system call in this example.
If the check fails, we return the error code to rax register and return.
When we return from ring0 to ring3, the value -1 will be popped in rax register.
And the return value we will get in the user program is this value.
If the checks pass, we can call system call function using index specified in rax and pass the arguments pointer
.
We also copy the return value to rax.
So in this example, the system call we actually invoke is system write.
One more thing left to do before we get to user part is that we extern screen buffer and use it in this file
.
If you don’t want to reference the screen buffer. You can use it only in write screen function.
So We need to make some changes in print file.
The first thing we are going to do is we are going to remove keyword static so that we can use
function write screen in syscall module.
The screen buffer is not used outside of the function. Instead, we define a variable
sb and initialize it with the address of screen buffer.
In the printk function,
we remove the screen buffer.
OK, we add the declaration.
of write screen function in the header file.
Ok back to syscall.c file.
now we get rid of screen buffer
and remove the argument.
In the build script,
we add the syscall file.
Alright, the kernel part is done, let’s get to user part.
Note that we cannot use c standard library when we write user program. So the first thing we are going to do
is we are going to write a simple library for user program.
We create a new folder
called lib.
The library includes print function, so we copy print file.
The screen buffer is not used
as well as the header file.
Function write screen is not used either.
Also, we change the printk to printf.
In the program, we can call printf just like we did in the normal applications.
OK, at the end of the printf function,
we change function write screen to writeu and pass the arugments buffer and buffer size.
This function will then execute int instruction to send the request to the kernel.
The return value indicates the count of characters being printed on the screen,
so we copy it to buffer size.
Because it is not defined in this file, we extern
writeu
Ok. The function write u is implemented in assembly code.
So we create a new file
syscall.asm.
In the text section,
we define writeu.
What we are going to do first is we are going to allocate 16-byte space on the stack for the arguments.
so we subtract rsp 16
Then we zero rax,
remember rax holds the index number of system call function. The index number is 0 for write screen function.
Next we copy the first and second arguments to the new allocated space.
Rdi holds the number of arguments we pass to the kernel. In this case we pass 2 arguments,
so we save 2 in rdi.
Then we mov rsi, rsp
and now register rsi points to the address of the arguments.
Ok with the arguments prepared, we execute int instruction, the vector number is 80.
After we return from the kernel, the return value is saved in rax.
Then we add rsp 16 to restore the stack and return to the caller.
Also, we global write u to make it accessible in the print module.
OK, that's it for this small library.
First, we navigate to the lib folder.
we type nasm -f elf64 -o syscall.o, then the input name syscall.asm.
As you see, we assemble it to the object file. press enter.
Compiling print.c file is the same as we did with other c files. So we just copy and paste it here.
Ok with the object files prepared, we can create library by typing ar rcs and the library name,
lib.a in this case, the two object files print.o and syscall.o
Alright, you'll see we have lib.a file.
At this point, we can write our first program.
So we create a new file
called main.c
We first include
lib.h
And in the main function,
we do nothing but print
process starts.
We haven’t created lib.h, let’s create lib.h
Just as we did before, we first add a guard.
the only function we add in this file
is printf function which is used in the main file.
Before we compile the file, we need another file which prepare the environment for main function.
In the normal cases where we use c standard library, we don’t need to do this because it did it for us.
But in our system, we have to do everything ourselves.
So let's do it now.
This file is written in assembly language and we call it start.
In the text section, we define label start
and simply call function main.
After we get to ring3, the start is the first function being called and the stack is prepared
when we set the process entry in the process module. So we simply call the main function.
Because we haven’t implemented exit service in the kernel, here after we return,
we just jump to the current position which acts as an infinite loop.
And note that we cannot use hlt instruction, because we are in user mode.
OK.
Next we global start so that linker will find this label and extern main function.
OK, we are done.
Just as we need linker script when building the kernel, we also need it building user program.
So we copied the linker file.
and in this file we change the virtual address to 400000. Remember this address is the virtual address
we set to the base of the user program in the process module. If the virtual address is not 400000
in this example, the memory referenced in the user program will not be correct.
Since we don't use the symbol end in the user program, we just remove it.
Now we can build the user program.
The files we need here includes lib.a, main.c and start.asm file. We create a build script.
here we just copy one.
As you see, we assemble the start file and compile main.c file.
Then we add them in the linker command as well as the library file. The last command converts the elf file to binary file
.
Ok let's build the project.
You can see user.bin file is generated.
The next thing we will do is write the user.bin file
to boot img so that we can read it in the memory and run the program.
So we copy the user.bin file
to the boot folder.
In the build script of the kernel project,
we can see kernel file takes up 100 blocks starting from 6. So we can write the user.bin
into boot image from 106
the count of blocks we write is 10 which is enough for the program.
Remember in the loader file, we loaded the kernel using bios service.
Here.
So we define label
load user
and use the same code to load user binary file.
We specify that we want to read 10 sectors of data in memory from sector 106.
The memory address we want to read into is for example 20000.
So we set the segment part of the address to
2000. After we load the user file in memory, we can copy the data to the new allocated page in process module
.
And the main function is not used in this case.
We also removed the declaration of main function.
The address is 20000, notice that we use p2v to convert the physical address to virtual address
and the size here is 10 sectors.
OK, the last thing we need to do is add init system call function in main.c file
We include syscall header file.
and init
system call
Alright, let’s build the project and test it out.
We navigate to the boot folder.
As you can see, process starts is printed on screen.
OK, that's it for this lecture.
See you in the next video.