Realmode Assembly - Writing bootable stuff - Part 2

Realmode Assembly - Writing bootable stuff - Part 2

Part 2: Hello World Bootloader


What is this?

This is going to be a walk-through in writing an Operation System in assembly which operates purely in Realmode.
Goal will be writing different kernels from a simple “Hello World” over small terminal programs to
graphically displayed games.

Requirements:

  • Being able to read x86 Intel Assembly
  • Being tolerant enough to accept my assembly code even though it might not be perfect
  • Reading the previous article

Notes

  • This information is the result of my research and own programming, everything said here might be wrong, correct me if you spot mistakes though!
  • I will try to list my sources at the bottom but I can’t guarantee that these are all of them.
  • I’M NOT RESPONSIBLE IF YOU BREAK SOMETHING USING INFORMATION I PROVIDED HERE.

Content of this Article:

This article will be about writing a Master Boot Record that loads a small
kernel into memory that then prints out a “Hello World” message

Tools used:

  • NASM for compiling assembly code to raw binary
  • A text editor to write the code in

Writing a loader for the kernel:

So to start we are going to write a minimalistic bootloader that just loads the kernel and then gives control to it. We will improve the bootloader later and make it more abstract and reliable but for the explanation this will be enough.

Let’s go:
We will need to execute two interrupts, one to reset the disk system which will ensure we will read the right partand one to read the kernel from hard drive to RAM. As already explained in the last article we will use the interrupt 0x13 which decides which disk operation to execute based on the ah register (higher byte of ax, which is the lower word of eax).

For the disk resetting we will use the first function of the interrupt 0x13, so ah will be set to 0. I will use the syntax int 0x13,0 to refer to this interrupt to make my life a bit easier. Int 0x13,0 takes two parameters ah = 0 and dl = drive number (a number representing a specific drive. for example: 0x80 = drive 0). Luckily the BIOS (or better said almost every BIOS) puts the drive number of the device currently booting from (in our case the hard drive the kernel is on or an emulated device) into dl before giving control to the MBR, so we will assume that the drive number is already in dl which will be the case most of the time.

|----int----|--ah--|------dl------|------Description------|
|  int 0x13 |   0  | drive number | Resetting Disk System |
|-----------|------|--------------|-----------------------|

As already explained previously the bootloader will be loaded into 0st segment at address 0x7c00 so we also have to tell NASM to calculate offsets starting from that address. Also let’s tell NASM that we are in 16bit mode so it tells us when we use illegal instructions for this mode and knows how to correctly refer to memory.

org 0x7C00 ;NASM now calculated everything with the right offset
bits 16
mov ah, 0
int 0x13 ;  int 0x13,0  dl = drive number
..

Now that the disk system is reset we can start reading from the correct offset of the hard drive we boot from. For that we will use int 0x13,2. It takes an address within the RAM to write in as argument into bx, we will just write it to 0x8000 as it’s free space. The al register will contain the amount of sectors to read, this will be the size of our kernel and as our HelloWorld Kernel will be small we just write a 1 into al (1 sector = 512 bytes, size of kernel has to be multiple of 512 bytes). If we increase the size of our kernel we have to increase this number as well and because it’s pretty annoying to change this every time we make the kernel bigger so we will later look at ways to make this easier for us. The address to read from is in the Cylinder-Head-Sector (CHS) format (which is a pretty old format for addresses on hard drives) so we have to do some converting (also knowing the math behind those numbers helps reading from other segments):

