Hello World in X86 NASM Assembly

by CoryHall in Circuits > Computers

4442 Views, 1 Favorites, 0 Comments

Hello World in X86 NASM Assembly

qemu4.png

Assembly (Or Assembler Code) is a low-level programming language that relies heavily on BIOS interrupts, which are functions that are used to perform actions on a computer. This instructable aims to teach the basics of the language, by creating a basic bootloader and displaying Hello World to the screen.

IMPORTANT NOTE

This tutorial was made using a WINDOWS pc. As a result of this, different methods will need to be used to compile and run your code on a different OS, such as linux.

Supplies

REQUIREMENTS

- NASM

- QEMU

- Visual Studio Code (A different text editor can be used if you so desire.)

Install QEMU

Click here to download QEMU. Select the latest version. When the .exe has finished downloading, run it, and go through the installation process. If required, run the program as an administrator.

Install NASM

Click here to download NASM. When the file has finished downloading, run the installer. Take note of the path in the installer, this will be important in the next step.

Add NASM to Path.

Environment Variables.png

Open the start menu, and type "Environment Variables", and press enter.

When the window has opened, click on environment variables.

Double click on "Path", then click new.

Type in the path of your NASM installation, and press OK, until all windows have closed.

Make a Project Folder

Make a folder for your project. Give it a name you can easily recognise, and ensure you know where it is located.

Create the File

NewFile.png

Right click in the file and click on new. Click on text document, and then completely delete the name, alongside file extension. If you cannot see the file extension, make sure "Show File Extensions" is checked in the view menu. Rename the file to "hello-world.asm"

The Bootloader

When you boot your computer, several things happen. In order to boot your OS, no matter what it is, the BIOS must first load the bootloader. This is a file made in assembly that is exactly 512 bytes in size. In order to identify the bootloader, the BIOS looks for two things. First of all, the bootloader will always start at the memory address 0x7C00. And secondly, it references the address 0x0AA55. We will start off by referencing these.

Creating the Bootloader

code1.png

First, we will need to specify two things, at the start of the program. We will need to specify the start address of the program and the type of program. For our purposes, we will start at the address 0x7C00, and we will use a 16 -bit program.

To do this, we will need to type:

org 0x7C00
bits 16

The "org" instruction tells the computer the memory address the program starts at. It is 0x7C00 as that is the specific memory address the BIOS searches for, as stated previously.

"bits 16" tells the computer to run in 16-bit mode. This means that we will not have access to any features provided in 32 or 64 bit mode, however it also means that it is easier to program.

At the end of the program, we need to limit the size of the program, and give it a special address that the BIOS will look for when identifying the program. To do this, type at the end of the program:

times 510 - ($-$$) db 0
dw 0x0AA55

The times process will fill any empty space up to 512 bytes with a zero. This means that the file will be exactly 512 bytes in size, which is the required size of a bootloader. While this may sound large, remember that a standard image taken on a phone is approximately 16 kilobytes (16,000 bytes) in size.

Booting the Bootloader

code2.png
qemu1.png

Currently, we have no way to boot the bootloader. Lets change that, shall we?

First of all, we need to do something. For now, we will simply halt the process when it loads. To do this, type (before the last 2 lines):

main:
	hlt
	jmp main

Here, the "main:" is declaring a process. Whenever this process is jumped to with the "jmp" instruction, the code inside it will run. Inside this, we halt the program with the "hlt" instruction. This stops all processes on the computer. However, there is a chance the program will break out of this. To counter this, we simply use the "jmp" command to jump back to the start of the main process.

In order to run our program, we must first use NASM to compile of it, and then run it using QEMU. To make this faster, make a new file in the same folder, and name it "run.bat". Inside this file, we will type:

nasm -f bin hello-world.asm -o hello-world.bin
powershell.exe
pause

Upon saving and running the program, we see that a new file, called "hello-world.bin" has been created. We can run this file in QEMU. After the file has been compiled, the batch file will open Windows Powershell. Now, we can type:

qemu-system-i386 -hda hello-world.bin

This will open a new window in QEMU. It should boot from hard disk, and then do nothing. This means that our code so far has worked.

Clearing the Screen

code3.png
qemu2.png

