In this article, we will write our own bootloader using 16-bit assembly language to create our own operating system.
Introduction
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.
Requirements
You need an assembler (that can convert your assembly instructions into raw binary format) and a simulator to view. I am using NASM (Netwide Assembler) and QEMU (Quick Emulator). For Linux, type the following commands to install NASM & QEMU.
  • sudo apt-get install nasm
  • sudo apt-get install qemu qemu-system-x86_64
For Windows, download them from the following sites respectively and install.
  • http://www.nasm.us/
  • https://qemu.weilnetz.de/
Coding
Starting of coding always comes with printing Hello World, doesn't it ? Here's the code that prints "Hello World! " on screen.
(file = hello_world.asm)
  1. [bits 16] ; tell assembler that working in real mode(16 bit mode)
  2. [org 0x7c00] ; organize from 0x7C00 memory location where BIOS will load us
  3. start: ; start label from where our code starts
  4. xor ax,ax ; set ax register to 0
  5. mov ds,ax ; set data segment(ds) to 0
  6. mov es,ax ; set extra segment(es) to 0
  7. mov bx,0x8000
  8. mov si, hello_world ; point hello_world to source index
  9. call print_string ; call print different color string function
  10. hello_world db 'Hello World!',13,0
  11. print_string:
  12. mov ah, 0x0E ; value to tell interrupt handler that take value from al & print it
  13. .repeat_next_char:
  14. lodsb ; get character from string
  15. cmp al, 0 ; cmp al with end of string
  16. je .done_print ; if char is zero, end of string
  17. int 0x10 ; otherwise, print it
  18. jmp .repeat_next_char ; jmp to .repeat_next_char if not 0
  19. .done_print:
  20. ret ;return
  21. times (510 - ($ - $$)) db 0x00 ;set 512 bytes for boot sector which are necessary
  22. dw 0xAA55 ; boot signature 0xAA & 0x55
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
  1. mov ah,0x02 ; set value for change to cursor position
  2. mov bh,0x00 ; page
  3. mov dh,0x06 ; y cordinate/row
  4. mov dl,0x05 ; x cordinate/col
  5. int 0x10
  6. mov si, hello_world
  7. call print_string
To get input first, set ax to 0x00 and call int 0x16. To display a character which has been input, move ah to 0x0E and call int 0x10. It will store char value to al register & key code to ah register.
e.g
  1. inputLoop:
  2. mov ax,0x00
  3. int 0x16
  4. cmp ah,0x1C ; compare input is enter(1C) or not
  5. je .inputLoop
  6. cmp al,0x61 ; compare input is character 'a' or not
  7. je exitLoop
  8. mov ah,0x0E ;display input char
  9. int 0x10
  10. exitLoop:
  11. ret
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
  1. ; load second sector from memory
  2. mov ah, 0x02 ; load second stage to memory
  3. mov al, 1 ; numbers of sectors to read into memory
  4. mov dl, 0x80 ; sector read from fixed/usb disk
  5. mov ch, 0 ; cylinder number
  6. mov dh, 0 ; head number
  7. mov cl, 2 ; sector number
  8. mov bx, _OS_Stage_2 ; load into es:bx segment :offset of buffer
  9. int 0x13 ; disk I/O interrupt
  10. jmp _OS_Stage_2 ; jump to second stage