(Following values are true for 1.44 MB 3.5" Floppy Disks , the theory applies for every hard drive. Obviously this memory layout doesn’t make sense for USB-Devices and similar but as it’s the way the BIOS works we will later add a Standard BIOS Parameter Block which will define the following values for the BIOS)

Each Sector has a size of 512 (BytesPerSector) bytes. Sectors start with 1.
Each Head contains 18 (SectorsPerTrack) sectors.
Each Cylinder (also called Track) contains 2(Sides) Heads. There are 2(Sides) Cylinders.

Using this we can calculate the CHS-Values for the logical segment(continuous numeration starting from 0) we want to read from. The bootloader is at logical segment 0 (as it’s the first segment on the hard drive). After that the kernel follows at logical segment 1 (as it’s directly after the bootloader on the hard drive and the bootloader is exactly 512 bytes (1 segment) in size.)

|----int----|--ah--|------dl------|-------ch------|------dh------|------cl--------|------Description------|
|  int 0x13 |   2  | drive number | Reading Track | Reading Head | Reading Sector | Read from Hard drive  |
|-----------|------|--------------|---------------|--------------|----------------|-----------------------|
Logical segment ls = 1
=>
Cylinder is saved in ch
ch = Cylinder/Track = (ls/SectorsPerTrack)/Sides = 0
Head is saved in dh
dh = Head =(ls/SectorsPerTrack)%Sides = 0
Sector is saved in cl
cl = Sector = (ls%SectorsPerTrack)+1 = 2

Our new code now looks like this:

..
mov bx, 0x8000     ; bx = address to write the kernel to
mov al, 1 		   ; al = amount of sectors to read
mov ch, 0          ; cylinder/track = 0
mov dh, 0          ; head           = 0
mov cl, 2          ; sector         = 2
mov ah, 2          ; ah = 2: read from drive
int 0x13   		   ; => ah = status, al = amount read

Ok next let’s add the remaining parts to make this bootloader work:

..
; pass execution to kernel	
jmp 0x8000
;$ = address of current position, $$ = address for start of segment, so ($-$$) = amount of already filled bytes of this segment
;pads everything from here up to 510 with 0's, also gives compiler errors if not possible which
;might happen if we already wrote more than 510 bytes in this segment and thus causes ($-$$) to be negative
;this is very useful as it makes sure that the resulting binary has a size multiple of 512 which is required to make everything work
times 510-($-$$) db 0
;Begin MBR Signature
db 0x55 ;byte 511 = 0x55
db 0xAA ;byte 512 = 0xAA

Ok now our bootloader is finally done but as we don’t have a kernel yet we can’t test it.

Complete MBR Code:

org 0x7C00
;Reset disk system
mov ah, 0
int 0x13 ; 0x13 ah=0 dl = drive number

;Read from harddrive and write to RAM
mov bx, 0x8000     ; bx = address to write the kernel to
mov al, 1 		   ; al = amount of sectors to read
mov ch, 0          ; cylinder/track = 0
mov dh, 0          ; head           = 0
mov cl, 2          ; sector         = 2
mov ah, 2          ; ah = 2: read from drive
int 0x13   		   ; => ah = status, al = amount read
jmp 0x8000
times 510-($-$$) db 0
;Begin MBR Signature
db 0x55 ;byte 511 = 0x55
db 0xAA ;byte 512 = 0xAA

Writing a kernel for the loader:

Now that we have a bootloader that loads our kernel let’s start writing our kernel. The kernel is supposed to print out a “Hello World” message and then halt/stop everything.

Printing something to the display means we need a way to interact with it. BIOS Interrupt 0x10 will help us here as it’s responsible for all kinds of video services (printing characters, drawing pixels, …). We will print the string “Hello World” character after character using int 0x10,0xE which takes a single character (ASCII) in register al, the page to write to in bh (there is enough memory to have a few text pages for quick swapping, default is page 0) and color attributes in bl (but this is only displayed if we are in a graphical mode with which we will mess later).

|----int----|--ah--|------------al------------|---------bh-------|-------bl-------|-----------Description-----------|
|  int 0x10 | 0xE  | ASCII Character to print | Page to write to | Color Attribute| Print a character to the screen |
|-----------|------|--------------------------|------------------|----------------|---------------------------------|

So our print character functions should look like this:

printCharacter:
	;before calling this function al must be set to the character to print
	mov bh, 0x00 ;page to write to, page 0 is displayed by default
	mov bl, 0x00 ;color attribute, doesn't matter for now
	mov ah, 0x0E 
	int 0x10 ; int 0x10, 0x0E = print character in al
	ret	

Given how to print a single character the remaining code for printing a string is pretty simple:

printNullTerminatedString:
	pusha ;save all registers to be able to call this from where every we want
	.loop:
		lodsb ;loads byte from si into al and increases si
		test al, al ;test if al is 0 which would mean the string reached it's end
		jz .end
		call printCharacter ;print character in al
	jmp .loop ;print next character
	.end:
	popa ;restore registers to original state
	ret

Now we just have to put it together by telling NASM the right offset and instruction size and actually calling the printNullTerminatedString function. Note that I added a padding again to ensure that the final kernel has a size multiple of 512 as it might result in problems reading from the hard drive if the size isn’t correct.

org 0x8000 
bits 16
mov si, msg
call printNullTerminatedString

jmp $   ; this freezes the system, best for testing
hlt		;this makes a real system halt
ret     ;this makes qemu halt, to ensure everything works we add both

printCharacter:
	;before calling this function al must be set to the character to print
	mov bh, 0x00 ;page to write to, page 0 is displayed by default
	mov bl, 0x00 ;color attribute, doesn't matter for now
	mov ah, 0x0E 
	int 0x10 ; int 0x10, 0x0E = print character in al
	ret	
printNullTerminatedString:
	pusha ;save all registers to be able to call this from where every we want
	.loop:
		lodsb ;loads byte from si into al and increases si
		test al, al ;test if al is 0 which would mean the string reached it's end
		jz .end
		call printCharacter ;print character in al
	jmp .loop ;print next character
	.end:
	popa ;restore registers to original state
	ret
msg db "Hello World!"
times 512-($-$$) db 0 ;kernel must have size multiple of 512 so let's pad it to the correct size

How to build and run this?

You can build it with the following commands:

nasm -fbin bootloader.asm -o bootloader.bin
nasm -fbin kernel.asm -o kernel.bin
cat bootloader.bin kernel.bin > result.bin

And run it with this:

qemu-system-i386 result.bin

Conclusion:

We now have a working bootloader and kernel. This concludes the HelloWorld kernel and bootloader part. The next part will be about how to build and run it in more detail also it will contain information on how to load the self written kernel binary to an USB-Drive and actually test it on your computer. I hope this part contained information useful to you. If I wrote something wrong just point it out and I will correct it. Feedback is appreciated.

Next Part

Previous Part


Link to source files:

Other Hello World bootloaders (both don’t load a kernel but are interesting anyways):

Sources:

30 Likes

I really like this series. Extremely interesting. :slight_smile:

I hope you can keep the requirements low, so that I can follow.

Can you use those assembler interrupts and commands in your normal OS?
Maybe to halt the system or reset the disk like you did before booting?

2 Likes

Great post @Leeky!

As discussed yesterday on IRC… this is slight modification poking the video memory instead of using the slow BIOS :wink:

org 0x8000
bits 16
	push  0xb800
	pop   es     		; Set ES to the Video Memory
	;; Clear screen
	mov ax, 0x1000    	; Clean screen blue background
	call  cls
	;; Print message
	mov   si, msg
	call  print
	;; Done!
	jmp $   ; this freezes the system, best for testing
	hlt	;this makes a real system halt
	ret     ;this makes qemu halt, to ensure everything works we add both

cls:
	xor   di,di
	mov   cx, 80*24		;Default console size
	repnz stosw
	ret

print:
	xor   di, di
	mov   ah, 0x1e		; Text Color :)

