In this lecture we will implement print function. The print function is really important for us.
Because when we add new code to the kernel, we need to know if it works and what errors we could encounter.
Printing message on screen is the easiest way to get the information.
So we will write the simplified version of print function like the printf function we used in c language.
In the preivous lectures, we enabled interrupt in the kernel file for testing interrupt handlers.
In following lectures,
we will not deal with interrupt until we get to ring3.
So we disable interrupts and the code for setting up ss register can also be removed.
OK, let's get started.
We create a header file
print.h
In the header file, we add a guard so that the header file is included only once.
we define a constant line size which is 160.
Remeber we have 80 characters each line in the text mode and each characters takes up 2 bytes.
The structure screen buffer
is used for printing message on the screen.
The buffer field is the address of screen buffer, b8000 in this case. The next two items are
the column and row which represents where we will print message next.
The print function in the system is called printk function which is used in kernel mode.
The first parameter is the address of format string. Here we use const indicating that
the data pointed to by format should not be modified.
The printk function is variable argument function which means it can take a variable number of arguments.
The variable argument function is like this,
there is ... at the end of the parameters which indicates that there are parameters,
but you don’t need to specify here.
The format string tells the function how to interrupt the variable arguments. and it returns
the count of the characters it actually prints
.
Ok let’s look at the implementation of printk function in print.c file.
The header files we will use are stdint stdarg print and library files.
The printk function uses macros in the stdarg header file.
We will talk about it in a minute.
Next we define a variable called screen buffer which is only used in this file.
we initialize it with the value b8000 and the 0,0 for row and column.
In the printk function, we define a few variables. Buffer is the array which can save a total of 1024 characters
which will be printed on the screen.
So we should make sure the number of characters we want to print is not larger than this value
when we call the function. The buffer size holds the count of the characters we want to print.
The integer is used to save the value we want to print on the screen.
To make it simple, the integer we print in our system is assumed to be 64bits.
So here we use fix width data type.
Next variable is a string pointing to the characters. Then we have a variable called args,
the type is va_list defined in the header file stdarg.h.
Ok now we can initialize the args list using va_start macro so that the variables can be used with other macros later
.
The first argument is the args, then the last known argument passed to the function. Since we only have one argument passed in
which is format,
this is the last known argument.
We write format here.
At the end of the function, we also need to do cleanup using va end before we return.
Ok now let’s parse the format string.
The format string used in printk function is much like that in printf in c language.
There are a few format specifiers we can use in printf function such as %d signed integer type,
%s string, etc. For simplicity, we only support x d s and u specifiers
and this is enough for our system.
Ok, first off, we use for loop to loop through each character in the format string.
The format string here is null-terminated string. So if we get the null character, it means we reach the end of the format string.
In the for loop, we check to see if the character is %. If the character is not %, it is a regular character
and we simply copy the character to the buffer, increment the buffer size
and move to the next one.
If it is %, we will check the next character. Here we use switch statement.
We have x hexadecimal type, u unsigned integer type, d signed integer type and s string specifiers.
As for other characters, we deal with it in the default here. Since it is not the specifier we support,
we simply copy the character % to the buffer, decrement index and continue the loop.
If the specifier is s, then we will get the corresponding pointer and read characters from it.
So we use va arg macro to retrieve the pointer. The first argument is args variable,
the next one is the type of the variable we want to retrieve which is a pointer to type char.
And we use variable string to hold the address of the string. Then we call function read string into the buffer
.
The first argument is buffer, then the location from which we write the string, buffer size in this case.
The last one is the address of the string.
The function returns the number of characters it writes into the buffer. So we add the result to update the buffer size,
then we break and continue the for loop.
Ok let’s see the read string function.
The first parameter is buffer, then the position and the address of the string.
The first thing we will do is define a variable called index. we initialize it with the value 0.
The for loop here is used to copy each character of the string to the buffer.
The null character means the end of the string. We copy each character in the string to the buffer
and update the position.
When we finish copying characters, we return the number of characters being copied,
the index here represents the number of characters we have copied into the buffer.
OK.
Next specifier we will implement is x. In our system, when we use x specifier,
it means that we want to print 64-bit hexadecimal value.
So we retrieve the value by specifying the type as 64-bit integer.
The value is stored in the variable integer.
Then we call function hex to string which convert the hex to characters and store it in the buffer.
The function also returns the number of characters copied in the buffer.
We pass the buffer, buffer size and the value we want to convert
.
Notice that this is unsigned integer type.
When we return, we update the buffer size and continue the process.
As you see, the parameters it takes is the same as read string except the last one.
In function hex to string, we need to define a digits buffer to save the converted characters
and digits map which is used to get the correct characters. Size stores the number of characters converted.
We use do while loop to convert the value.
We copy each of the converted characters to the buffer and update the size.
Here the digits mod 16 will get the first digit on the right and we use it as an index
to get the correct character. As you can see, the characters in the digit map are arranged in a way
that the index of the character actually represents the value.
For example, if the digits is A, then we mod 16, the result is 10,
the value at the index 10 of the map is A.
Ok, then we need to divide the value by 16 which will make the next digit become the first digit
.
Now we can repeat the process until the value is 0.
So each digit mode 16
and we use the result as an index to find the correct character in the digits map.
Notice that when we finish, the characters in the buffer are in the reverse order.
Because we convert the digits and copy them into the buffer from right to left.
So we have to copy them to the buffer backward. We use for loop to do that.
The index of the last character is size - 1 and the first character is 0.
So we copy it to the buffer backward and update the position.
At the end of the characters, we also add the character H which represents this is a hexadecimal value.
If you like, you can add prefix 0x to the buffer instead of suffix h.
Ok we return size+1, because H is added in the end.
Next we get to the specifier u.
The process is pretty much the same. We retrieve the unsigned integer value
and call function unsigned decimal to string to convert the value to characters and copy them to the buffer
.
Also we update the buffer size after the function returns.
Let's look at this function.
This function also deals with unsigned 64-bit integers.
As we did in function hexadecimal to string, we define a digit map, since this is decimal value,
we only need 10 digits 0 through 9, digit buffer and the size.
We use do while loop to convert the decimal value. Instead of mod 16, the digits mod 10 in this case
and copy the character to the buffer.
Then we divide the digits by 10 which makes the second digit become the first one.
If the value is non- zero,
we repeat the process. When it is done,
the size holds the number of the characters being copied to the buffer.
Remember the converted characters copied to the buffer is in the reverse order.
We copy them to buffer from the last character to the first one using for loop.
And then we simply return the size.
The last specifier is d
signed integer type.
The function we use is called decimal to string. The arguments are the same as unsigned decimal to string
except that the integer is signed integer.
Let's define this function.
The first thing we are going to do is we are going to check the value passed in is positive or negative value
.
If it is negative value, we convert it to the positive
and the first character we will print is minus sign.
So we copy the minus sign to the buffer and update the size to 1.
Note that we will get overflow if the digits here is the minimum value. But the result should be correct
based on the data type and systems we use.
Alright, the actual work of converting value is done in function unsigned decimal to string. We pass the buffer,
position and the digits we want to convert
.
If it is positive value we simply call the unsigned decimal to string function. The return value is
the number of the characters it actually prints, and we update the size and return.
Ok that’s all the formats we can support in the print function. Let’s continue. After we exit out for loop,
we have characters ready to write into the screen buffer. So we call write screen to do that.
In this function, we need the buffer which contains the characters, the buffer size,
the address of the screen buffer and the character attribute. The structure includes current position
which tells us where we will write the character.
We copy it to the variable column and row.
Then we use for loop to copy the character to the screen buffer.
The first if statement is used for scrolling screen. We will discuss it in a moment. If the character is not new line,
we just copy the character and the attribute to the buffer.
The address is stored in the field buffer of the structure. Here we multiply row by the macro line size
to get the address of current line and add the column value. Then Update column and check to see
if column is larger than or equal to 80. If it is, we increment row and set column to 0
so that characters will be printed at the beginning of the next line.
If we find the new line in the buffer, we will change the position to the next line
so that the next character will be printed from there.
We increment row and set column to 0.
Suppose we have too much info printed on the screen and there is no room to print,
what we will do is scroll the screen.
So if row is greater than or equal to 25, we copy the characters which is in the second line through the last line
to the first 24 lines and leave the last line empty. It’s like scroll the page.
We use memcpy which we have implemented in the last lecture to copy the characters.
And then use memset to empty the last line of the screen buffer.
And also, don’t forget to decrement row number, since we only move one line up.
Ok, we could see the screen get scrolled up when the message is printed out of the screen.
At the end of the function, write the current position back to the structure for later use.
OK, back to the print function.
we pass the buffer, buffer size and the address of the structure,
this is the global variable screen buffer. And the attribute, here we use f which means the character is printed in white
.
Alright,
return the buffer size which is the number of the characters being printed and we are done.
In the main.c file, we include print header file and print the test message. For example,
hello and welcome.
and the hexadecimal value 123456789abcd
this is 64-bit value.
The first print function prints string. The next one prints the variable in hexadecimal value. Before we test our program,
we need to add print file in the build script.
So we add print.c file and in the linker command, we add print.o file.
OK, let's build the project and test it out.
OK, as you see, hello and welcome is printed and the hexadecimal value 123456789abcd
,
is printed and suffix H is
also added here.
That's it for this lecture.
See you in the next video.