Bios which stands for Basic Input Output System, is usually stored on a small chip on the motherboard.
It initializes hardware and provides runtime services for operating systems.
When we boot our computer, BIOS initializes and tests the hardware, then find the boot device, read the first sector
which is the boot sector into the memory location 7c00
and control is transferred to it.
The information on the first sector is also known as master boot record,
MBR.
The program in this video will be write into the first sector of the disk.
When BIOS reads it into the memory, our code will be executed.
At this point we have control of the computer.
BIOS also provides low-level services or functions for us to use.
and become unavailable in protected mode and long mode.
When booting our operating system, we will go through 3 different modes real mode, protected mode
and long mode.
when our boot code executes, we are in real mode.
Then we enter the protected mode for preparing for the long mode.
Finally, we will switch to long mode.
Long mode consists of two sub modes, 64-bit mode and compatibility mode.
In compatibility mode, we can run 16-bit or 32-bit programs under 64-bit operating system without recompiling those programs.
.
Our operating system is designed for the 64-bit kernel and 64-bit programs. So we will focus on 64-bit mode in this course.
.
In this caused by long mode, I always mean 64-bit mode.
As we move along with our projects, please don’t get confused with these two terms,
they mean the same thing in this course.
Ok, we call all the services we need before we enter long mode.
The services we will use in our code include print function which prints characters on the screen,
load files from hard disk in memory, such as the kernel file.
We also get memory information of the computer and set video mode.
.
When we are in long mode, we cannot use print functions provided by BIOS.
Instead, we set a specific video mode and then we can print characters in that mode.
It is worth mentioning that Modern computers also come with UEFI, Unified Extensible Firmware Interface
which is expected to replace BIOS.
This course is based on BIOS.
So when we test our code on a real machine, make sure that legacy BIOS mode is enabled.
What we are going to talk about next is real mode.
we don’t cover too much about real mode. Since what we will do in real mode includes load kernel in memory,
get information about hardware, etc. Then we will switch to long mode and the operating system will be running in 64-bit mode.
.
So first off, we will see memory addressing and how memory address is formed in real mode.
There are 4 special registers called segment registers which are 16 bits registers.
Actually, there are more than four segment registers we can use.
But in this course, we only use these four registers.
.
CS stands for cde segment,
DS Data segment.
,
ES Extra segment and SS stack segment.
The format of an address is the segment register, then colon and offset.
This is called logical address.
So how do we get physical address?
we shift the value in segment register left by 4 bits,
that is multiply the value by 16 and then add the offset to the result which produces 20-bit physical address.
.
For example, this instruction will read the value in the address at ds 50 to ax. Suppose we have 1000 in ds register,
,
the memory address we get is 1000 times 16 and then plus the offset 50 which produces 10050 in hexadecimal.
.
the addresses in this example are hexadecimal value. We can use prefix 0x or suffix h to represent a hexadecimal number.
.
So this instruction will copy the data in address at 10050 to register ax.
.
Move on to the next example, suppose ss register holds the value 100 and sp, stack pointer register
points to address 500.
Stack operations such as, push and pop, use ss register. When we pop the value on the stack into register bx,
the address of the value we pop is 100 times 16 plus 500. So the address of the memory
we actually reference is 1500.
CS and ES registers also follow the same rule calculating the memory address.
some other instructions will use es register to access data,
we will talk about that when we use them.
and cs is used to calculate the address of instructions.
Another thing I want to mention is that the default segment register for accessing data is ds register,
,
so if we don’t specify it in this example, ds is still used as default register.
the general-purpose registers. As you can see, there are registers of different sizes.
,
For example, aL ah bL bh are available to use.
We can also use 16-bit and 32-bit registers such as eax, ebx registers.
Note that 64-bit registers are not available in real mode.
Before we enter 64-bit mode, we don’t use them in our boot file.
OK, now back to our boot file,
Here is the first program which will print hello message on the screen.
Since we will run the program without an operating system,
we have to do everything ourselves.
As you can see, we need a couple of blocks of code to print the simple hello message.
Now I'm going to walk you through each line of the code.
This file can be divided into several parts.
The first part consists of two directives. The directive bits tells the assembler that our boot code is running on the 16-bit mode,
because real mode which the boot code is running at is 16-bit mode.
Directive org indicates that the code is supposed to be running at memory address 7c00.
.
Remember BIOS loads the boot code from the first sector into memory address 7c00
and transfers the control to it. If we don’t specify 7c00 in this case,
the memory reference within our code will be incorrect when the program is running.
OK, move to the assembly code.
We use label start to indicate that this is the start of our code.
What this block of code does is initialize the segment registers and stack pointer. The xor instruction
zeros ax register and then copies the value in ax which is 0, to
ds, es and ss segment registers.
So these segment registers are set to 0 after running the first four instructions.
Since we set the segment registers to 0,
we don’t concern about the value in these registers when we reference memory address later in the code.
the offset is actually the memory address we want to access because segment registers are 0s now.
Then we assign 7c00 to sp register. Here is the memory map after we initialize segment registers
and stack pointer.
The boot code starts at 7c00 and the stack is set up below 7c00.
Because stack grows downwards, when we push a value into stack,
the stack pointer will point to 2 bytes below 7c00 because the default operand size for push and pop instructions is 2 bytes in 16-bit mode.
.
As we push more data on the stack, sp register will decrement as shown in this example.
OK.
at this point, we can access data in memory and do stack operation such as push and pop.
The next part, as its name implies, prints message on the screen so that we can see our system is running.
Printing characters is done by calling BIOS service.
The BIOS services can be accessed
with BIOS interrupts.
As you see, we use instruction int,
and then the interrupt number to call the specific BIOS service.
The print function is interrupt 10 and notice that this number is hexadecimal number.
Before we call a BIOS function, we need to pass parameters.
Print function has relatively large set of parameters for different purposes. Since we only want to print string
in the boot process,
let’s see how to pass parameters to print string.
.
Register ah holds the function code. Here we use 13 which means print string.
Register AL specifies the write mode, we set it to 1, meaning that the cursor will be placed at the end of the string.
Then we save A to bx.
Bh which is the higher part of bx register represents page number.
BL, the lower part of bx holds the information of character attributes.
We also zero register dx. Dh which is higher part of dx register represents rows and dL represents columns.
.
Since we want to print message at the beginning of the screen, we set them to 0. Bp holds the address of the string
we want to print.
As you see, we defined the message hello.
Next instruction copies the address of message to bp register.
If we want to copy the data in the variable message to bp,
we need to add
square brackets to do that.
But what we want here is just the address of the variable message, so we don't use square brackets.
Ok. Let’s move on. CX specifies the number of characters to print. The dollar sign means the current assembly position
which is the end of the message in this case. Minus the address of message,
the result is the size of string or the number of characters.
Here we define a constant message length, using directive equ, to represent the number of characters, and copy it to register cx.
.
Ok.
With all the parameters initialized, we will see the message hello printed on screen
once we execute int instruction.
And then we reach the end of the code. Halt instruction places the processor in a halt state.
Note that interrupts will resume execution.
So here we write jump End right after the halt instruction.
If an interrupt is fired, the processor will run the instruction following halt,
which is jump instruction in this case.
As a result, we will jump back to label end and execute halt instruction again.
So this is an infinite loop.
The code just ends here.
Let's move on.
Directive times repeats command the specific times.
Here the expression specifies how many times db is repeated. The double dollar sign
means the beginning of the current section.
We only have one section in this example. So it represents the start of our code.
The dollar sign minus double dollar sign represents the size from the start of the code to the end of the message.
.
Then we subtract it from 1be. The result is the size shown here. As you can see,
the result of this expression is repeat db command so that the space from the end of the message to the offset 1be are filled with 0s.
.
So what is in the offset 1be?
In offset 1be, we have what it’s called partition entries.
There are four entries with each entry being 16 bytes in size.
You can see we only defined the first entry and other entries are simply set to 0.
The format of partition entry is shown here.
The first byte is boot indicator,
we simply set 80 meaning that this is a bootable partition.
The next three bytes construct a starting c,h,s value. C stands for cylinder, h, head and s, sector.
The first byte represents head value.
The second byte is divided into two parts. Bits 0 through 5
is used as sector value.
Bits 6 to 7 is used as cylinder value.
The last byte holds the lower 8 bits of cylinder value.
The head and cylinder values start from 0, so cylinder 0 is the first cylinder.
Whereas sector values start from 1, meaning that sector 1 is the first sector.
OK, let's move on.
The fourth byte is partition type,
we set it to f0.
Then the next three bytes construct the ending c,h,s value. Here we simply set them to ff.
FF is the max value we can set in a byte.
The next double word represents LBA address of starting sector.
LBA means logical block address. In the boot process,
we will load our file using Lba instead of c,h,s value.
The last double word specifies how many sectors the partition has.
The value we set is 10mb.
If you don’t fully understand the meaning of each field of the entry, no worries.
Because the value in the partition entry does not reflect the real partition.
The reason we define this entry is that some BIOS will try to find the valid looking partition entries.
.
If it is not found, the BIOS will treat usb flash drive as, for example, floppy disk.
We want BIOS to boot the usb flash drive as hard disk. So we add the partition entry to construct a seemingly valid entries.
.
The last two bytes of our boot file is signature which should be 55AA.
the size of boot file is 512 bytes.
So sector size is assumed to be 512 bytes in this course.
When we write the boot file into the boot sector, it gets the same data as boot file.
OK.
With boot the image prepared, we use build script to build our project and write the binary file into boot image.
.
If you know the makefile, you can build the project using Makefile.
In this course, we will use a simple build script.
So let's create a new file.
In the script, we have two commands to write, the first one assembles the boot file.
The assembler we use in this example is nasm.
So we write nasm
-f
f to specify the output file we want to produce which is binary file.
Then dash o,
the name of the output file. We name it boot.bin
and the file we want to assemble, that is boot.asm.
If the assembly file has no errors, we will see the boot.bin generated.
What we are going to do next is we are going to write the binary file into the boot image
to make it bootable.
So the command we use is dd command.
And input file,
which is boot.bin
and the output file,
boot image.
.
and then the block size
is set to 512 bytes.
The count means how many blocks we want to write. Here we set it to one
because we only write the boot.bin which is 512 bytes into the first sector.
And then we specify
no trunc
which means we don’t truncate the output file. So the size of boot image remains unchanged
after we run dd command.
If you choose the Windows environment just like I did in this video, there is extra work we need to do.
.
First, we show the status bar by clicking view in the menu.
In appearance, we Click Show Status Bar.
At the bottom of the screen, you can see the status bar. Noticed that there is CRLF, which is
line break in windows and we need to choose LF which works for build script in Linux, so we click it
and choose LF.
All right, now, we can save it
to shell script.
Here.
and name it build.
click save.
OK, that's it, since this is our first program, we will see how to build the project in Windows 10,
Linux and Mac OS in the next three lectures, so you can choose one of those lectures based on the
operating system you are working on.