.loop:
	lodsb       ;loads byte from si into al and increases si
	test  al, al ;tests end of string
	jz    .end
        stosw	    ; Stores AX (char + color)
	jmp   .loop ;print next character

.end:
	ret


msg   db "Hello World!"
times 512-($-$$) db 0 ; Make it a disk sector

7 Likes

Let’s start with the interrupts. As they are only available in realmode you need to switch from your current mode (Long Mode / Protected Mode) to it and that’s nothing normal user programs can do (nor do I recommend that, didn’t try it myself though). But if you are running DOS you can use them freely as far as I know.
The command thing is a bit different as it’s not using the BIOS. For example the hlt instruction is only available from ring 0 (kernel mode) so again nothing a user program can do but nothing impossible either.

Hope this helped.

###Sources:

3 Likes

ok, thank you for explaining :slightly_smiling_face:

Would have been too easy and powerful

1 Like

Great tutorial.
I’ve completed this part. Also I’ve implemented ISR and IRQ. I would like to know what’s next!

1 Like

I’m happy that you enjoyed it!
The next parts that I’ve planned will be about How to Run/Build it and Writing a small input/output console kernel.
I haven’t messed with the Interrupt Service Routines that much yet, only ever written a custom keyboard handler with it, I’m very interested in your code if you are willing to share it! :smiley:

I’m happy to share my code and open to any improvements.
miniOS

Feel free to contribute :slight_smile:

3 Likes

Interesting stuff there!
I never messed with protected mode before but I see lots of familiar code. :smiley: I also see that you are working with both C and Assembly which is apparently quite common but I’ve never messed with that either (as I mainly did real mode assembly to get into x86 assembly). Is that something people are interested in for further walk-throughs ( I mean this is a assembly article but I could still show some c/asm code)? The resources also look excellent, will read through them (but I have lots of other stuff to read first).Loading the OS to GDB is very interesting as well, my debugging was more of a trial and error approach, need to experiment with that for the next part!

Thank you for comment.
I also see that you haven’t messed with graphical modes yet so this might get interesting for you as well!

2 Likes

I want to extend it further. But it’s just difficult and time consuming for me to do it alone

1 Like

Nice series @Leeky . Looking forward to the next parts to extend my knowledge on more low level stuff other than asm for reversing :smiley:

1 Like

HI all, you can find a basic OS made by me. See here https://github.com/ashishhacker/TinuOS

1 Like

yeay ! :grin: Is there part 3 ?

1 Like

Fret not! Part 3 is being worked on.

1 Like