Hi there! This is my first attempt to write a program in x86 assembly. I’ll call it BootBoot and it will be a boot loader that boots a program from the first floppy drive, displays an effect, and then boots grub and the OS. I am going to write a blog post after each commit to remember the steps I followed (hoping that writing my next ASM program will be easier if every step of this one is documented). In this post (part 1) my goal is to load a program that boots from the floppy drive and clears the screen.
Prerequisites:
My tools (for the moment) are:
-
NASM: an asssembler for the x86 CPU architecture.
To install it on Debian: sudo apt-get install nasm -
QEMU: a generic and open source machine emulator and virtualizer.
To install it on Debian: sudo apt-get install qemu qemu-system-x86 - GNU Make: a utility which controls the generation of executables and other target files of a program from the program’s source files.
I guess everyone has this one: sudo apt-get install make
I need nasm
to assemble the assembly code and qemu
to boot an imaginary floppy drive to avoid actual reboots of the computer I use for the development. I’ve put my initial rules in a Makefile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
bin = bootboot ASFLAGS = -f bin $(bin): bb.asm nasm $(ASFLAGS) -o $@ $< floppy.img: $(bin) dd if=/dev/zero of=$@ bs=1024 count=1440 dd if=$< of=$@ bs=512 conv=notrunc .PHONY: clean clean: rm -f $(bin) .PHONY: run run: floppy.img qemu-system-i386 -fda $< |
ASFLAGS are the assembler flags and -f
means to generate flat binary files (files without headers). I am going to use one flat file for all my code for simplicity.
The following rule creates a floppy image of 512 bytes:
1 2 3 |
floppy.img: $(bin) dd if=/dev/zero of=$@ bs=1024 count=1440 dd if=$< of=$@ bs=512 conv=notrunc |
To load it using qemu one needs to call make run which is equivalent to creating a floppy image and calling qemu-system-i386 -fda <floppy image> afterwards. More on Makefiles here and here. -fda means the first floppy disk found in the system (floppy disk a).
With this Makefile I can type make, make clean and make run to assemble, clear and run the program respectively.
Code:
I am going to push my commits in this repository. The code of this first commit is here.
Let’s start writing the code:
First thing, we need to set the address from which the assembler will begin reading the commands. We can do this by calling: org 7c00h where 7c00h (h is for hex) is the address where the bios loads the first sector.
When we boot an x86 system, it is in what is called “real mode” or “8086 mode”, which is the architecture we would have if we were booting an actual 8086 computer. As 8086 is a 16-bit microprocessor, we must tell the assembler that we are going to use 16 bits. The x86 assembly command for this is: bits 16.
We are going to use the label start: to mark the beginning of the program. I am going to follow a convention I’ve seen in other people’s code in this program: I will use labels that start with a dot for repetitive local changes (for example loops) and labels without a dot to mark pieces of code where I tend to jump often (for example a piece of code that I’d put in a function if I was writing C).
And before we begin writing the actual program I am going to push the 7c00h address in the stack pointer calling mov sp, 7c00h.
The code so far is the following:
1 2 3 4 5 |
org 7c00h bits 16 start: mov sp, 7c00h |
Let’s clear the screen!
Now, we need to set video mode. I’m going to use a relatively “simple” video mode that uses 256 colors: 320x200
. To select it we’ll use the video bios interrupt
int 10h.
According to the documentation here we need to fill the ax
register with 0 in the 16 higher bits (register ah
) to select the service and with the number that corresponds to the desired video mode in the 16 lower bits (register al
). The desired video mode is:
13h = G 40x25 8x8 320x200 256/256K . A000 VGA,MCGA,ATI VIP, and so we write:
1 2 |
mov ax, 13h int 10h |
to fill the ah
with 0
, the al
with 13
(in hex) and to send the video mode interrupt (
int 10h).
Now we can finally clear the screen.
As we might need to do this often, we can use a label clearscreen
(in C that would be a function) where we can jump everytime we need to clear the screen. Something like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
clearscreen: mov ax, 0a000h mov ds, ax mov di, 0 mov cx, 64000 mov ax, 3 .loop_begin: mov [di], al inc di dec cx jnz .loop_begin ret |
To understand clearscreen
we need to understand a few things about mode 13h
and real (8086
) mode.
In 13h
, the video RAM is mapped in a0000
and the first 64000
bytes appear on the screen immediately after update.
The memory addresses in real-mode are using 20bits
and have 2 16-bits overlapping parts: the offset part and the segment part. The segment is shifted by 4bits (to the left so it becomes 20bits) and is added to the overlapping offset.
The segment registers we can use are es
, ds
, cs
, ss
(and: fs
, gs
in some systems: 386 and on). In clearscreen I used the ds
that is the default segment register.
We can’t write values to ds
directly. Therefore we are going to use a common register (ax
) to write our value and then move its content to ds
:
1 2 3 |
mov ax, 0a000h mov ds, ax mov di, 0 |
We shifted the a0000
by 4bits (and so it become a000
and we wrote 0a000
because it starts by a letter and not a digit) and then we moved its value to ds
.
Then we’ve set di
(one of the registers we usually use in address calculations and stands for destination index) to 0
. Note that this is because the offset of the address calculation can be paired with any register.
Now we can start a loop and fill all the 64000 bytes from the a0000 memory address and on. To do so we need a color and a counter.
The color we are going to use is the one we set to ax in this line: mov ax, 3 (it’s a light blue in the default palette).
We will need a counter to count the 64000 bytes so that we know when to exit the loop: mov cx, 64000.
Let’s see the loop:
1 2 3 4 5 6 |
.loop_begin: mov [di], al inc di dec cx jnz .loop_begin ret |
The brackets around [di]
are used to dereferrence it: we are going to modify the memory address of di
. We move the lower 8 bits of al
in di
address to write the color in the memory and we increase di
to point at the next byte (
inc di ).
Then, we decrease the counter (
dec cx ). We want to exit the loop when the counter value reaches 0
, and so we jump to the beginning of the loop while cx
is not 0
with
jnz .loop_begin (where jnz
means jump, non zero).
When we exit the loop
ret will redirect the execution to the command that follows the
call clearscreen. It’s similar to return
in C.
After that we create an infinite loop to have our program refreshing the pixels for long so that we can check the color.
1 2 3 |
; infinite loop end: jmp end |
There’s one final “trick” I used at the end of the program:
1 2 |
times 510-($-$$) db 0 dw 0aa55h |
This “tells” the assembler to fill the remaining bytes of the 512 bytes of the floppy disk with 0 (the bytes that aren’t occupied by the program). I did this to fill the image I use with qemu to avoid potential problems.
Result:
VoilΓ the program running in qemu after this first commit:
Putting things together:
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
org 7c00h bits 16 start: mov sp, 7c00h mov ax, 13h int 10h call clearscreen end: jmp end clearscreen: mov ax, 0a000h mov ds, ax mov di, 0 mov cx, 64000 mov ax, 3 .loop_begin: mov [di], al inc di dec cx jnz .loop_begin ret times 510-($-$$) db 0 dw 0aa55h |
Makefile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
bin = bootboot ASFLAGS = -f bin $(bin): bb.asm nasm $(ASFLAGS) -o $@ $< floppy.img: $(bin) dd if=/dev/zero of=$@ bs=1024 count=1440 dd if=$< of=$@ bs=512 conv=notrunc .PHONY: clean clean: rm -f $(bin) .PHONY: run run: floppy.img qemu-system-i386 -fda $< |
and that’s it.
Both files will be extended in the next post.
Links:
[1]: Ralf Brown’s Interrupt List
[2]: Mode 13h in Wikipedia
[3]: X86 memory segmentation
[4]: NASM – The Netwide Assembler