Hello, in this section we use C language to implement our kernel.
C language will be the main language to use in the following lectures. We need to do some preparation first.
The compiler we use is GCC and the executable file we will generate is elf file.
Since the kernel assembly code is also in the kernel, we will see how to combine the assembly and C files together.
The nasm assembler can output elf file. We open the build script.
In the build script, we change the options to generate the elf file instead of binary file.
Elf64 means we generate the elf64 object file. The name of the object file is
kernel.o.
Then we will link the object file with other files written in c. In the kernel assembly file,
we define two sections.
OK, we opened the kernel file.
We define two sections, data section and the code section.
So we remove these two directives.
and define
data section
and text section.
The data section is used for the data defined globally such as idt, tss etc.
Ok now we copy the data in the data section.
The idt are not used here and will be moved to other files. We simply remove them.
And we copied the data
in the data section.
In the text section, the interrupt handlers are not defined in the kernel file and will be implemented in other files,
so we remove all the related code here,
such as the set handler,
load idt instruction,
set handler function
and the use entry
the handler itself.
Because we don't get to ring3 in the kernel entry, the code here is not used
and we place the kernel entry
at the end.
Now the code is initializing the TSS and PIT, PIC,
then we will enter kernel entry and jump to kernel main function in c.
Before we call the main function, we adjust the kernel stack pointer
to 200000,
which is new stack pointer we will use in the c code.
If you return from main function, we will stop here.
Normally we will jump to the main function and never return. But we only test our c function in this lecture
,
so we return from the function.
Another thing we need to do before we create our c file is that we need to declare the start of the kernel globally
so that the linker will find it.
We extern
the main function,
since the kernel main function is not defined here but in the c file. Next we make the label start globally
,
so we global start.
and the linker will know that the start is the entry of the kernel.
Alright, let's create our first C file.
We call it main.c.
The first thing we are going to do is add a header file. So we include
stdint
What we want to do here is we want to use fixed width data types. For simplicity, the system we write will assume
the data has specific length in the c code.
So we will define variables with fixed width data types.
The next header file is
stddef
These are the two headers we import in the project.
OK, the first function is kernel main
which is called from the assembly file, you can name the function to other name you like.
This is a simple function which don’t return value and have no parameters either.
The only thing we will do in this function is print character on the screen so that we will know we are
in the C function.
First off, we create a pointer and initialize it with the value b8000. We define a pointer
to type char.
Since each character on the screen takes up two bytes. The first byte is assigned to C
we assign the attribute a to it.
So the character is printed in green.
OK, we are done. When we return from the function, we will return to the infinite loop and stop there
.
Now, we switch to the build script.
and compile the c file.
So we type gcc and we use c99 standard
so -std c99
and then we specify we use large code model by typing
mcmodel large
so that the code generated is for the large mode. Otherwise we could have relocation truncated to fit error.
Next flag we add is free standing
which tell GCC we don’t need c standard library and other runtime features.
In the freestanding environment, there are still some of headers available to use, such as stdint file we used in the file
.
Next one is no stack protector.
To enable it, we need runtime support which we don’t provide in our code.
So here we disable this feature.
The next flag we add is no red zone.
The red zone is an area of 128 bytes below the stack pointer which can be used
by leaf functions without changing rsp register.
The red zone is specified in system v AMD64 calling convention which we use in the code.
We need to disable red zone in the kernel, otherwise the kernel stack could be corrupted if the interrupt occurs.
OK, then -c
and the name of the file main.c in this case. -c means that we only want gcc to compile the file into an object file
without linking to an executable file.
Ok now we have two object files kernel.o and main.o. Let’s link them to generate the kernel.
We use the linker and specify no standard lib
which means we don’t use startup file and library files.
We also use our own linker script.
We will talk about it in a minute.
Then we write -o the name of the file we want to produce. We call it kernel.
And the object file we want to link. kernel.o and main.o
When we generate the kernel, it is an elf format file. What we want to do is we want to generate a binary file
so that we can simply load it in the memory and jump to it from the loader as we did before.
So here we use objcopy
-O
And the format of the file we want, here we want binary file.
Then the file we want to convert,
kernel, in this case. The binary file is called kernel.bin
and we will load this file in the memory as we did in the last section.
Now, let's take a look at the linker script.
First off, we create the linker script.
called linker.lds
The first thing we will do is specify the output format. We do this by writing output
format
and then the format elf64-x86-64.
Next, we type entry start
which indicates that the label start is the entry of the kernel. You may notice that in the kernel assembly file,
we don’t use org directive to tell the assembler we want our kernel file running in the address 200000.
But in the loader file, we still jump to the address 200000
after we load the kernel.
So we use the linker script to do it.
We have several sections define in the files. We write sections.
The first segment we write is text segment.
and in the curly braces we write *(.text)
,
which means the .text sections in the object files, kernel.o and main.o in this case,
are added in text segment of the output file. Because we want our code running at the address 200000,
we type .= and the address 200000.
This means that current location is set to 200000.
The next segment is read only data segment, just like the text segment,
we add all the .rodata sections here.
The next segment is data segment.
And the last segment we care about is BSS and we write it here.
Also we want the data section aligned to the 16-byte boundary. So we write
. = align 16.
Ok that’s it for the linker script.
In the build script.
we add
-T
the linker name we just created
and we are done.
Let's test it out. We save the files
and build the project in the terminal.
Now, we run the script in the terminal.
OK, let's test out the program in bochs.
Alright,
We can see the character C is printed on the screen.