Series directory

  • introductory
  • The preparatory work
  • BIOS starts in real mode
  • GDT and Protected Mode
  • Exploration of Virtual Memory
  • Load and enter the kernel
  • Display and print
  • The global descriptor table GDT
  • Interrupt handling
  • Virtual memory improvement
  • Implement heap and malloc
  • The first kernel thread
  • Multithreading switch
  • The lock is synchronized with multithreading
  • Implementation of a process
  • Enter user mode
  • A simple file system
  • Load the executable program
  • Implementation of system calls
  • The keyboard driver
  • To run a shell

To realize the Boot Loader

Continuing with the next preparation article, from this we will start to write the Boot Loader. Some of the tutorials on the web may skip this stage and give you the boot loader so that you can start writing the kernel directly. For example, Jamesm’s Kernel Development Tutorials are among the tutorials recommended above. However, I strongly recommend that the Boot Loader also implements itself, especially for beginners, for the following reasons:

  • It is not difficult, compared to the later kernel;
  • Help you quickly improve the assembly ability, which is still very important in the later C language kernel writing, debugging;
  • Boot runtime programming helps you to establish a proper understanding of disk, memory, instructions and data loading and mapping relationships, to prepare for later kernel, executable loading, and virtual memory, especially if you feel vague about this area.
  • In the boot stage, the framework of segment and virtual memory will be preliminarily built, laying a foundation for subsequent kernel writing.

Boot and enter BIOS

This is a classic problem, is the computer motherboard boot after the power to start, what happened?

First of all, we need to know the state of CPU and memory after boot. After boot, the initial mode of CPU is real mode, the address width is 20 bits, that is, the maximum address space is 1MB. The partition of the 1MB space is fixed, each of which has a specific purpose and is mapped to a different device:

The work of the BIOS

Let’s take a look at what happens when I boot up:

  1. The CPU instruction register IP is forcefully set to address after boot0xFFFF0This address is mapped to the code on the BIOS firmware, which is the address of the first instruction after the computer is turned on;
  2. The CPU starts to execute the code on the BIOS. This part is mainly to check the hardware input and output devices, as well as to establish an initial interrupt vector scale. It is not necessary to explore in depth at present.
  3. The final phase of the BIOS code is to check the boot diskmbrA partition, or MBR partition, is the first 512B content on diskBoot partition; The BIOS does a check on the 512B: the last 2 bytes of the 512B must be two magic numbers:0x550xaa, otherwise it is not a valid boot disk;
  4. After the check passes, the BIOS loads the 512B into memory0x7C00To 0x7E00, and then the instruction jumps to 0x7C00 to start execution; At this point BIOS out of the stage;

Plot the chart above, remove the distractions, and leave only the things we care about:

  • The yellow part is the MBR loaded into memory, starting at address 0x7C00;
  • The white part is the memory space we can use freely later;
  • The slash shaded part is the BIOS code;

The figure marks the main workflow of BIOS, starting from address 0xFFFF0, through a series of code execution, finally checking and reading the first 512B sector on the disk, loading it into the yellow part, that is MBR, address is 0x7C00, and then the instruction jumps to enter the execution of MBR;

The work of the MBR

So what does the MBR need to do? Because the MBR is limited to 512B in size, you can’t put a lot of code and data in it, so the most important thing it does is:

  • Loads the subsequent loader part from disk to memory, and jumps to the loader to continue execution;

Memory layout planning

Therefore, we need to plan the memory layout during the entire Boot Load phase. Here we directly present the disk and the overall picture of memory:

We are currently focusing on the parts with less than 1MB of memory, and different parts are marked with different colors:

  • Yellow: the MBR
  • Blue: loader
  • White: Free to use

After the BIOS work, now the instruction has come to the MBR part, it needs to load the blue part of the loader from the disk to the memory, the address is set as 0x8000, note that this address can be freely specified, as long as it is in the white area in the figure, and there is enough space for it. Our loader part is also not very large, according to the relatively surplus estimate, 4KB is enough.

The MBR code

The path is SRC /boot/mbr.S for your reference.

