In this issue we're going to start getting into some really fun stuff--graphics. Because this involves writing directly to memory, and most operating systems won't let you do that*, we're going to need to emulate our code on a virtual machine.
*For some stupid reason, Windows allows this for raw binaries in COM (but not EXE) format.
We need two things:
1) A virtual processor. I use Bochs IA-32 Simulator, which you can download for free from their site. If it's lagging too much, you can try downloading it from their mirror site.
Note that this emulates 32-bit only, which is fine because I'm not teaching 64-bit ASM here. (The emulator runs on 64-bit processors, but it doesn't simulate one.)
2) A virtual disk. You can feel free to put your programs on bootable media like a flash drive or CD drive, but remember that you can screw up your system. We'll use image files, and mount them on our file system as if they were real disks. This is a built-in behavior on Linux (and I think UNIX) systems. Windows can't do this without special software, which you can get here (also free).
Making a Hello World Program
We've already made a "Hello World" program before, but this is a different kind. We now have no operating system whatsoever. No system calls, no protected memory, no libraries...we're completely on our own. Sounds scary, but that means that we call all the shots, and can do whatever we want--read from location 0, flash the BIOS, erase the hard drive...
Fire up your favorite text editor, and let's get going! Down here all we can rely on is the BIOS to do some of the dirty work for us. First thing we need to do is set the video mode. Your graphics controller has a bunch of different modes. For now we're going to start with the easy-but-ghetto VGA family, and then later get into fun SVGA-type stuff that modern OSes use.
Within the VGA set of modes, we have a number of sub-modes, each supporting a specific resolution and a number of colors. Typically the higher the resolution, the lower the number of colors supported. Each mode is either specifically for text, or graphics. (You can print text on the graphics modes, but it looks kinda weird.)
Here are a few modes:
(Note: for text modes, the resolution is the number of columns and rows of displayable letters.)
[U]Mode# Type Width/Height/Colors[/U] 00h Text 40x25x16 03h Text 80x25x16 13h Graph. 320x200x256 18h Graph. 640x480x16There are a lot more than these four, but I thought these were a good place to start. Mode 3 is the default video mode that the computer boots in. I would imagine that'd be assigned to zero, but whatever. Anyway, we can tell the BIOS to switch video modes by calling interrupt 0x10. (I covered interrupts earlier in this series.) Calling parameters:
AH: 0x00 (the subfunction #)
AL: Video mode to change to
So if we wanted to change to video mode 13h, we'd do:
mov ax, 0013h int 10hSimple, right? Now let's get to the interesting part: printing our "Hello World" message. We can print out a boring old white-on-black message, or we could take advantage of the 16 colors and make it interesting. I vote interesting.
We can change the foreground and background color of a character by changing its attributes. This is a one-byte number that contains the foreground color in the low 4 bits, and the background color in the high 4 bits. This is why text modes never have more than 16 colors in VGA. How do we know what colors those are? There are eight basic colors: black, red, green, yellow, blue, magenta, cyan, and white. Colors 0-7 correspond to dim shades, and 8-15 are brighter shades of the same colors. (Black turns to gray in the bright mode; white in dim mode is light gray, and pure white in bright mode.)
That's all well and good, but how do we change the character attributes? Again we use interrupt 0x10, but this time we use subfunction 9:
AL: Character to display
BH: Page number (just put 0 for now)
CX: Number of times to repeat
So to display a bright green 'A' on a dark red background, we do:
mov ax, 0941h mov bx, 001ah mov cx, 0001h int 10hBy now you're probably pissing yourself with excitement. Hold on, we're almost there.
Putting It Together
To print our "Hello World" message, we'll have one string with a character followed by its attribute byte, terminated by a null character. In NASM syntax we'd have:
hello_message: db "H",19h,"e",9ah,"l" ...etc... , 00h, 00hOur print function is going to accept a string, read a character into AL, then read the next byte (the attribute) into BL, display the character, and continue until it hits a null character. I strongly suggest you try writing it by yourself first, and then check back to see how I did it. We must do this in 16-bit code. The processor starts out in "real," i.e. virtual 8086 mode, which means it's 16-bit. Switching over to 32-bit mode is beyond the scope of this tutorial.
EDIT: The original code in the first version this tutorial didn't move the cursor over, and thus printed all the characters of the string in one location. It's fixed now.
print: push bp mov bp, sp sub sp, 4 ; make room for two temp vars ; get the current cursor position mov ah, 03h mov bh, 00h int 10h ; save row and column mov [bp - 4], dx ; load address of string into BX mov bx, [bp + 4] ; store address of string into our local variable ; as well. we have to do this because the BIOS ; might overwrite the value, and we need to keep ; it across calls. mov [bp - 2], bx .print_loop: mov bx, [bp - 2] ; load string pointer mov al, [bx] ; read character mov bl, [bx + 1] ; read attribute cmp al, 00h ; check for null je .done ; break if null ; prepare interrupt call to write character mov ah, 09h ; subfunction mov bh, 00h ; page 0 (ignore for now) mov cx, 0001h ; rep count int 10h ; call interrupt add word [bp - 2], 2 ; increment pointer ; move cursor over mov dx, [bp - 4] ; load the last position we saved inc dl ; increment column (DH=row, DL=col) mov [bp - 4], dx ; store the incremented result back mov ah, 02h ; int 10h, subfunction 2 mov bh, 00h ; page 0 (ignore for now) int 10h ; call interrupt jmp .print_loop ; on to next character .done: add sp, 4 ; clean up local vars pop bp ret 2 ; pop off 2 bytes of arguments on returnNow that we've gotten that out of the way, let's build a few things around it to finish it up:
use16 cpu 8086 org 7c00h ; CODE start: mov dx, hello_message push dx call print ; deliberately hang the system cli hlt <your print function here> ; DATA hello_message: db "H", 29h, "e", 6fh, "l", 0e7h, "l", 10h, db "o", 9ah, " ", 0a0h, "W", 87h, "o", 0d9h, db "r", 0eah, "l", 01h, "d", 0deh, "!", 0adh, db 00h, 00hA few things of note:
- use16: This tells the compiler that our default addressing mode is 16-bit.
- cpu 8086: This tells the compiler that we can only use instructions valid for the 8086.
- org 7c00h: This tells the compiler that our code is going to be loaded at address 7c00h. This is where the BIOS always loads the boot code.
We have to compile it now, but we've got a problem: The output file has to be a raw binary, because we have no operating system to interpret an EXE or ELF format. How do we do this? We can tell nasm to compile to raw binary using the -f option, which specifies what the output format is. For this we pass in bin as the argument. When I compiled it, I used this:
nasm -Wall -Werror -fbin -O0 -o hello.bin hello.asmAs a general rule, always use the -Wall –Werror switches. If the compiler is warning you about something, it's warning you for a good reason.
Wow...this tutorial has gotten way too long. I'll continue with part B later, where I'll show you how to put your program onto a virtual disk, and run your first operating system-less ASM program.
Next In This Series
Edited by dargueta, 18 November 2010 - 07:27 PM.