Note: For some reason CodeCall hates me and shrinks every image I attach. Click on the thumbnail to see the original image.
1. Get Bochs
Bochs is a full x86-32 system simulator, complete with a BIOS, screen, disks, APM, and so on. First we need to get Bochs.
Windows: Download Bochs x86 PC emulator from SourceForge.net
Linux (Debian): At the command prompt, type sudo apt-get install bochs, and once that's done type sudo apt-get install bochs-x. This is the GUI libary that'll let you see the virtual screen.
Linux (Other): RPM Package
You may also be able to get it from a package installer or something. I only use Ubuntu, so I don't know how you'd do it in Red Hat or other distros.
2. Create a folder for this.
If I have to explain how to do this...you shouldn't be here. Trust me, we're going to end up with a number of files, and finding them all in a huge directory is not going to be fun.
3. Create a virtual disk.
Bochs provides a utility to create disks that you'll need to run to create your virtual hard drive. We're going to go with something really small because we don't need much right now; we can always make a bigger one later. Linux people need to chdir to their project directory in the command prompt, then type bximage. Windows people: Start > Programs > Bochs > Disk Image Creator Utility (or whatever it's called). You should get something like this:
Pick the .16 option for now.
If you remember our program from last time, we're going to need to put it onto our newly-created fake disk. Compile it (remember to use the -fbin option!) and blast it onto the image with the utility I wrote. (Source code is attached, compile it on your system.)
NOTE: The source code I posted for the print() function last time was defective. I've since fixed it, so you should go back and take a look at the changes. If you're too lazy to do that, I've attached it as well. Note to Windows people: Use something like ConTEXT Programmer's Editor (freeware) to view it. Line ending control characters are different in Linux and Windows, so Notepad will put everything on one line. More advanced text editors probably won't.
Almost done. We need to set up Bochs to find the disk image and use our program. Start it up by typing "bochs" on the terminal; Windows people have it on their Programs list. You'll get a main menu with seven options, like this:
We want #3, "Edit Options." We're going to need to save them later. Once you get into the editing menu, you're going to see a list of more options to mess with. We're not going to do anything fancy, so we're just going to choose "Disk Options," #10 on my version. From there go to Floppy Disk 0, and enter the file path to the disk image we created earlier. If you're in the same directory as the image, just type the name. (Told you, didn't I?)
Next it'll ask you for the disk size; put 160K, and when it asks if the disk is inserted or not, type "inserted" without quotes.
That's it for that. Now we need to save our options somewhere, so we don't have to keep changing them every time we want to test our program. Get back to the main menu, and choose "Save options to." Save the file in the same directory as you're in right now, otherwise Bochs will barf on you because it won't find the disk image.
Run Your Creation
First things first: Your code as it stands won't boot. That's right, it won't work. Why? To guard against corrupt disks, when the BIOS loads the first sector of the boot code it checks to see that the last two bytes of the sector are 55h AAh. We don't have that yet. What you need to do is stuff the remaining 512 bytes in your code with nulls (or whatever garbage you like) until you get to byte 510, and then write 55h AAh. We can do this quickly and safely with this:
times 510-($-$$) db 0 db 55h, 0aahGood thing about this is that if we write more than 510 bytes of code, NASM will barf once it hits that and tell us that we wrote too much to fit. If we want to write more than that, we're going to have to load the rest of our code somewhere else and jump to it.
Once we've got that fixed, let's run it!
Linux users, navigate to your directory and type bochs -qf <your config file name here>. Windows people need to run Bochs and load the file from the main menu.
You should get a screen that looks like a normal computer booting up, followed by "Hello, CodeCall!" in rather hideous psychedelic colors, like this:
I just put in random attributes; you might want to color-coordinate a bit so you don't get unsightly ** like that. (By the way, the screen might flicker a bit. This is normal.)
Making It Go Faster
If you should decide to make a really long string, you'll notice that it takes a while for the BIOS to write all the characters to the screen. This'll be even more obvious when we get to graphics. No one likes this kind of lag. Fortunately, there's a way around it: writing directly to video memory.
What? We can write directly to memory?! Yes, we can. Remember what I said earlier? We're not running on top of an operating system...we are the operating system. As such, we can do whatever we want, which includes writing to whatever address we want. Ish. Right now we're still in 8086 mode, so we're restricted to addresses 0x00000-0xFFFFF. Once I show you how to break out of 8086 mode we'll be able to write to every address in existing memory.
How does writing to memory write to the screen? We take advantage of memory-mapping. The VGA video controller--the thing that's drawing on the screen--monitors a small range of our memory, and uses that as its input data for whatever it's doing. It'd be stupid to do everything with one byte, so we map just enough memory so that we can write to every part of the screen in the current video mode. For example:
Let's say video mode X has a resolution of 640x480 pixels and 16 colors per pixel. 16 colors can be represented by four bits, so we can use one byte to represent two pixels. We have 640*480 = 307200 pixels, divided by two pixels per byte --> exactly 150K of memory. A modern screen at 1024x768 with 32-bit color gets you: 1024 * 768 * 4 bytes per pixel = 3 MB of memory. Not too bad.
But back to writing to memory: Unfortunately, there isn't any one single base address to write to for graphics or text modes, but luckily there aren't too many. Three, to be exact: 0xA0000, 0xB0000, and 0xB8000. The segmented addresses we'll be using in 8086 mode are A000:0000, B000:0000, and B800:0000 respectively. Each VGA video mode--VGA, mind you--uses one of these addresses as its base address to write to. I say VGA because we're going to switch to SVGA and XGA later. VGA is an old controller, and standard versions can't do anything beyond 640x480. SVGA and XGA allow us much larger resolutions and color depths, up to 1024x768.
I'm digressing again...
The general rule of thumb is that VGA graphics modes use A000:0000, two-color text modes use B000:0000, and color text and low-color graphics modes use B800:0000. Here's a small table of some sample addresses:
MODE BASE ADDRESS 00-06h B800:0000 07h B000:0000 ...reserved... 0Dh-13h A000:0000(Modes above 13h are kinda non-standard things that won't always work everywhere. I've listed the modes I know to be standard.)
Another great thing about writing to video memory: you don't have to worry about wraparound with rows and columns. Just keep writing to memory and it'll automatically go to the next line. Let's modify our print() function to use this direct-to-memory method. For now we're going to hard-code it to use mode 3 settings. If we look at the table, we see that video mode 3 uses B800:0000 as the base address. So:
print: push bp ; create the usual stack frame mov bp, sp ; save segment registers and other key ones. we have to save these by ; standard C convention. push ds push es push si push di mov ax, 0b800h ; load video segment into ES mov es, ax ; load data segment address into DS. the BIOS assumes everything is in one ; segment, so all segment registers start out the same; therefore, our ; data, code, and stack segments are all the same. we only set DS here ; because it may have changed before this function was called. mov ax, cs mov ds, ax mov si, [bp + 4] ; load string offset into SI xor di, di ; ES:DI = A000:0000 now .print_loop: mov ax, [ds:si] ; load both character and attribute at once cmp al, 00h ; check for null character je .done ; hit end of string, break out of loop ; not a null character, write character and attribute to video memory mov [es:di], ax ; increment pointers inc si inc di jmp .print_loop .done: ; restore registers pop di pop si pop es pop ds pop bp ret 2 ; pop off 2 bytes of arguments on return
You'll get something like this:
Er...why is "Hello World!" all the way up at the top? Because we wrote to the beginning of video memory, which corresponds to the top left corner of the screen. When we're writing to memory, we don't care where the cursor is because it doesn't matter. We can go wherever we want.
Alright, I think that's enough for today. Next time we're going to do something a bit more fun...
- Disk-file burning source code
- Source code for printing to the console using the BIOS
- Source code for printing to the console using direct-to-memory writing. (DTM)
Terribly-formatted table of video modes
VGA on Wikipedia
Edited by dargueta, 21 August 2010 - 03:41 PM.