For clearing the screen, copy 0x13 to ax & call video interrupt.
  • mov ax,0x13
  • int 0x10
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.
  1. mov ax,0x13 ; clears the screen
  2. int 0x10
  3. ;//////////////////////////////////////////////////////////
  4. ; drawing window with lines
  5. push 0x0A000 ; video memory graphics segment
  6. pop es ; pop any extar segments from stack
  7. xor di,di ; set destination index to 0
  8. xor ax,ax ; set color register to zero
  9. ;//////////////////////////////////////////////
  10. ;******drawing top line of our window
  11. mov ax,0x02 ; set color to green
  12. mov dx,0 ; initialize counter(dx) to 0
  13. add di,320 ; add di to 320(next line)
  14. imul di,10 ;multiply by 10 to di to set y cordinate from where we need to start drawing
  15. add di,10 ;set x cordinate of line from where to be drawn
  16. _topLine_perPixel_Loop:
  17. mov [es:di],ax ; move value ax to memory location es:di
  18. inc di ; increment di for next pixel
  19. inc dx ; increment our counter
  20. cmp dx,300 ; comprae counter value with 300
  21. jbe _topLine_perPixel_Loop ; if <= 300 jump to _topLine_perPixel_Loop
  22. hlt ; halt process after drawing
  23. ;//////////////////////////////////////////////
  24. ;******drawing bottm line of our window
  25. xor dx,dx
  26. xor di,di
  27. add di,320
  28. imul di,190 ; set y cordinate for line to be drawn
  29. add di,10 ;set x cordinate of line to be drawn
  30. mov ax,0x01 ; blue color
  31. _bottmLine_perPixel_Loop:
  32. mov [es:di],ax
  33. inc di
  34. inc dx
  35. cmp dx,300
  36. jbe _bottmLine_perPixel_Loop
  37. hlt
  38. ;//////////////////////////////////////////////
  39. ;******drawing left line of our window
  40. xor dx,dx
  41. xor di,di
  42. add di,320
  43. imul di,10 ; set y cordinate for line to be drawn
  44. add di,10 ; set x cordinate for line to be drawn
  45. mov ax,0x03 ; cyan color
  46. _leftLine_perPixel_Loop:
  47. mov [es:di],ax
  48. inc dx
  49. add di,320
  50. cmp dx,180
  51. jbe _leftLine_perPixel_Loop
  52. hlt
  53. ;//////////////////////////////////////////////
  54. ;******drawing right line of our window
  55. xor dx,dx
  56. xor di,di
  57. add di,320
  58. imul di,10 ; set y cordinate for line to be drawn
  59. add di,310 ; set x cordinate for line to be drawn
  60. mov ax,0x06 ; orange color
  61. _rightLine_perPixel_Loop:
  62. mov [es:di],ax
  63. inc dx
  64. add di,320
  65. cmp dx,180
  66. jbe _rightLine_perPixel_Loop
  67. hlt
  68. ;//////////////////////////////////////////////
  69. ;******drawing line below top line of our window
  70. xor dx,dx
  71. xor di,di
  72. add di,320
  73. imul di,27 ; set y cordinate for line to be drawn
  74. add di,11 ; set x cordinate for line to be drawn
  75. mov ax,0x05 ; pink color
  76. _belowLineTopLine_perPixel_Loop:
  77. mov [es:di],ax
  78. inc di
  79. inc dx
  80. cmp dx,298
  81. jbe _belowLineTopLine_perPixel_Loop
  82. hlt
  83. ;***** print window_text & X char
  84. ;set cursor to specific position
  85. mov ah,0x02
  86. mov bh,0x00
  87. mov dh,0x01 ; y cordinate
  88. mov dl,0x02 ; x cordinate
  89. int 0x10
  90. mov si,window_text ; point si to window_text
  91. call _print_YellowColor_String
  92. hlt
  93. ;set cursor to specific position
  94. mov ah,0x02
  95. mov bh,0x00
  96. mov dh,0x02 ; y cordinate
  97. mov dl,0x25 ; x cordinate
  98. int 0x10
  99. mov ah,0x0E
  100. mov al,0x58 ; 0x58=X
  101. mov bh,0x00
  102. mov bl,4 ; red color
  103. int 0x10
  104. hlt
  105. ;set cursor to specific position
  106. mov ah,0x02
  107. mov bh,0x00
  108. mov dh,0x02 ; y cordinate
  109. mov dl,0x23 ; x cordinate
  110. int 0x10
  111. mov ah,0x0E
  112. mov al,0x5F ; 0x5F= _
  113. mov bh,0x00
  114. mov bl,9 ; red color
  115. int 0x10
  116. hlt
  117. ;set cursor to specific position
  118. mov ah,0x02
  119. mov bh,0x00
  120. mov dh,0x05 ; y cordinate
  121. mov dl,0x09 ; x cordinate
  122. int 0x10
  123. mov si,hello_world_text
  124. call _print_DiffColor_String
  125. hlt
  126. ;set cursor to specific position
  127. mov ah,0x02
  128. mov bh,0x00
  129. mov dh,0x12 ; y cordinate
  130. mov dl,0x03 ; x cordinate
  131. int 0x10
  132. mov si,display_text
  133. call _print_WhiteColor_String
  134. hlt
Here hello_world_text and window_text are defined in first sector.
  1. window_text db 10,'Graphics in OS......', 0
  2. hello_world_text db 10,10, ' Hello World!',0
  3. display_text db '! Welcome to my Operating System !', 0
Download the source code to view complete code for our operating system.