1 00:00:01,500 --> 00:00:07,740 In this lecture, we will learn the system calls and see how to print message in the user programs using this mechanism. 2 00:00:07,740 --> 00:00:13,950 Setting up the system calls is pretty much the same as we did with hardware interrupts 3 00:00:13,950 --> 00:00:14,640 . 4 00:00:16,300 --> 00:00:22,120 For example, we need to set the corresponding interrupt gate descriptor and process the request in the handler 5 00:00:22,120 --> 00:00:22,600 . 6 00:00:23,140 --> 00:00:29,350 The major difference is that we use instruction int to fire the software interrupt. 7 00:00:29,440 --> 00:00:33,280 In this example, we want to print message. The print function 8 00:00:33,280 --> 00:00:38,600 will execute int instruction and the vector number we use in this case is 80. 9 00:00:39,460 --> 00:00:46,000 When we run int instruction, cpu will retrieve the corresponding descriptor in the idt just as we have learned 10 00:00:46,000 --> 00:00:49,510 in the section exceptions and interrupts handling. 11 00:00:50,530 --> 00:00:57,010 Then we enter kernel mode and this request is processed in the handler. After it performs some checks of the request 12 00:00:57,010 --> 00:00:57,740 , 13 00:00:58,210 --> 00:01:05,250 it then gets to system call part and the write screen function will finally be called. 14 00:01:05,260 --> 00:01:07,180 At this point, we should see the message on the screen. 15 00:01:08,210 --> 00:01:11,900 As you see, we need to prepare the kernel part and user part. 16 00:01:13,030 --> 00:01:15,670 So let’s start off by implementing the kernel part. 17 00:01:18,680 --> 00:01:24,740 Because we use vector number 80 in this example, we will initialize it in trap.c file. 18 00:01:27,250 --> 00:01:29,290 In function initialize idt, 19 00:01:30,660 --> 00:01:32,790 we add initialize idt entry. 20 00:01:36,330 --> 00:01:38,760 The vector number is 80 in this example, 21 00:01:42,830 --> 00:01:47,750 and this is a hexadecimal number. The entry point for this interrupt is 22 00:01:49,670 --> 00:01:50,480 sysint 23 00:01:51,690 --> 00:01:56,140 and the attribute of the idt entry is different from others. 24 00:01:56,160 --> 00:01:57,120 Here we set it to 25 00:01:58,590 --> 00:02:05,930 ee. The difference is that we set the dpl to 3 instead of 0. Because we will fire the interrupt in ring3, 26 00:02:05,940 --> 00:02:10,259 the dpl should be set to 3, 27 00:02:10,650 --> 00:02:13,890 otherwise we have no privilege to access this descriptor. 28 00:02:15,940 --> 00:02:19,390 Ok we add the declaration of the function in the header file. 29 00:02:22,910 --> 00:02:28,490 The form of this type of functions are the same, so we just copy and paste the declaration 30 00:02:30,280 --> 00:02:32,140 and change the name to sysint. 31 00:02:35,690 --> 00:02:41,840 Alright, the next thing we are going to do is we are going to add the entry point in the trap assembly file. 32 00:02:46,840 --> 00:02:48,220 So we define it here. 33 00:02:50,100 --> 00:02:51,060 sysint 34 00:02:53,510 --> 00:02:58,850 and push 0 which means it includes no error code. 35 00:02:59,840 --> 00:03:06,840 Then push trap number 80 so that we know this is the software interrupt in the handler function 36 00:03:06,860 --> 00:03:07,340 . 37 00:03:08,410 --> 00:03:11,230 After we push two values, we jump to trap. 38 00:03:12,990 --> 00:03:13,440 OK. 39 00:03:17,100 --> 00:03:18,570 Ok declare it global 40 00:03:21,470 --> 00:03:22,240 and we are done. 41 00:03:23,450 --> 00:03:24,980 In the trap.c file, 42 00:03:27,670 --> 00:03:30,490 we add one case in handler function. 43 00:03:31,940 --> 00:03:35,750 So in the switch statement, we write case 44 00:03:37,340 --> 00:03:37,950 80 45 00:03:40,260 --> 00:03:42,600 This is the trap number we pushed on the stack. 46 00:03:43,710 --> 00:03:45,660 next we call system call function 47 00:03:47,950 --> 00:03:54,400 and pass tf to this function where we will use tf to reference the data in the user stack. 48 00:03:56,180 --> 00:03:57,070 Then we break. 49 00:04:00,250 --> 00:04:05,470 Since this function is defined in syscall.c file, we include the header file. 50 00:04:07,960 --> 00:04:09,430 Let's take a look at this file. 51 00:04:13,810 --> 00:04:20,740 As you see, we defined a type system call. In the system, the type of system call functions is defined 52 00:04:20,890 --> 00:04:22,230 with system call type. 53 00:04:23,350 --> 00:04:26,800 So let's open the syscall.c file and see what's in this file. 54 00:04:32,440 --> 00:04:38,860 The array system calls holds the system call function pointers. The number of the functions we can use is 10 55 00:04:38,860 --> 00:04:40,930 which is enough for the moment. 56 00:04:41,950 --> 00:04:46,840 The function initialize system call simply saves the system write function in the first element of the array 57 00:04:46,840 --> 00:04:50,980 and this is the only function we implement here. 58 00:04:52,290 --> 00:04:58,350 The system write function is the function we want to use in the user mode. 59 00:04:58,350 --> 00:05:04,380 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 60 00:05:04,380 --> 00:05:10,410 and execute int instruction. When we run system write function, 61 00:05:10,800 --> 00:05:15,400 all we need to do is call function write screen and pass the string to it. 62 00:05:16,470 --> 00:05:18,960 Here what the argptr actually referenced 63 00:05:19,170 --> 00:05:21,330 is the data on the stack in user mode. 64 00:05:22,560 --> 00:05:29,160 The attribute of the character is e. If there is no mistake in the string, we should see the message in yellow 65 00:05:29,160 --> 00:05:30,270 printed on the screen. 66 00:05:31,500 --> 00:05:34,580 Return the count of character being printed and we are done. 67 00:05:36,470 --> 00:05:38,840 So how do we find the correct system call. 68 00:05:39,850 --> 00:05:42,250 This is done by the function system call. 69 00:05:43,510 --> 00:05:52,840 You can see, the trap frame is passed in the function and we copy the value of rax to i. 70 00:05:52,840 --> 00:06:00,460 Here rax rdi rsi are the values we pushed on the stack in the trap when the interrupt is fired, here, 71 00:06:00,460 --> 00:06:04,750 which means they are the value of registers when we execute int instruction in the user mode. 72 00:06:07,540 --> 00:06:14,800 The rax is used to hold the index number of system call, 0 in this case. By specifying the index number, 73 00:06:14,800 --> 00:06:17,500 we can easily find the correct system call. 74 00:06:18,750 --> 00:06:20,760 The rdi holds the parameters count. 75 00:06:21,900 --> 00:06:27,930 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 76 00:06:27,930 --> 00:06:29,340 . 77 00:06:31,940 --> 00:06:37,020 Then we perform some checks. Because the user programs are in the untrusted area, 78 00:06:37,460 --> 00:06:40,630 we have to make sure the requests are valid. 79 00:06:40,640 --> 00:06:42,200 Here we just do some simple checks. 80 00:06:42,590 --> 00:06:46,670 For example, the number of parameters should be 0 or positive. 81 00:06:47,150 --> 00:06:53,900 The index number must be 0, since we only have one system call in this example. 82 00:06:53,900 --> 00:06:57,170 If the check fails, we return the error code to rax register and return. 83 00:06:58,240 --> 00:07:05,650 When we return from ring0 to ring3, the value -1 will be popped in rax register. 84 00:07:05,650 --> 00:07:08,530 And the return value we will get in the user program is this value. 85 00:07:10,480 --> 00:07:17,920 If the checks pass, we can call system call function using index specified in rax and pass the arguments pointer 86 00:07:17,920 --> 00:07:18,490 . 87 00:07:19,390 --> 00:07:21,280 We also copy the return value to rax. 88 00:07:22,480 --> 00:07:26,770 So in this example, the system call we actually invoke is system write. 89 00:07:27,870 --> 00:07:34,530 One more thing left to do before we get to user part is that we extern screen buffer and use it in this file 90 00:07:34,530 --> 00:07:35,240 . 91 00:07:36,060 --> 00:07:40,910 If you don’t want to reference the screen buffer. You can use it only in write screen function. 92 00:07:41,850 --> 00:07:44,340 So We need to make some changes in print file. 93 00:07:47,500 --> 00:07:53,590 The first thing we are going to do is we are going to remove keyword static so that we can use 94 00:07:53,590 --> 00:07:55,510 function write screen in syscall module. 95 00:07:56,950 --> 00:08:02,250 The screen buffer is not used outside of the function. Instead, we define a variable 96 00:08:04,670 --> 00:08:08,330 sb and initialize it with the address of screen buffer. 97 00:08:10,220 --> 00:08:11,690 In the printk function, 98 00:08:15,630 --> 00:08:17,640 we remove the screen buffer. 99 00:08:20,300 --> 00:08:22,580 OK, we add the declaration. 100 00:08:24,320 --> 00:08:26,690 of write screen function in the header file. 101 00:08:33,270 --> 00:08:35,820 Ok back to syscall.c file. 102 00:08:37,900 --> 00:08:39,970 now we get rid of screen buffer 103 00:08:43,140 --> 00:08:44,760 and remove the argument. 104 00:08:48,010 --> 00:08:49,450 In the build script, 105 00:08:53,180 --> 00:08:54,830 we add the syscall file. 106 00:08:55,880 --> 00:08:59,810 Alright, the kernel part is done, let’s get to user part. 107 00:09:02,190 --> 00:09:08,130 Note that we cannot use c standard library when we write user program. So the first thing we are going to do 108 00:09:08,130 --> 00:09:11,030 is we are going to write a simple library for user program. 109 00:09:11,040 --> 00:09:14,040 We create a new folder 110 00:09:15,200 --> 00:09:15,860 called lib. 111 00:09:17,650 --> 00:09:21,490 The library includes print function, so we copy print file. 112 00:09:27,230 --> 00:09:28,970 The screen buffer is not used 113 00:09:29,850 --> 00:09:31,230 as well as the header file. 114 00:09:35,680 --> 00:09:38,040 Function write screen is not used either. 115 00:09:39,900 --> 00:09:42,750 Also, we change the printk to printf. 116 00:09:43,700 --> 00:09:48,380 In the program, we can call printf just like we did in the normal applications. 117 00:09:49,360 --> 00:09:51,940 OK, at the end of the printf function, 118 00:09:53,820 --> 00:10:02,220 we change function write screen to writeu and pass the arugments buffer and buffer size. 119 00:10:03,240 --> 00:10:07,840 This function will then execute int instruction to send the request to the kernel. 120 00:10:09,180 --> 00:10:12,780 The return value indicates the count of characters being printed on the screen, 121 00:10:13,170 --> 00:10:15,600 so we copy it to buffer size. 122 00:10:17,020 --> 00:10:21,070 Because it is not defined in this file, we extern 123 00:10:22,460 --> 00:10:22,820 writeu 124 00:10:27,720 --> 00:10:35,460 Ok. The function write u is implemented in assembly code. 125 00:10:35,460 --> 00:10:36,090 So we create a new file 126 00:10:39,770 --> 00:10:42,220 syscall.asm. 127 00:10:46,100 --> 00:10:47,330 In the text section, 128 00:10:49,400 --> 00:10:51,080 we define writeu. 129 00:10:53,430 --> 00:10:59,430 What we are going to do first is we are going to allocate 16-byte space on the stack for the arguments. 130 00:10:59,940 --> 00:11:02,310 so we subtract rsp 16 131 00:11:04,020 --> 00:11:05,380 Then we zero rax, 132 00:11:06,970 --> 00:11:13,900 remember rax holds the index number of system call function. The index number is 0 for write screen function. 133 00:11:15,190 --> 00:11:19,210 Next we copy the first and second arguments to the new allocated space. 134 00:11:21,330 --> 00:11:28,560 Rdi holds the number of arguments we pass to the kernel. In this case we pass 2 arguments, 135 00:11:28,570 --> 00:11:30,880 so we save 2 in rdi. 136 00:11:31,900 --> 00:11:34,570 Then we mov rsi, rsp 137 00:11:35,970 --> 00:11:39,660 and now register rsi points to the address of the arguments. 138 00:11:40,820 --> 00:11:47,580 Ok with the arguments prepared, we execute int instruction, the vector number is 80. 139 00:11:50,050 --> 00:11:53,890 After we return from the kernel, the return value is saved in rax. 140 00:11:55,100 --> 00:12:00,230 Then we add rsp 16 to restore the stack and return to the caller. 141 00:12:01,180 --> 00:12:08,490 Also, we global write u to make it accessible in the print module. 142 00:12:09,430 --> 00:12:11,470 OK, that's it for this small library. 143 00:12:12,780 --> 00:12:15,150 First, we navigate to the lib folder. 144 00:12:17,910 --> 00:12:31,660 we type nasm -f elf64 -o syscall.o, then the input name syscall.asm. 145 00:12:32,890 --> 00:12:37,360 As you see, we assemble it to the object file. press enter. 146 00:12:40,240 --> 00:12:46,930 Compiling print.c file is the same as we did with other c files. So we just copy and paste it here. 147 00:12:48,440 --> 00:12:58,010 Ok with the object files prepared, we can create library by typing ar rcs and the library name, 148 00:12:58,010 --> 00:13:08,900 lib.a in this case, the two object files print.o and syscall.o 149 00:13:10,480 --> 00:13:14,170 Alright, you'll see we have lib.a file. 150 00:13:15,220 --> 00:13:17,920 At this point, we can write our first program. 151 00:13:18,920 --> 00:13:20,240 So we create a new file 152 00:13:21,260 --> 00:13:22,460 called main.c 153 00:13:26,470 --> 00:13:27,790 We first include 154 00:13:30,810 --> 00:13:31,410 lib.h 155 00:13:33,960 --> 00:13:35,250 And in the main function, 156 00:13:39,060 --> 00:13:42,660 we do nothing but print 157 00:13:43,950 --> 00:13:44,640 process starts. 158 00:13:48,540 --> 00:13:52,810 We haven’t created lib.h, let’s create lib.h 159 00:13:58,060 --> 00:14:01,360 Just as we did before, we first add a guard. 160 00:14:04,180 --> 00:14:06,100 the only function we add in this file 161 00:14:06,120 --> 00:14:09,960 is printf function which is used in the main file. 162 00:14:11,020 --> 00:14:16,330 Before we compile the file, we need another file which prepare the environment for main function. 163 00:14:17,260 --> 00:14:22,780 In the normal cases where we use c standard library, we don’t need to do this because it did it for us. 164 00:14:22,780 --> 00:14:27,100 But in our system, we have to do everything ourselves. 165 00:14:27,850 --> 00:14:29,000 So let's do it now. 166 00:14:29,740 --> 00:14:33,520 This file is written in assembly language and we call it start. 167 00:14:36,320 --> 00:14:39,410 In the text section, we define label start 168 00:14:41,160 --> 00:14:43,200 and simply call function main. 169 00:14:44,480 --> 00:14:50,630 After we get to ring3, the start is the first function being called and the stack is prepared 170 00:14:50,630 --> 00:14:55,490 when we set the process entry in the process module. So we simply call the main function. 171 00:14:56,560 --> 00:15:02,920 Because we haven’t implemented exit service in the kernel, here after we return, 172 00:15:02,920 --> 00:15:06,910 we just jump to the current position which acts as an infinite loop. 173 00:15:08,440 --> 00:15:12,430 And note that we cannot use hlt instruction, because we are in user mode. 174 00:15:13,520 --> 00:15:13,950 OK. 175 00:15:13,970 --> 00:15:20,510 Next we global start so that linker will find this label and extern main function. 176 00:15:22,250 --> 00:15:23,170 OK, we are done. 177 00:15:24,270 --> 00:15:29,670 Just as we need linker script when building the kernel, we also need it building user program. 178 00:15:30,820 --> 00:15:32,350 So we copied the linker file. 179 00:15:38,930 --> 00:15:46,570 and in this file we change the virtual address to 400000. Remember this address is the virtual address 180 00:15:46,580 --> 00:15:53,600 we set to the base of the user program in the process module. If the virtual address is not 400000 181 00:15:53,600 --> 00:15:58,040 in this example, the memory referenced in the user program will not be correct. 182 00:16:00,120 --> 00:16:04,970 Since we don't use the symbol end in the user program, we just remove it. 183 00:16:08,100 --> 00:16:09,930 Now we can build the user program. 184 00:16:11,980 --> 00:16:20,080 The files we need here includes lib.a, main.c and start.asm file. We create a build script. 185 00:16:20,080 --> 00:16:23,470 here we just copy one. 186 00:16:26,120 --> 00:16:30,630 As you see, we assemble the start file and compile main.c file. 187 00:16:31,400 --> 00:16:37,400 Then we add them in the linker command as well as the library file. The last command converts the elf file to binary file 188 00:16:37,520 --> 00:16:39,020 . 189 00:16:39,800 --> 00:16:41,240 Ok let's build the project. 190 00:16:47,110 --> 00:16:50,080 You can see user.bin file is generated. 191 00:16:51,120 --> 00:16:52,710 The next thing we will do is write the user.bin file 192 00:16:52,750 --> 00:16:58,320 to boot img so that we can read it in the memory and run the program. 193 00:16:59,100 --> 00:17:01,230 So we copy the user.bin file 194 00:17:03,640 --> 00:17:04,569 to the boot folder. 195 00:17:07,700 --> 00:17:10,040 In the build script of the kernel project, 196 00:17:12,069 --> 00:17:18,910 we can see kernel file takes up 100 blocks starting from 6. So we can write the user.bin 197 00:17:18,910 --> 00:17:21,550 into boot image from 106 198 00:17:22,920 --> 00:17:27,180 the count of blocks we write is 10 which is enough for the program. 199 00:17:28,400 --> 00:17:33,170 Remember in the loader file, we loaded the kernel using bios service. 200 00:17:34,900 --> 00:17:35,320 Here. 201 00:17:36,530 --> 00:17:37,970 So we define label 202 00:17:40,630 --> 00:17:41,890 load user 203 00:17:43,600 --> 00:17:46,850 and use the same code to load user binary file. 204 00:17:48,500 --> 00:17:55,280 We specify that we want to read 10 sectors of data in memory from sector 106. 205 00:17:57,310 --> 00:18:02,060 The memory address we want to read into is for example 20000. 206 00:18:02,590 --> 00:18:05,590 So we set the segment part of the address to 207 00:18:06,750 --> 00:18:15,630 2000. After we load the user file in memory, we can copy the data to the new allocated page in process module 208 00:18:15,630 --> 00:18:16,050 . 209 00:18:21,590 --> 00:18:24,230 And the main function is not used in this case. 210 00:18:26,140 --> 00:18:28,660 We also removed the declaration of main function. 211 00:18:32,490 --> 00:18:40,710 The address is 20000, notice that we use p2v to convert the physical address to virtual address 212 00:18:40,720 --> 00:18:42,630 and the size here is 10 sectors. 213 00:18:44,270 --> 00:18:50,930 OK, the last thing we need to do is add init system call function in main.c file 214 00:18:53,750 --> 00:18:57,890 We include syscall header file. 215 00:18:59,650 --> 00:19:00,820 and init 216 00:19:02,360 --> 00:19:03,110 system call 217 00:19:05,130 --> 00:19:07,640 Alright, let’s build the project and test it out. 218 00:19:09,980 --> 00:19:11,640 We navigate to the boot folder. 219 00:19:21,110 --> 00:19:24,380 As you can see, process starts is printed on screen. 220 00:19:26,280 --> 00:19:27,900 OK, that's it for this lecture. 221 00:19:28,080 --> 00:19:29,400 See you in the next video.