Welcome back.
we have walked through the fat16 image. Let’s add a small file module in the loader.
we first take a look at file.h
The structures are defined in the file.h.
As you can see, we have bios parameter block
and directory entry structure which we have discussed in detail in the last video.
Remember we loaded the fat16 image to this address,
so we define a constant fs base to represent the base address of the file image.
Ok let’s move to file.c and see what we have in the file.
The header files we will use include file.h, lib.h print.h debug.h and stdbool.h
Just as we did with other modules, the first function we will implement is initialization function.
We name it init fs which takes no parameters and doesn’t return a value.
In the function, the first thing we are going to do is we are going to get the address of the bios parameter block.
Because all the essential data is stored in this structure.
The function we use in this case is get fs bpb. After we get the address of bpb,
we perform a simple check.
The last two bytes of the first sector should be 55 and aa. If we find that they are not equal to the values,
we print invalid signature and assert false to stop the system
because what we have in this location is not the image if that happens.
OK, that's it for the initialization function.
Next we get to function get fs bpb.
As you can see, this function returns a pointer to structure bpb. As we have parsed the fat16 image manually
and the whole image file is loaded in the address fs base,
so we first find the partition entry at the offset 1be.
The starting lba is located at offset 8,
So we add 8 to the offset.
The lba value is 4 bytes long. We define a variable lba to get the lba value.
With the starting lba retrieved, then we locate the fat16 partition by adding the lba offset
to the base address.
The sector is assumed to be 512 bytes. So here we multiply the lba by 512.
The result is the base address of the partition and we return this value.
Alright, at this point, the initialization is done.
Next we will find the kernel file. To find a file, we need to locate the root directory section
where each entry represents a file or folder.
So first off, we write function
get root directory.
The function returns the base address of root directory section which stores the directory entry arrays.
So the type of the value is a pointer to structure directory entry.
In the function, we use the info stored in the bpb to locate the directory section.
So we define a variable bpb and get the base address with function get fs bpb.
Next we calculate the offset. As we have learned in the last lecture, the root directory section follows the fat tables.
So we multiply the table size by the count of fat tables.
And don’t forget to add the reserved sectors.
Then times the bytes per sectors. The result is the offset,
we add it to the base of the partition
and this is the base address of the root directory section.
Next function we will implement is get root directory count
which will return the count of the entries root directory can hold.
The count value is also stored in the bios parameter block. So we get the bpb and the field root entry count stores the value
.
So we simply return
the value root entry count.
Ok with root directory info being retrieved, let’s see how to search a file.
To search a file, we define a function search file.
It takes one parameter the path name. The path name includes file name and extension name.
The character . separates them.
Therefore, we will split the path name before we search the file.
As you can see, the file name and extension name are like this, the file name is 8 bytes data
and extension name is 3 byte data.
If we have characters less than that, the free space is filled with space or blank.
Since we search files in the root directory section, we define variable root directory count and directory entry pointer
.
Now we split the path by calling function split path which takes 3 arguments, that is
,
the path name we want to split, the file name and extension name.
After we return from it, we check its status, if the status is true, we will search the file
using file name and extension name. Otherwise we return the max value to the caller
indicating that the file is not found.
OK, let's continue.
To find the file in the root directory, we use directory pointer and directory count.
In the for loop, we loop through each of the entries to see if the file is there.
If the first byte of the file name is 0 meaning that the entry is not used, or the file name is e5
which means the file in the entry is deleted,
If that is the case, we continue the loop.
The next check we will perform is for the long file name,
we don't support the long file name features in the system, so if we find a long file name entry,
we just skip this entry. The attribute of a long file entry
is F.
So if we find that, the attribute is equal to F, we continue the process.
If all the checks pass, we will do the comparison which is done in function is file name equal.
We pass the current root directory entry, the file name and extension name. If they are equal,
this is the file we are looking for and return the index to the caller.
OK, that's it for the function search file.
Let’s take a look at function is file name equal.
This function is simple, all it does is compare the file name as well as extension name with the directory entry
.
If the result is equal, we return true, otherwise we return false.
Let's move on.
We haven’t talked about split path. Let’s see what we have in this function.
As you can see in this function, the first part is for parsing the file name
and the second part is for the extension name if the file has one.
So when we retrieve the file name, we loop through each character of the path name
until we get 8 characters or we find the character . which means the following characters are for the extension name.
or we reach the end of the path name, the null character.
For simplicity, we don’t support subfolders in the system, so we don’t allow forward slash included in the path name
.
If we find that, we return false.
As for normal characters, we just copy them in the name buffer.
After we exit out the loop, the file name is stored in the buffer.
Then we check if the character is dot,
if it is a dot, we will get the extension name.
Here we increment I to make it point to the next character which is the start of the extension name
.
In the for loop, we retrieve at most 3 characters. We also check the null character, if it is null,
then we reach the end of the name.
We don’t allow forward slash in the extension name either. So if we find that, we return false.
Next we copy the characters to the extension name buffer.
After it is done, we will check the last character to see if it is null character. If it’s not a null character
,
it means that we still have characters after we retrieve the name.
and this is not a valid name.
So we return false.
Otherwise, we return true to the caller.
What we are going to do next is we are going to load file.
So let's take a look at function
Load file.
It takes two parameters path name and the memory address we want to load the file into.
First off, we define a few variables, index which holds the directory entry index. The file size,
the first cluster index of the file, the directory entry pointer and the return value which is initialized with -1
.
To load a file, the first thing we will do is search the file.
If the file exists, the return value is the index of the root directory entry. So we check the return value
.
If it’s not equal to the maximum value,
it means that the index is valid.
We get the root directory by the function get root directory. Then locate the entry with the index.
The data we want in the directory entry is the file size and cluster index.
They will be passed in the function read file.
Function read file will read the data of the file to the memory address specified by us. The return value of the read file
,
is the size of data it actually reads.
If it reads all the data, we return 0 to the caller.
Otherwise we return -1 indicating that we don’t load the file successfully.
OK, let's move to read file function.
The first argument is the first cluster index and then the memory address we want to load the data into.
The size of the data is specified in the last argument.
As you see, the actual work of reading a file is done in function read raw data.
And read file function simply wraps read raw data and return the size.
Before we get to function read raw data, we need other functions to retrieve the cluster info.
We have seen how to find all the clusters of a file with the file allocation table in the last video.
The first data we need is the address of fat table. We start with function get fat table.
Just as we did before, we get the bios parameter block. Then the offset of the fat table
is the reserved sector count times the bytes per sector.
Add it to the base of the partition and we are done.
Next function is get cluster value. The parameter it takes is the cluster index. In the function,
we define a variable fat table and get the table by the function get fat table.
Remember each cluster value is 2 bytes. So we define a pointer pointing to 16-bit value.
To get the cluster value is simple, all we need to do is locate the item with the index and return the value
.
Next function is get the cluster offset. It takes one parameter which is the cluster index
.
To find the address the cluster value represents, we need to know the reserved sector count, the fat table size
and the size of root directory section.
So here we define 3 variables reserved size, fat size and directory section size.
Remember cluster value starts from 2. We assert cluster index is greater than or equal to 2
.
Now we can get the bpb and calculate the sizes.
The reserved size is reserved sector count times the sector size.
The fat table size is the fat count * fat table sector count * sector size.
the directory section size is the directory entry count * the size of directory entry.
With all these three variables, we add them together
and the cluster index starts from 2. So we subtract 2 from it and then times cluster size.
Ok this is the offset of the cluster.
The last one is get cluster size. The cluster size is stored in the bios parameter block. So we first get the bpb.
Then we retrieve the data by multiplying bytes per sector by the sector per cluster and this is the cluster size
.
Ok all the functions are prepared. we can implement read raw data.
The function returns the actual size of data being read.
The variables we need in this function include bpb, the data pointer which points to
the data we want to copy.
The read size indicates how much data we read.
The cluster size and cluster index.
First off, we retrieve the bios parameter block and the cluster size.
The index here is used to locate the fat table entries.
So the first index is the starting cluster index. Since we could have multiple clusters in the file,
the while loop is used to read all the clusters until we get to the end,
in this case, the condition is read size is greater than or equal to the size.
OK, now we get the address of the data using cluster index, The function is get cluster offset and
we add it to the address of the partition and this is the address of the data indicated by the cluster index
.
Then we go ahead and get the next cluster value
using the index and the return value is next cluster index.
If the next cluster index is greater than fff7, then it means that this is the last cluster
and we copy the data to the buffer then we break the loop.
If the next cluster index is less than fff7, then it’s a valid cluster index
and the current cluster is not the last one.
So we simply copy one cluster of data to the buffer.
Then we update the buffer and read size.
Since we copied one cluster of data. we add one cluster size.
After we are done, we return the actual size of data being read.
And also, here we can perform a simple check. The cluster index should start from 2.
So if it is less than 2, we simply return to max value, indicating that the read operation failed.
OK, we finished file module.
Now, let's go to the main function of the loader.
We include
file header, and in the main function, we get rid of printk function
and initialize the file system.
After the initialization is done, we can load the kernel file and the user file, so we load file the
kernel.bin
the base address is set to 200000.
Then we load
user.bin
and the base address we want to load the file into is 30000.
Since what we do here is load the kernel, we use assertion to do the check, so we include debug.h
and assert
the load file functions succeed.
After the main function is done.
Let's go to the entry assembly file.
As you can see, after we return from the main function, we jump to the infinite loop.
Since we have loaded the kernel file, we can jump to kernel.
So we mov rax
the virtual address of the kernel is the same as we saw in the previous lectures.
THen we jump to rax.
If everything goes well, we should see kernel is running.
Since we add a new module,
we need to add the file in the build script.
and add file.o to the linker command.
OK, the loader project is done.
let's build the loader project.
We open the terminal and navigate to the loader directory.
we run the build script.
OK, the loader.bin file is written into the image.
Another thing I need to mention is that because in the last section, the kernel needs 3 user programs
to run properly.
And now we have only loaded one user program in the loader.
So in the main function of the kernel project.
The only thing we are going to do in the kmain is print message, so we comment out all the other functions.
And print kernel is running.
OK, let's see the result.
So we build the kernel project.
We haven't changed the build script, so let's do some editing.
The loader.bin file is not used here as well as boot.bin file
So we don't write boot.bin and loader.bin to the image.
As we have talked about in this section, we don't write the kernel.bin file and user files into the OS image.
So the commands here are not used either.
The command we used here is only for compiling the kernel project.
OK, let's build the project.
Before we run bochs, we have to mount the image so that we can copy user.bin and kernel.bin to this image.
Since we don't have user.bin file in this lecture, we just rename the boot.bin to user.bin
and copy kernel.bin and user.bin to the image.
OK, now we dismount the image.
Let's run bochs to see what we get.
Alright, you can see the message kernel is running printed on screen.
That's it for this lecture.
See you in the next video.