First, focus on the beginning:

SECTION mbr vstart=MBR_BASE_ADDR

MBR_BASE_ADDR is defined in Boot.inc as 0x7C00, which means that the entire content of the MBR is addressed from 0x7C00, including code and data. This is important because we already know in advance that the BIOS will load the MBR to this location, so the addressing of the entire contents of the MBR must start here, so that the BIOS can properly address the code and data in the MBR after jumping to the first instruction of the MBR.

The entry of the MBR is marked as mbr_entry, and the following functions are defined. We can use C language to comment on it:

void init_segments();

There are several segment registers initialized with an initial value of 0, which indicates how the segment memory is distributed in flat mode. You don’t have to go into that now. I also moved the stack to 0x7B00, which is just a free play, not a requirement.

Next load the loader:

void load_loader_img();
// The assembly code for this function uses registers to pass arguments directly. void read_disk(short load_mem_addr, short load_start_sector, short sectors_num);

Here is the main work of the MBR. Loading the loader from the disk to the location of memory 0x8000. The three parameters of read_disk are passed as follows:

// Loader is loaded at 0x8000; short load_mem_addr = LOADER_BASE_ADDR; // Loader image starts at sector 2 on disk, immediately after MBR; short load_start_sector = 1; // Loader size is 8 sectors, total 4KB; short sectors_num = 8;

The read_disk function involves reading the disk, using a bunch of CPUs to control the disk’s ports and interrupts, and you need to consult the documentation to use it. It’s a long and tedious process, and I’ve just picked it up from Chapter 3 of the book Operating System Truth Restore. You don’t have to dig deep to use it, you just need to know what it does.

After loading the loader, you can jump to loader address 0x8000 to execute:

jmp LOADER_BASE_ADDR

The entire instruction run jump process from BIOS -> MBR-> Loader is shown in the following figure. The loader part is shaded in light blue because it actually has no valid data at the moment, waiting for us to implement it later and load it into memory:

Finally, there is one crucial little thing: after all this code, we are not even close to 512B, so we fill in all the remaining space with 0 (we can fill in anything, it won’t work anyway), and finally write two magic numbers 0x55 and 0xaa at the end of 512B:

times 510-($-$$) db 0
db 0x55, 0xaa

At this point, the MBR encoding is complete, which is very short and simple. Next we need to compile it and make it into a startup image, load it into Bochs and run it.

Run the MBR

First you need to create a disk image, which again uses Bochs ‘bximage command-line utility:

>> bximage -hd -mode="flat" -size=3 -q scroll.img 1>/dev/null

It produced a 3MB file full of zeros, which was enough for our project to hold all the data from the MBR, the boot, the kernel, and other user programs. The bximage print log will also tell you what parameters to set for the disk in the configuration file bochsrc.txt, which is handy.

Next compile MBR.s using NASM:

nasm -o mbr mbr.S

Then you get a 512B MBR file. Next, write it to a disk image, using the dd command:

dd if=mbr of=scroll.img bs=512 count=1 seek=0 conv=notrunc

Notice that the MBR is written to the first sector of the disk image file (512B).


Now we have a disk image like this:

Then you can load the disk image into Bochs and run it just like before:

bochs -f bochsrc.txt

Before you do, though, the MBR would be wise to make one small change. Because we don’t have any loader content in the image at this time, the loader is actually all 0 after loading, this is not executable code, so the CPU will hang after the last instruction of the MBR, JMP LOADER_BASE_ADDR. So you can precede this instruction with a JMP $, which is like an endless loop while (true) {}, so that you can suspend Bochs and see if it stops in this instruction, and if it does, the MBR is running successfully.

conclusion

MBR is short and concise, there are not too many difficulties in itself, but the beginning of the end is difficult, as the beginning of the whole kernel image, we need to start to plan the layout of the entire memory in advance. If you are not familiar with the principles of assembly, instruction, memory and so on, the MBR is also a very good practice opportunity. It is recommended that you refer to the decompiled MBR code and Bochs debugging, which can quickly help you to establish the relevant knowledge.