Print Test

Test that you can directly program your CPU!

This page is an in-depth dive into the code you need for printing characters to the screen. If you need to know how to run/boot up code like this, see Background (covers the manual approach) or Toolchain (for a more streamlined workflow).

Here we'll go over the machine code for printing a character to the screen using BIOS interrupts. After this page, it should be easy to write code that prints stuff out to the screen, and at the end, you'll see the code for printing Hello World.

To print a character, you just need to:

If you have no idea what any of this means, don't worry, that's what this page is for.

At the end of the program, I also put the bytes \eb\fe, which are the same as JMP $, AKA "jump to the current position" or "loop forever," so this page will go over that as well.

The BIOS provides a suite of "interrupt services," which are snippets of code that get run whenever certain "interrupts" are invoked. You can manually invoke an interrupt via the INT instruction, which takes the interrupt address to invoke as an operand, (e.g. INT 0x10) and is encoded as \cd followed by the operand (e.g. \cdDle). They're analogous to function calls in other programming languages.

When the BIOS boots up, it registers a bunch of services at different interrupt addresses. These are put there so that whatever program is running can use them to easily interface with peripherals. One of the interrupt services is the Video Services Interrupt which is registered at interrupt address 0x10. It uses the value of ah to determine what "sub operation" to do (e.g. print a character if ah is 0x0E, draw some visual, etc.), and when printing a character, it uses al to get the ASCII value of the character to print.

The full program will look like this:

\b0A\b4Sho\cdDle\eb\fe
b0 41 b4 0e cd 10 eb fe
MOV al, 'A'
MOV ah, 0x0E
INT 0x10

JMP $

Walkthrough

Character → al

We'll use the MOV instruction, with an immediate value, to put the character we want to print into al, but the CPU and BIOS interrupts do not care about how data are placed into the registers, only what the data are when read, so whether you're using a MOV, AND, or even something spasticated, like CPUID to load a value into al, as long as the value you want goes there, you're good.

The MOV instruction, to move an immediate value—a value literally from your code—into the al register, is encoded as \b0, followed by the byte to store into that register. The ASCII character A has the value 0x41, so, for example:

MOV al, 'A' ; Assembles to 0xB0 0x41

This puts the ASCII value for the letter A into the al register, so when we call our print command, the letter A will get printed.

0x0Eah

Next, we need to do something similar with ah: We need to put the value 0x0E into it. The base opcode for moving an immediate value into ah is \b4, (similar to that for moving to al: \b0):

MOV ah, 0x0E ; Assembles to 0xB4 0x0E

Lastly, we need to call the video services interrupt. This needs to be done after the previous two operations, so that the video services interrupt sees the values in ah and al to know what to do and what to print.

The INT instruction (short for INTerrupt) has the base opcode \cd, and then the next byte is the interrupt to call, which is Dle (or 0x10 in hexadecimal):

INT 0x10 ; Assembles to 0xCD 0x10

And that is enough, on its own, to print a character, but I always add an infinite loop at the end so that the CPU doesn't start executing whatever nonsense got copied from my flash drive to there.

Once the processor reaches that \cd interrupt instruction and processes it by jumping to the interrupt location and running the code the BIOS put there, it finishes sending some data to the video driver and then returns from that interrupt, and goes back to processing the rest of your code. It will try and continue to the next instruction, whether or not you wrote one there (in which case it may start executing garbage).

I put an infinite loop at the end of my program. I've found that, in some cases, when my code bugs out or something goes wrong, I'll see the same program run in a very fast loop. For example, while trying to print Hello World!, there were a couple times where e.g. I'd forget the infinite loop at the end and the processor would just keep printing the text over and over again. It would instantly fill the screen and then start flashing. The bare minimum needed here for printing is these 6 bytes (comprising these three instructions), but in total this program will be 8 bytes with the jump at the end.

The base opcode for the (short) JMP instruction is always \eb in real mode. The next byte is the operand for the jump instruction, and is treated as a signed, 8-bit number representing the offset from the next instruction, (following the JMP instruction), to jump to.

The jump's offset operand is always relative to the byte following the jump instruction, so a jump with the operand zero, \eb Nul, doesn't loop forever, it continues from the next instruction. For example:

\b4A\b0Sho\ebNul\cdDle

The jump here has operand 0 (Nul), so when the jump gets executed, the processor goes to the next instruction, offset by 0, (which is: \cdDle), and then runs it.

