They say that liberty is not secured at sword’s point. Maybe, but you can liberate yourself from mediocre software conventions through bare metal. In my previous post, we began tumbling down the rabbit hole, always unable to have a clear understanding of our system because of its overwhelming complexity.
In this post, we hit bare metal1 on the PC platform. This hardware has a lot of weird corners of its own, but unlike the software side, there’s nothing you can do to change the situation, so you can be stoical about it. From that point on, we can begin amassing real, solid knowledge about computing.
The binaries we’re about to assemble can theoretically run on any PC2 with the “legacy BIOS3” option enabled, but it’s a lot more convenient to use an emulator for two reasons:
For these reasons, you want to begin your development on an emulator4 which is a convenient and predictable platform. The best one out there is QEMU5.
QEMU has an executable for each architecture it targets and that executable has
a plethora of emulated machines and options to choose from. The executable
we’re interested in is
qemu-system-i386 and the default machine it emulates
is called “pc”6, an alias to the “i440fx + PIIX” machine. That’s the “old
school” PC, with old school floppy drive and IDE controllers. The more modern
PC platform, with Intel’s “ICH” family of chipsets is called “q35”.
In our adventure, we’ll begin by targeting the BIOS, so the machine we target doesn’t matter7. Soon enough, however, we’ll grow out of the BIOS and begin poking the hardware directly. At this point, the choice of machine will matter.
I suggest8 that we use the default machine because old school hardware is easier to interface with. The downside of this is that your drivers will only run on old PCs, but this is something you can improve later.
Alright, eager to begin? Yeah? Too bad, you’ll have to wait further as I babble some more, this time about the BIOS booting sequence.
When powering up, the BIOS selects a drive to boot from and reads its first
sector, which on the PC platform is always 512 bytes. It verifies that this
sector is actually a boot sector by checking the last two bytes of the sector
for a magic number, which is
0xaa. It then copies the sector at
0x7c00 and then jumps to that address.
Therefore, if you want to write code that will run on a bare metal PC, what you have to do is to assemble a 512 bytes file and place it on the first sector of your boot media. When running in an emulator, the file is the media, so it’s easy.
An important aspect of the PC booting sequence is that it always boots up in what we call the “real mode”, that is, in a mode that is 100% compatible with the original 8086. This means 16-bit registers and instructions. It’s also a requirement to be in this mode to call BIOS functions. You remember when you heard about the legendary backward compatibility of the Intel x86 family? That’s what we talk about and it’s going to make you swear.
Enough babbling, let’s flex our fingers and get going.
Code in this article is also available in a tarball.
We’re going to print “Hello World!” on a bare metal PC using BIOS functions through a boot sector we’ll assemble using NASM. The listing is so simple that I’m just going to give it straight to you and then explain it:
BITS 16 org 0x7c00 mov ax, 0 mov es, ax mov ah, 0x13 ; Function: Write string mov al, 0 ; write mode: don't update cursor mov bh, 0 ; page number mov bl, 0xf ; color: white mov cx, 12 ; Number of characters to write mov dh, 0 ; Row mov dl, 0 ; Column mov bp, msg ; Pointer to string int 0x10 loop: jmp loop msg: db 'Hello World!' times 510 -( $ - $$ ) db 0 db 0x55, 0xaa
If you save this to
hello.asm, you can compile this with
nasm -o hello.img
hello.asm and then run it under QEMU with
hello.img9. You’ll see your “Hello World!” printed at the top left of the
corner, mingled with BIOS boot messages because we haven’t cleared the screen.
I’ll briefly explain what’s going on here. Full explanations are out of the scope of this article, but you can get those answers from good i386 assembler tutorials and OSDev wiki. It’s ok if you don’t because as we go along, I’m going to explain things in a “need to know” basis.
BITS 16 line tells NASM that we want to compile code in real mode.
This code below runs not only on a i386, but on any CPU of the x86 family,
including the original 8086.
org 0x7c00 directive tells NASM that
this code will run at offset
0x7c00 in memory. Without this, the
reference below will be wrong10 because NASM will think that
somewhere near address zero.
The next two lines go together and have the effect of setting the
ES is a special “segment” register allowing the original 8086 to
address more than 64 kilobytes of memory (the maximum 16-bit address). The idea
is that “segment registers” such as
ES11 moves the address window by 16
times its value. For example, if
ES is 1, then calling
int10h below with
BP=0 will effectively reference absolute address
0x10. A bit confusing? I
told you you’d swear. We won’t be doing anything fancy with segments in 16-bit
mode, no need to think too much of it.
Anyways, for the
msg reference below to work, we need
ES to be zero12.
There are no form of
mov instruction allowing an immediate13 source
argument with a "segment register" destination argument, so we need to set
another register to zero,
AX in this case, and then set
ES with the value
The following 8 lines are arguments setup for the BIOS function
INT10h. We’ll talk about interrupts in more details later, but for
now, what you should know is that BIOS functions are called through the
instruction which triggers a software interrupt of the specified index.
Arguments to BIOS functions are passed through specific registers. The most
important argument is
AH14 because it specifies the subfunction to call
and thus the nature of the call. In this instance, “INT10h AH=0x13” means, as
you can see in the documentation, “Write string”. Most of the other arguments
are self-explanatory. As you can see, the
msg label is used as a source
mov. When assembling, NASM will calculate the offset where
ends up in the binary and replace all references to this label by this offset.
mov instruction is encoded,
msg is the equivalent of a constant
int 0x10 line triggers the interrupt handler
0x10, which lives in the
BIOS. This is the equivalent of a
call and will return once it’s finished
doing its thing.
The next line is an infinite loop because we don’t have anything to do after after having written the string. It defines a new label and jumps to it, which means it’s stuck there forever.
The next two lines are data and are never executed.
db is a special NASM
directive to write literal data into the executable and this directive supports
string literals. No need for a terminating null because we specify our string
length in the
We’re finished with our boot sector, but we still need to add the magic
0xaa at the end of the sector. To do this, we use special NASM voodoo to
fill the exact number of zeroes we need to get to byte 510, and then spit
0xaa with a regular
That’s it, the whole machine is yours now! If you write this to a USB key with
“dd if=hello.img of=/dev/sdX”, it should make that USB key
bootable on most PCs. Feels nice right? You still have a long way to go, but at
least you have a foot in the door.
The whole world is open to us now, we could go anywhere from here, but paths are mostly dictated by constraints. 512 bytes is really tight, real mode is really ugly, Forth is really intriguing. Will we build a bootloader, go in protected mode, hack a Forth right away? Stay tuned for the answer, same bat-hour, same bat-channel!
Next: One sector to rule them all
To the initiated, the contents of this article will look similar to what the OSDev wiki offers. It’s true, and it’s going to be mostly true for the next few articles15. This wiki is an awesome resource for PC development, I can't recommend it enough. However, they insist, when you read them, that you grow into an OS developer. They want you to sweat a little bit rather than spoon feed everything to you. This series of articles I'm writing doesn't aim to make you an operating system developer16, but to broaden your understanding of computing.
I thus believe that the path I’m offering you is quicker, easier, more seductive, even though it leads you to the Dark Side. But it’s worth it, right?
On modern hardware, what we call “bare metal” is far from it because that “metal” is wrapped by layers upon layers of very complicated firmware that make you believe you’re actually flipping hardware switches. That illusion will suffice for now and the knowledge you’re gathering is real, not fake. Later, you can become a vintage hardware enthusiast and flip switches for real. ↩
Why the PC? Because it’s ubiquitous. Its innards are ugly and full of pitfalls, but resources about it are plentiful. Moreover, it’s the only hardware that Dusk OS supports at the moment. In other story arcs, we’ll explore other architectures, some of them beautiful. ↩
The BIOS is a piece of software living in a ROM chip in the computer. Its entry points and behavior is (supposed to be) standard across all PCs and serve as a generic way to access hardware. ↩
If you’re like my former self, you treat emulators like some piece of magical black box. In reality, the way they fundamentally work is simple and straightforward. One day, we’ll explore this, but for now, we can continue to treat it as a black box. ↩
It is packaged on the vast majority of Linux distros, but there’s sometimes one packages per target architecture. You’re looking for the “x86” target. For example, on Debian, the package to install is “qemu-system-x86”. ↩
Run “qemu-system-i386 -machine help” for a list. ↩
… much. Consistency across BIOS implementations in the PC world is far from perfect. ↩
Did I say “suggest”? I meant “force you to” because that’s the only machine I’m going to cover for now. ↩
"hda” being a reference to the first IDE drive of the system, so it’s as if your machine was booting up from the HDD. ↩
The “loop” one will be fine though because all jumps use relative adressing. More on this later. ↩
There are many such registers that serve in different contexts. ↩
Another way to proceed would be to keep “org” to zero and then set “ES” to 0x7c0. ↩
"constant” in layman terms. ↩
Each 8086 “general” register, AX, BX, CX, DX, are divided in two “H” (high) and “L” (low) 8-bit sub-registers. ↩
But not for the whole story arc, as we’ll soon dive into the worderful world of Forth, which OSdev doesn’t cover. ↩
Although I’d be happy if it did! ↩