In this article, we will write our own bootloader using 16-bit assembly language to create our own operating system.
Writing an operating system is the most complicated task in the world of programming.
The first part of an operating system is the Bootloader. Bootloader is a piece of program that runs before the operating system starts running. It is used to boot other operating systems and usually each operating system has a set of bootloaders specific for it. Bootloaders usually contain several ways to boot the OS kernel and also contains commands for debugging and/or modifying the kernel environment.
We will create a 3 stage OS. The first stage is to just display messages on the screen with colors, the second is to take input from user, and third stage is for drawing. The bootloaders are generally written in 16-bit assembly (also called Real mode), then the bits can be extended to 32-bit (Protected mode).
So, the bootloaders must be written in 16-bit assembly. Before you move to the next part, you must have some knowledge about 16-bit assembly language. Quick review of 16-bit registers.
Data registers
AX is the primary accumulator; it is used in input/output and most arithmetic instructions.
BX is known as the base register, and it could be used in indexed addressing.
CX is known as the count register; CX registers store the loop count in iterative operations.
DX is known as the data register. It is also used in input/output operations.
Index Registers
- Source Index (SI)
It is used as source index for string operations. - Destination Index (DI)
It is used as destination index for string operations. - Segment registers
Data segment(DS), Code segment(CS), Extra segment(ES), Stack segment(SS) - 8-bit registers AH, AL, BH, BL, CH, CL, DH, and DL.
where H is higher & L is Lower.
AH(8-15)+AL(0-7) = AX
For more, learn 16 bit assembly language tutorial and BIOS interrupts in assembly. Download the source code to view the complete OS code.
OK! Now, what the hell is this ?
[bits 16]
This line tells the assember that you are working in 16-bit real mode. It will convert the assembly data to 16-bit binary form.
[org 0x7c00]
This is assember directive. 0x7c00 is the memory location where BIOS will load us.
xor ax,ax
mov ds,ax
mov es,ax
mov bx,0x8000
First, we are setting the registers (such as ax, ds, and es) to zero, which we will use further. Then, we will copy the memory location 0x8000 to bx register because we want to perform operations/instructions. As we are loaded at 0x7c00 memory location, we need memory location above it.
hello_world db 'Hello World!',13,0
The above line defines the string with label hello_world, where 13 is New line and 0 is end of string.
mov si, hello_world
call print_string
Pointing to the first character of hello_world string to source index (si) register and then calling print_string function. Copying 0x0E to ah register. This will tell to interrupt handler that takes value/ASCII character from al & prints it using int 0x10.
AH = 0x0E
AL = character
BH = page
BL = color (graphics mode)
int 0x10
.repeat_next_char Label for continue to loop until end of string occurs.
lodsb
This instruction loads the first character from si to al register using ASCII code. Then, we will compare whether al contains 0 or not. If not, then print it and jump to loop, otherwise jump to .done_print.
int 0x10
This is BIOS video interrupt which takes char value from al register & prints it.
times (510 - ($ - $$)) db 0x00
A boot sector always be a 512 byte. starting with address 0x00.because on hard drive, there are only 512 bytes of sectors.
dw 0xAA55
This is the magic number of bootable devices. This line is boot signature that makes our code into bootable code. It defines word 0xAA & 0x55. These are last two bytes of our first sector.
Because of this number, BIOS loads us at 0x7c00 location when computer starts.
For Linux, type the following command to compile file.
nasm -f bin hello_world.asm -o myos.bin
Once file is compiled successfully and myos.bin file is created, run it in QEMU.
qemu-system-x86_64 myos.bin
For Windows, open NASM application. It will prompt a command at location where NASM is installed. Perform same commands as performed for linux, just with giving full file name path.
Consider I have file in C:\Users\Pritam\Documents\temp folder.
nasm.exe -f bin "C:\Users\Pritam\Documents\temp\hello_world.asm" -o "C:\Users\Pritam\Documents\temp\myos.bin"
and to run in QEMU.
"C:\Program Files\qemu\qemu-system-x86_64.exe" "C:\Users\Pritam\Documents\temp\myos.bin"
where i have installed QEMU.
This will print the following output.
Here, I have created .bin file but you can also create .iso file. Once it successfully prints "Hello World!", attach secondary device/USB drive and boot .bin/.iso in it. You can use dd command on linux or rufus software on Windows.
To print a string on screen at specific location or to set cursor at specific location, use the following actions.
AH = 0x02
BH = page
DH = row
DL = column
e.g
As described above every sector has a size of only 512 bytes, if you write code which is taking more than 512 bytes, it will not work and assembler will give you an error.
So, to use more memory, you need to load/read next sector into main memory. To load/read sectors in main memory,
- AH = sector number(1,2,3 etc.)[1 is already taken by our bootloader]
- AL = number of sectors to read
- DL = type of memory from where to read(0x80 is for hard drive/USB drive)
- CH = cylinder number
- DH = head number
- CL = sector number
- BX = memory location where to jump after loaded
- int 0x13 = Disk I/O interrupt
Then, jump to your memory location (label in assembly).
e.g
- ; load second sector from memory
-
- mov ah, 0x02 ; load second stage to memory
- mov al, 1 ; numbers of sectors to read into memory
- mov dl, 0x80 ; sector read from fixed/usb disk
- mov ch, 0 ; cylinder number
- mov dh, 0 ; head number
- mov cl, 2 ; sector number
- mov bx, _OS_Stage_2 ; load into es:bx segment :offset of buffer
- int 0x13 ; disk I/O interrupt
-
- jmp _OS_Stage_2 ; jump to second stage
For clearing the screen, copy 0x13 to ax & call video interrupt.
For graphics, we need to access video memory segments. This can be done by pushing 0x0A000 into stack, and setting di,ax,es to specific values.
AX = color
DI = x & y cordinates(y=320 for next line(320*200 display))
[ES:DI] = value of x,y cordinates & color(AX)[segment :offset]
Here's the code that only draws our graphical simple window on a screen with text.
-
- mov ax,0x13 ; clears the screen
- int 0x10
-
-
-
- ;//////////////////////////////////////////////////////////
- ; drawing window with lines
-
-
- push 0x0A000 ; video memory graphics segment
- pop es ; pop any extar segments from stack
- xor di,di ; set destination index to 0
- xor ax,ax ; set color register to zero
-
-
-
- ;//////////////////////////////////////////////
- ;******drawing top line of our window
- mov ax,0x02 ; set color to green
-
- mov dx,0 ; initialize counter(dx) to 0
-
- add di,320 ; add di to 320(next line)
- imul di,10 ;multiply by 10 to di to set y cordinate from where we need to start drawing
-
- add di,10 ;set x cordinate of line from where to be drawn
-
-
- _topLine_perPixel_Loop:
-
-
- mov [es:di],ax ; move value ax to memory location es:di
-
- inc di ; increment di for next pixel
- inc dx ; increment our counter
- cmp dx,300 ; comprae counter value with 300
- jbe _topLine_perPixel_Loop ; if <= 300 jump to _topLine_perPixel_Loop
-
- hlt ; halt process after drawing
-
-
- ;//////////////////////////////////////////////
- ;******drawing bottm line of our window
- xor dx,dx
- xor di,di
- add di,320
- imul di,190 ; set y cordinate for line to be drawn
- add di,10 ;set x cordinate of line to be drawn
-
- mov ax,0x01 ; blue color
-
- _bottmLine_perPixel_Loop:
-
- mov [es:di],ax
-
- inc di
- inc dx
- cmp dx,300
- jbe _bottmLine_perPixel_Loop
- hlt
-
-
-
- ;//////////////////////////////////////////////
- ;******drawing left line of our window
- xor dx,dx
- xor di,di
- add di,320
- imul di,10 ; set y cordinate for line to be drawn
-
- add di,10 ; set x cordinate for line to be drawn
-
- mov ax,0x03 ; cyan color
-
- _leftLine_perPixel_Loop:
-
- mov [es:di],ax
-
- inc dx
- add di,320
- cmp dx,180
- jbe _leftLine_perPixel_Loop
-
- hlt
-
-
- ;//////////////////////////////////////////////
- ;******drawing right line of our window
- xor dx,dx
- xor di,di
- add di,320
- imul di,10 ; set y cordinate for line to be drawn
-
- add di,310 ; set x cordinate for line to be drawn
-
- mov ax,0x06 ; orange color
-
- _rightLine_perPixel_Loop:
-
- mov [es:di],ax
-
- inc dx
- add di,320
- cmp dx,180
- jbe _rightLine_perPixel_Loop
-
- hlt
-
-
-
- ;//////////////////////////////////////////////
- ;******drawing line below top line of our window
- xor dx,dx
- xor di,di
-
- add di,320
- imul di,27 ; set y cordinate for line to be drawn
-
- add di,11 ; set x cordinate for line to be drawn
-
- mov ax,0x05 ; pink color
-
- _belowLineTopLine_perPixel_Loop:
-
-
- mov [es:di],ax
-
- inc di
- inc dx
- cmp dx,298
- jbe _belowLineTopLine_perPixel_Loop
-
- hlt
-
-
-
- ;***** print window_text & X char
-
- ;set cursor to specific position
- mov ah,0x02
- mov bh,0x00
- mov dh,0x01 ; y cordinate
- mov dl,0x02 ; x cordinate
- int 0x10
-
- mov si,window_text ; point si to window_text
- call _print_YellowColor_String
-
- hlt
-
-
-
- ;set cursor to specific position
- mov ah,0x02
- mov bh,0x00
- mov dh,0x02 ; y cordinate
- mov dl,0x25 ; x cordinate
- int 0x10
-
- mov ah,0x0E
- mov al,0x58 ; 0x58=X
- mov bh,0x00
- mov bl,4 ; red color
- int 0x10
-
- hlt
-
- ;set cursor to specific position
- mov ah,0x02
- mov bh,0x00
- mov dh,0x02 ; y cordinate
- mov dl,0x23 ; x cordinate
- int 0x10
-
- mov ah,0x0E
- mov al,0x5F ; 0x5F= _
- mov bh,0x00
- mov bl,9 ; red color
- int 0x10
-
- hlt
-
-
- ;set cursor to specific position
- mov ah,0x02
- mov bh,0x00
- mov dh,0x05 ; y cordinate
- mov dl,0x09 ; x cordinate
- int 0x10
-
- mov si,hello_world_text
- call _print_DiffColor_String
-
- hlt
-
-
-
- ;set cursor to specific position
- mov ah,0x02
- mov bh,0x00
- mov dh,0x12 ; y cordinate
- mov dl,0x03 ; x cordinate
- int 0x10
-
- mov si,display_text
- call _print_WhiteColor_String
-
- hlt
Here hello_world_text and window_text are defined in first sector.
- window_text db 10,'Graphics in OS......', 0
- hello_world_text db 10,10, ' Hello World!',0
- display_text db '! Welcome to my Operating System !', 0
Download the source code to view complete code for our operating system.