In this lecture, we will edit the loader file to load the image in the memory. Before we delve into the detail
,
let’s take a look at the boot process of the system.
As you can see, what it’s showing in the slide is the boot process in the previous lectures when we don’t implement the file system
.
In this section, we can write the boot code and loader file into the image as we did before.
The difference is that the kernel file and user files can be stored in the fat16 image as normal files,
the loader will find and load the kernel in the memory.
Once the kernel is loaded, we can load the user files with the help of the file system.
Therefore, when we finish the boot file and loader file, write them into the image,
We don't need to write the kernel file each time we complete the kernel.
Instead, we can copy the kernel to the image just as we copy the normal files in the fat16 partition
.
Now, let's get started.
In the boot folder, we create a new folder called loader where we add more stuff in it
with assembly and c files.
The assembly file is responsible for loading the image and c module which is a new module is used for loading the kernel
.
We start off with the assembly file.
As you can see loader file is moved into the loader folder. We made some changes in this file in order to load the fat16 image
.
So let's open the loader file.
The first change we made is we removed load kernel and user parts.
Because the kernel and user programs are now stored in the fat16 image,
we will move this part to the c module of the loader which can read the file from the image.
Alright,
let's move on.
In the slide, we know that the memory location where we will load the image
is set to higher part of 1g region.
Remember in the system, we only use the lower 1g of ram.
So the file system is located in the higher part of this region.
Another thing we need to do before we read the image is that we should make sure that the memory at this location
is the free region we can use. To perform the check,
we use the memory map info.
Here is code of retrieving the memory map by the bios service.
Because the loader file in this example is larger than the one in previous lectures,
it could be located in the address 9000 and above.
So we change the destination of memory info block from 9000 to 20000.
In this case, we access the memory using es register.
First off, we copy the value 2000 to es and change the address 9000 to es 0 and change the address 9000 to
to es:0.
So the segment part of the address is 2000 and offset is 0. The physical address it points to
is address 20000
.
The offset part of base address of memory info block is changed to 8.
In this case, es di is pointing to the address 20008
which is the destination address.
OK, at this point, we can check each block of memory info to see if we can load the image.
Once we get a block of data here,
we do the test. The structure is discussed in detail in section memory management.
The first check we will perform is the memory type, we only find the memory region with type 1 free memory.
The memory type is the third field in the structure which is at offset 16. If it’s not equal to 1,
we jump to continue.
Otherwise, it's a free region.
We go ahead and check if the memory region includes this range.
The first member of the structure is the base address which is 8 bytes long. Here we check the higher part of the base address,
if it is nonzero, it means that the base address of this region is larger than 4gb
which means it’s not the region we want,
we simply jump to continue.
If this check passed, we move the lower part of the address to eax, and compare it with the address.
If it’s larger than this address, we also jump to continue.
Otherwise we will compare the higher part of the length with 0.
The length is also 8 bytes data. So if the higher part is nonzero, it means that
the length of this region is larger than 4gb
and this is the region where we can load the image. So we jump to find.
Otherwise, we add the lower part of the length to the base address and then compare it with the address plus the size of image
100mb in this case.
If eax is less than the value, we will jump to continue,
otherwise this is the region we are looking for.
Here we define a variable load image and initialize it with 0. If we find the region,
we set the value to 1.
After get memory done, we check the value of load image. If it’s 1, then we can move on.
If it’s not 1, we jump to read error and stop the system because we cannot load the fat16 image
.
OK, now we can load fat16 image. Loading fat16 image in this example is not an easy task to do.
.
Here is the problem we will face in the real mode.
Remember we can only access 1mb of memory in real mode
and the fat16 image in this example is 100mb.
How can we load the entire image into memory in the real mode.
The method we choose is using big real mode.
In the big real mode,
we can access 4gb of memory just like in the protected mode.
To do it, we just modify the segment limits in the hidden part of the segment register.
Remember we have talked about it in the lecture long mode. When we load a segment descriptor into the segment register
,
the descriptor value will be loaded into the hidden part of the segment register
and then the value becomes the valid value.
In the real mode, the segment limits is 1mb
and what we will do in this case is switch to protected mode
and load a descriptor with the segment limits being 4gb, once the descriptor is loaded,
the segment limits will be change to 4gb.
Then we switch back to real mode,
at this point, we can access 4gb of memory using this segment register since the limits is 4gb
instead of 1mb.
It sounds a little bit complex.
let’s check it out in the bochs debugger so that you can see it in action.
In bochs, we have what it’s called magic break point.
In the configuration file,
we add magic break enable equals 1.
After we enable magic point in the box configuration file, we can set a break point in the code
by adding instruction xchg bx bx.
When we run the code in the debugger, it will automatically stop here. So let's do a test.
For example, we add xchg bx bx
we built the project and see the result.
In the build script of the loader,
We only built the loader file to test it out.
In the terminal, we run the build script.
To run the debugger, we right click bochs configuration file and choose debugger,
then we click start.
In the command prompt we type c to continue.
Now you can see bochs stops at instruction mov ax 3. If we check it out,
in the loader file, you can mov ax 3 is right after xchg bx bx.
Ok let’s examine the registers. To check the general-purpose registers, we type reg and press enter.
And the value of general-purpose registers is showing here, what do we want here is segment register
So we type sreg and press enter.
here shows all the segment registers with limits being 1mb.
Therefore we cannot access memory above the 1mb limits.
We type q to exit it out.
With that in mind, we know what the problem is and now let's change the limits of the segment register
.
The segment register we choose is fs register.
So the process is like this, we switch to protected mode, load the fs register with a new descriptor
and then switch back to real mode.
The code here is switch to protected mode as we have talked about before.
Note that we don’t load idt
because we still need the bios services when we are back to real mode.
Once we enable protected mode, we do nothing but load fs register with the segment selector 16
which specifies the third descriptor for data segment.
And the limit of the data segment here is 4g.
Then we go back to real mode by clearing the pe bit in cr0. At this point,
we should see fs segment register has 4gb segment limits.
We add a break point here.
And see the result.
run the debugger.
We type sreg to check the segment registers.
OK, you can see right now the limits of fs register is 4g, whereas other registers
still have 1mb limits
.
The next thing we are going to do is we are going to load the image.
To load the image, we read a couple of sectors in the low memory address and then copy the data to the high memory address.
This is because the destination address
the bios service uses is 16-bit address.
So we cannot load the image directly to the memory above 1mb.
The code of loading sectors is no different from what we have here.
We just wrap it as a function called read sectors. In the function, we pass the lba value of sectors
we want to read
in register ebx and the sectors count in register ax.
The address we want to load the sectors into is set to 60000.
So the segment part of the address is 6000. Before we return from the function,
if the carry flag is set, we set al to 1, otherwise al is cleared.
So we can check the status of read sectors by testing al register.
Ok we initialize the arguments and call function read sectors.
Remember we have created the image with the chs value being 203 16 63.
This is the total sectors we have in the image.
In this example, we want to read 100 sectors each time,
so we divide it by 100 and move the result to cx. Here cx registers acts as a counter,
we will repeat the process according to cx.
Ebx specifies the sector we want to read, which starts from the first sector. So we clear ebx.
Edi holds the high memory address into which we want to load the file data. Here we set it to this value
which means the memory above it
is used for file system only. And the memory below it is used for the system.
Then we save ecx ebx and edi on the stack because we will use them in the loop. And then initialize ax with 100
and call read sectors. After we return, we test al register,
if al is nonzero, we know that we have an error and jump to read error.
If the operation succeeds, we restore edi and ebx values.
The next thing we are going to do is we are going to move the data to the high memory part.
Function read sectors read 100 sectors and we copy 4 bytes of data each time,
so here we divide the size of 100 sectors by 4.
Read sectors load the data in memory 60000, we save the address to esi.
Ok we start copying the data.
we copy the data into eax. You can see, we specify the segment register fs to address the memory.
If we use other segment registers,
the address is out of limits.
Ok next we move eax to the high memory address specified in register edi.
Right, we have copied 4 bytes of data.
Let's continue.
we increment esi and edi by 4 bytes which move to the next position. And repeat the process
using instruction loop.
After we exit out the loop, 100 sectors of data is copied
and we continue reading remaining sectors until we are done.
So we restore the old ecx value by popping it from the stack.
And don’t forget to adjust ebx by adding ebx 100 and continue the process.
When it is done, we could have remaining sectors smaller than 100 to read into memory.
We read remaining sectors. To get the count of remaining sectors, we mod the total count by 100
and call reading sectors.
After we read the sectors successfully, we can move the data.
Divide the size of the remaining sectors by 4 and save the address of the data in esi
.
The copy part is the same as read fat part. We copy 4 bytes of data each time.
When we are done, all the data of the image is copied in the high memory part. At this point,
we can switch to protected mode and long mode.
In the beginning of this video, I said that the loader has assembly module and c module. In the c module, we will parse the file system
and load our kernel. Therefore, right after we enter 64-bit mode,
we will copy the c module of the loader to the address
100000 and jump there.
So the destination is 100000 in this case. Since we load 15 sectors of data in the boot code,
the size of c module is simply set to 15 sectors.
The virtual address of the destination is also changed to the correct address. In this example,
we will append the c module to the loader.bin.
To reference the base address of the module, we define label c module at the end of the file.
So the c module will be appended here.
It’s worth mentioning that we could have unexpected result when we load the fat16 image.
When we load the fat16 image using the bios disk service, the bios disk service seems to use the fs segment register.
Therefore, we better to initialize the fs segment register after we set the fs limits to 4g.
So here, before we call disk service.
we initialize fs to 0. So we zero out ax register and then mov fs ax
Next we store fs value on the stack before we call disk service, so we push fs
Then we restore it from the stack right after it returns.
So here we pop fs.
Another location we need to fix
Is the read remaining sectors here
So we also need push fs
and then pop fs after we
return from read sectors.
Alright, this file is done.
let’s discuss what we will have in the c module. The tasks we will do include file system initialization,
load kernel and user program.
While we are doing the tasks,
we also print string and using assertion to do the check.
So, we need print module, debug module, library file and entry file.
As you see, I have added them. These files are the same files we created in the kernel.
So we just copied them from the kernel project.
The entry file is pretty much the same as kernel assembly file which is used to prepare for the main function
.
So let's take a look.
The only thing we will do is set stack pointer and jump to main function.
OK, let's move to main.c file.
This file is simple, in the main function we call printk function to print the message loader is working.
The last file we will talk about is linker script.
remember in the loader file we jump to this virtual address.
This is the base address
where we load the c module of the loader file,
After we set base address in the linker script, we can append the c module to the loader.bin file in the build script.
The build script is only for the loader project.
You can see it compiles the c module and generate entry.bin file.
Instead of writing the entry file to the image, we append it to the loader.bin by specifying the entry file
and pointing to loader.bin. And then we write the loader.bin file to the image.
Here we set the size of the loader to 15 sectors.
Alright, that's it. Let's build the project and test it out.
We run bochs.
OK, you can see loader is working.
That's it for this lecture.
See you in the next video.