When we load the bootloader, we do not want to see the BIOS output. In order to get rid of it, we must clear the screen. To do this, we will type this above the main process:

cls:
	pusha
	mov al, 03h
	mov ah, 00h
	int 10h
	popa
	ret

This process, called "cls", will clear the screen when called. This is through the use of the system interrupt 0x10. More specifically, interrupt 0x10 AL=0x03. Interrupt 0x10 is the collection of interrupts used for graphics. Setting the AL register to 0x03 through the use of the "mov" instruction, the operation performed by the interrupt will end up clearing the screen. The use of the "pusha" instruction saves the value of all the registers, as they are overwritten in this process. At the end of the process, before returning with the "ret" instruction, we must call the "popa" instruction to restore the data saved with the "pusha" instruction.

Now, to clear the screen, we need to call the cls process from our main process. To do this: we simply need to add to the main process:

call cls

Now, when we execute the run.bat file and run the program through powershell, the screen will be cleared when the file boots.

Setting a String to Print

code4.png

Now, we need to define a few things.

Underneath the first two lines, type:

%define ENDL 0x0D, 0x0A
%define ENDS 0x00

These two lines will allow us to use ENDL or ENDS instead of their values. This means that, when creating a string to be printed, it is easier for us to understand what is going on.

Now, we must create a string. Underneath the main process, type:

string: db "Hello World!", ENDL, ENDS

Here, we have created a string. We have used the "db" or "define byte" process in order to set the string to "Hello World!" We then add a new line, with the ENDL command we defined, and then we end the string with ENDS.

At the moment, this does not change what happens when the program is ran, however, in the next step, we will go over printing the string.

The Print Command

code5.png

Here's where things start to get complicated. We now need to write text to the screen. There is no interrupt in 16-bit architecture we can use to immediately print a string, which we can do in higher level programming languages, such as Python's print() command or the C++ std::cout function. Instead, we need to program our own print function. To start, we will make a process called "printstr". This process will go above the cls process.

printstr:
	push si
	push ax

This is a fairly basic process, as all it does is store the values of the si and ax register. We will pop these and restore their data later. Now, we must loop through each character and display them until the whole string has been printed. To do this, we will make a new process underneath the printstr process.

.loop:
	lodsb
	or al, al
	jz .done

	mov ah, 0Eh
	mov bh, 0
	int 10h

jmp .loop

here, the "lodsb" instruction will load the byte stored in the "si" register, at the current memory address. After this, the "or" instruction will work out if the length of the string has been reached. The "jz" instruction will then jump to the .done process if the line above is equal to zero (if the end of the string has been reached). It will then set the ah register to 0x0E, which will move the cursor to the right when the interrupt 10h is called. We will then set the bh register to 0. the bh register represents the "Page Number" of the display. If this changes, the output will be different. The bl register can be used to set the colour of the text. Then, we call the interrupt 10h to display the text. Finally, we need to add the .done process.

.done:
	pop ax
	pop si
	
	ret

This process simply restores the values of the ax and si register to what they were when the printstr process was called, and then returns to the main loop.

Finishing Up

code6.png
qemu3.png

Now, we have almost reached the end. But, we aren't done yet. Now, we must move the string variable into the si register, and then call the printstr process. Inside the main process, type:

mov si, string
call printstr

However, if we were to run this now we may encounter some weird bugs, as the program will run the printstr process immediately. To prevent this, we need to add a new process above the printstr process that jumps straight to the main process. It would also now be a good idea to remove the hlt and jmp instruction from the main process, to prevent any looping.

start:
	jmp main

Now, we can compile and run the program.

Done!

Congratulations! You now know the basics of x86 assembly, and have made your very own bootloader and print function! I hope you enjoyed my instructable. I will be uploading more programming-related guides in the future!

Challenge

Very good! You've now reached the end of this instructable. Now, maybe try a challenge! When you complete one, click "I made this!" and upload an image of the challenge, and give a description on how you did it. This will allow other people to learn even more!

Challenge #1 Text Editor

- Change the string printed to the screen

Challenge #2 MORE! (Intermediate)
- Add another string, printed on the next line

Challenge #3 Colours (Hard)

- Change the background Colour

- Change the foreground Colour