In this case, the \ebNul is like a couple of NOP (\90) instructions.

Another example, looping forever, would be a JMP with the operand -2:

\b4A\b0Sho\eb\fe\cdDle

The operand of a JMP instruction is signed, so -2 is encoded as \fe (-1 is encoded as \ff and 0 is encoded as \00 AKA Nul).

In this code block, the \cd (interrupt) is never executed and so no character is printed (because once the processor reaches \eb\fe, it loops indefinitely).

So to complete the program, I just shove an \eb\fe at the end. That's all you need to go ahead and try booting.

On my computer, it takes a few seconds (how long it takes varies) to load, during which I see a flashing caret, then the letter A appears.

Printing Multiple Characters

Especially if you've already gone through Background, you may want to do something a little more spicy than just printing a character. Like printing multiple characters.

Printing multiple characters is pretty intuitive. After the first character, you've already put 0x0E into the AH register, so now all you need to do is change AL to the new character you want and then call \cdDle again to actually perform the print. In fact, if you want to print the same character multiple times in a row, you don't even need to change AL:

\b4Sho\b0H\cdDle\b0e\cdDle\b0l\cdDle\cdDle\b0o\cdDle\b0 \cdDle\b0W\cdDle\b0o\cdDle\b0r\cdDle\b0l\cdDle\b0d\cdDle\b0!\cdDle\eb\fe

At first glance of this code, you might think there's an l missing from "Hello," but the ASCII value of l is already in the AL register, so just calling the print interrupt \cdDle again suffices to print a second l.

If you tried this and it's not working, there are a couple potential reasons; read these two asides!

Some BIOSes overwrite certain parts of your code when they copy it from your boot drive into memory and then jump to it to start executing. It's very strange. Mine does it: It overwrites bytes 0x1C - 0x1F and byte 0x24 with the value Nul (the value 0). If you're seeing part of your text getting printed, or you're seeing "Hello World!" (or some part of it) spammed on the screen, it's possible that certain bytes in your program were just obliterated by your BIOS.

Conventionally, operating systems, bootloaders, and other "boot-level" programs (like ours!) would have a near—not short but near—jump as their first three bytes, and then after the first three bytes, there would be data, called the BIOS parameter block (not code that the CPU is supposed to execute). The near jump would jump over, to some address past that data, to some boot loading code, that would then get executed. This boot code would then read the data located in the BIOS parameter block before it.

Some BIOS implementations, for one reason or another, will try and fill in some of that data with certain values. They expect data to be in the BIOS parameter block, and (I guess for consistency or something?) would overwrite parts of that block with "correct" values for the system they're running on.

The BIOS parameter block is a fairly well-documented concept; you can probably find its structure and format with a quick search online. I never looked into it, so I don't have much to give in the way of it, but I can say that once I found which bytes were being overwritten for me, I just jumped over them (exclusively them, nothing more) and everything's been working fine since.

Copy-pastable code (or something similar) needs to be added here that you can use to check what bytes in memory your motherboard BIOS firmware is overwriting.

You might hear that the BIOS "could" overwrite AH or AL when you call \cdDle to print the character you put in AL. The official BIOS specification (which you can find here: https://bitsavers.org/pdf/ibm/pc/pc/6025008_PC_Technical_Reference_Aug81.pdf) does not permit this; it guarantees that the entire AX register (including the top 8 bits, AH, and the bottom 8 bits, AL), is saved and restored by the time the "WRITE_TTY" BIOS routine returns. BIOS is an old standard however, and the motherboard market has become extremely saturated since those days; word over the grapevine (online) is that it's very well possible that a modern motherboard—especially a cheap one that happens to support BIOS booting—doesn't support the '81 BIOS specification to the T. So your motherboard might end up clobbering (overwriting whatever you put, and not restoring) the AL or AH registers.

You can always check for yourself though by just running code and seeing what happens.

This page gave you a gentle walkthrough into printing characters (using BIOS interrupts), and went over what each "piece" of printing a character is actually for (i.e. \b0A vs cdSho). Now you can write prints yourself, or put printing within other pieces of code you write.

The next page, Print Hex, gives you some code for converting a byte into hexadecimal characters, and then printing it. You can use it to read things like the values in memory or in registers pretty easily.

Other Stuff

Here are a few examples of printing junk running:

\b4Sho\b0H\cdDle\b0i\cdDle\b0!\cdDle\eb\fe
Code corresponding to above image; prints Hi!.