I just do random things, it could be a game in unity or it might be building a simple calculator in asm!
Hello everyone, thanks for clicking on my first-ever blog post! Today, I will be “attempting” to write my very own boot-loader with a very simple and primitive calculator. It might only do addition but it still works!
The first thing we need to do is have a file that we can run, and it will set the rest up! I called this file “boot.asm”. It starts by setting up the dx register for disk loading and then setting up the stack. We then need to set up the ES:BX memory address.
After this, we need to call disk_load, which sets up the kernel; we will discuss this later. At the end of this, we need to actually set up the 512 bytes the boot loader can access by just jumping to every byte and setting it to zero.
At the very last 2 bytes, we need to set the 511th digit to AA and the 512th digit to 55. This basically tells the bios that this can be loaded.
bits 16
global _start
_start:
mov dh, 2
mov dl, 0
mov bp, 0x9000
mov sp, bp
mov bx, 0x1000
mov es, bx
mov bx, 0
call disk_load
jmp 0x1000:0
%include "drivers/disk.asm"
times 510-($ - $$) db 0
dw 0xAA55
I should probably tell you what the “disk.asm” file does. It contains the disk_load function. It’s actually simpler than expected. All we need to do is run an interrupt and tell it what sectors to read and the drive number. This is where we need the ES:BX memory address to be set up.
Once we move all the right data into the right register, we can run interrupt 13 which reads the sectors into memory so we can actually use them in the kernel. In case we get an error when reading the disk, we can just throw an error that will just halt everything, hence the hlt command.
disk_load:
push dx
mov ah, 0x02
mov al, dh
mov ch, 0x00
mov dh, 0x00
mov cl, 0x02
int 0x13
jc disk_error
pop dx
cmp dh, al
jne disk_error
ret
disk_error:
hlt
After all of this setup, we can actually run the kernel! We do need to set up a couple of drivers beforehand. We need a way to print a character to the screen, set the video mode, clear the screen, and last but not least, create a new line.
We can do all of this in less than 40 lines of code! Now, if I understood assembly better, I would make it so that I can print more than 1 char at a time. But I decided to put myself through less pain and just cause more work for myself later on.
Either way, we can do this all with some interrupts. We can set the video mode by calling the 0x10 or the 10th intercept; we do need to set up 2 prior registers first, ah and al. We can set ah to 0 or 0x00 and al to 1 or 0x01. We can then call interrupt 10 and it will set the video mode. We now need a way to clear the screen.
Once again, we can use the 10th interrupt. We can set ax to 0x0003 and then call intercept 10. We can clear the screen this way. Now, we once again need to use the 10th interrupt to print a character to the screen. We set ah to 0x0e and then call interrupt 10.
Now, the last thing we need for this driver is to be able to create a new line. Surprisingly, we do not use the 10th interrupt, we instead use the print char function from earlier to do it. We set the ax register to 0x0a and then call print_char. After that, we need to set al to 0x0d and then call print_char again, and this should be it for our video driver!
ret
set_video_mode:
mov ah, 0x00
mov al, 0x01
int 0x10
clear_screen:
push ax
mov ax, 0x0003
int 0x10
pop ax
ret
print_char:
push ax
push bx
push cx
push dx
push si
push di
mov ah, 0x0e
int 0x10
pop di
pop si
pop dx
pop cx
pop bx
pop ax
ret
new_line:
push ax
mov ax, 0x0a
call print_char
mov al, 0x0d
call print_char
pop ax
ret
We need to create a driver to read the keyboard driver. We can do this with another interrupt, but instead of using the 10th interrupt, we use the 16th interrupt. We can do all of this by setting ah to 0, and then just call the 16 interrupt. This should be the end of our keyboard driver!
ret
read_keyboard:
push bx
push cx
push dx
push si
push di
mov ah, 0x00
int 0x16
pop di
pop si
pop dx
pop cx
pop bx
ret
Now, we have all we need to run our kernel! A lot of the code in our kernel is just printing characters to write whole words. Since I did not know how to write a function to write an entire word at a time, I have to specify each individual character.
After I write “Please Input Any Digit"!” to the screen, we need to read the keyboard. We can do this with a loop, now we can only have 1 thing that can trigger the loop to end. We do this with a lot of fancy code I do not even understand very well, so I am not even going to attempt to explain.
We then do this all again. We can take the 2 digits and convert their uni code to just the numbers by subtracting 48 from each. We then add both together and add 48 back to it so we can print the uni code on the screen. This is all I added to the kernel!
bits 16
start:
call set_video_mode
call clear_screen
mov al, 'P'
call print_char
mov al, 'l'
call print_char
mov al, 'e'
call print_char
mov al, 'a'
call print_char
mov al, 's'
call print_char
mov al, 'e'
call print_char
mov al, ' '
call print_char
mov al, 'I'
call print_char
mov al, 'n'
call print_char
mov al, 'p'
call print_char
mov al, 'u'
call print_char
mov al, 't'
call print_char
mov al, ' '
call print_char
mov al, 'A'
call print_char
mov al, 'n'
call print_char
mov al, 'y'
call print_char
mov al, ' '
call print_char
mov al, 'D'
call print_char
mov al, 'i'
call print_char
mov al, 'g'
call print_char
mov al, 'i'
call print_char
mov al, 't'
call print_char
mov al, '!'
call print_char
l1:
call new_line
mov al, '>'
call print_char
mov al, ' '
call print_char
call read_keyboard
call print_char
cmp al, '1'
mov cl, al
cmp al, '2'
mov cl, al
cmp al, '3'
mov cl, al
cmp al, '4'
mov cl, al
cmp al, '5'
mov cl, al
cmp al, '6'
mov cl, al
cmp al, '7'
mov cl, al
cmp al, '8'
mov cl, al
cmp al, '9'
mov cl, al
cmp al, '0'
mov cl, al
mov al, '1'
cmp al, '1'
jne l1
call new_line
mov al, '1'
call print_char
mov al, 's'
call print_char
mov al, 't'
call print_char
mov al, ' '
call print_char
mov al, 'D'
call print_char
mov al, 'i'
call print_char
mov al, 'g'
call print_char
mov al, 'i'
call print_char
mov al, 't'
call print_char
mov al, ':'
call print_char
mov al, ' '
call print_char
mov al, cl
call print_char
call new_line
call new_line
mov al, 'P'
call print_char
mov al, 'l'
call print_char
mov al, 'e'
call print_char
mov al, 'a'
call print_char
mov al, 's'
call print_char
mov al, 'e'
call print_char
mov al, ' '
call print_char
mov al, 'I'
call print_char
mov al, 'n'
call print_char
mov al, 'p'
call print_char
mov al, 'u'
call print_char
mov al, 't'
call print_char
mov al, ' '
call print_char
mov al, 'A'
call print_char
mov al, 'n'
call print_char
mov al, 'y'
call print_char
mov al, ' '
call print_char
mov al, 'D'
call print_char
mov al, 'i'
call print_char
mov al, 'g'
call print_char
mov al, 'i'
call print_char
mov al, 't'
call print_char
mov al, '!'
call print_char
l2:
call new_line
mov al, '>'
call print_char
mov al, ' '
call print_char
call read_keyboard
call print_char
cmp al, '1'
mov dl, al
cmp al, '2'
mov dl, al
cmp al, '3'
mov dl, al
cmp al, '4'
mov dl, al
cmp al, '5'
mov dl, al
cmp al, '6'
mov dl, al
cmp al, '7'
mov dl, al
cmp al, '8'
mov dl, al
cmp al, '9'
mov dl, al
cmp al, '0'
mov dl, al
mov al, '1'
cmp al, '1'
jne l2
call new_line
mov al, '2'
call print_char
mov al, 'n'
call print_char
mov al, 'd'
call print_char
mov al, ' '
call print_char
mov al, 'D'
call print_char
mov al, 'i'
call print_char
mov al, 'g'
call print_char
mov al, 'i'
call print_char
mov al, 't'
call print_char
mov al, ':'
call print_char
mov al, ' '
call print_char
mov al, dl
call print_char
call new_line
end:
call new_line
mov al, cl
call print_char
mov al, ' '
call print_char
mov al, '+'
call print_char
mov al, ' '
call print_char
mov al, dl
call print_char
mov al, ' '
call print_char
mov al, '='
call print_char
mov al, ' '
call print_char
sub cl, 48
sub dl, 48
add cl, dl
add cl, 48
mov al, cl
call print_char
jmp $
%include "drivers/vga_driver.asm"
%include "drivers/keyboard_driver.asm"
times 1024-($-$$) db 0
In the very end, we just need to combine everything. I do this with a very simple make file. It’s not much but it works!
all: clean run
boot.o: boot.asm
nasm $< -f elf -o [email protected]
drivers/keyboard_driver.o: drivers/keyboard_driver.asm
nasm $< -f elf -o [email protected]
drivers/vga_driver.o: drivers/vga_driver.asm
nasm $< -f elf -o [email protected]
drivers/disk.o: drivers/disk.asm
nasm $< -f elf -o [email protected]
kernel/kernel.o: kernel/kernel.asm
nasm $< -f elf -o [email protected]
boot.bin: boot.o kernel/kernel.o drivers/disk.o drivers/vga_driver.o drivers/keyboard_driver.o
ld -m elf_i386 -o [email protected] -Ttext 0x1000 $^ --oformat binary
run: boot.bin
qemu-system-i386 -fda boot.bin
clean:
$(RM) *.bin *.o
$(RM) drivers/*.o
I am sorry about the amount of code, there is definitely a better way to do this. I am just too much of a noob to figure it out. I will also link my GitHub repo at the bottom of this post for anyone curious! If anyone has any suggestions please tell me in the comments! Thanks for reading my first blog post!
https://github.com/RoboTiberius/Primiative-Boot-Loader
I am also taking heavy inspiration from:
https://www.youtube.com/watch?v=5FnrtmJXsdM&list=PLT7NbkyNWaqajsw8Xh7SP9KJwjfpP8TNX&index=2