My Bootloader Does Not Work

From OSDev Wiki
Jump to navigation Jump to search

Here follows a list of the (excessively made beginner) mistakes that occur when writing a bootsector. There are several tutorials out there that suggest really broken behaviour, and there are many errors typically coming out of ignorance.

If you came here wondering how to start at all, have a look at rolling your own or bootloaders in general.

GAS-specific issues will not be discussed here: it is the only common assembler that can not emit flat binaries, and as such requires the aid of a significantly more complicated linker. YASM supports GAS syntax without adding the huge problem surface it brings. Particularly beginners will want to switch until they have understood the problems they're having.

Draw a map

Or type it out. An x86 bootsector is loaded at 0x00007C00 in physical memory. There's a BIOS area at 0x00000000 to 0x000004FF, and there's an EBDA somewhere between 0x00080000 and 0x0009FFFF. There's a lot of memory again after 0x00100000 but you can't tell the BIOS to use it, you'll have to check for it, enable it, and real mode dislikes it in general, so you don't want to avoid going there as long as possible.

Plot in this map where you will put a stack (give it 0x1000 bytes), where you want to load any pieces from the disk like second stages, FAT tables and anything else that does not come preallocated within your bootsector. Look up how big everything is, and make sure nothing overlaps with anything else. You need this later, and

You will have to tell us what's in your map if you decide to ask a question later

You read something from disk and did not put it on the memory map

And more often than not, the fact that you're looking here is that you just loaded a sector from disk over your stack or your existing code.

90% of the errors happen within the first 10 lines of assembly

And more than often several. Fix them all.

In real mode, memory is always accessed as segment*16 + offset. There's always a segment involved, and there's always an offset involved in anything real-mode related. Now that you have your map, you'll have to find yourself pairs of segments and offsets that point to each of those addresses. There are three standard methods of going at it.

  • The segment is zero, making the offset the physical address. This limits you to the first 64k of memory, but that might also exactly be the memory you want.
  • The offset is zero, making the segment the physical address divided by 16. You're not allowed to round off. You also need to be careful with that you need to set segment registers all over the place.
  • The offset is the lowest 16 bits of the address, and the next four bits end up in the segment followed by three zeroes. Write it out on paper. It's more complicated, but it works for any address in the 640K you could think of.

There's no ORG statement

Your code involves a CS and an IP, the segment and offset. Your data is in the same block, using another segment register and offset. ORG tells the assembler at which offset, not the segment, you will be using it. If you picked segment=0 and offset=0x7C00, then ORG 0x7C00 is correct. If you pick segment=0x07C0 and offset=0 then ORG 0 is required. This might happen to be the default for many assemblers, but if you don't put it there we will assume it's wrong anyway.

You are assuming a wrong CS:IP value

Some BIOS start the bootloader with the CS 0x7C0 and IP 0. Others with CS 0 and IP 0x7C00. Both is the same memory location but different segment registers. Do a far JMP to set it to your preferred value. For example:

ORG 0x7C00
start: JMP 0:skip
skip: ... your code here ...

There's no BITS 16 statement

There are several execution modes. Don't let your assembler guess one.

There's no reference to SS or SP

All, but a select few of the registers are undefined. The stack is not where you put it on your map. Set it or things will break.

MOV AX, 0
MOV SS, AX
MOV SP, 0x7C00

This puts the stack directly in front of your bootsector. That means you also occupy 0x6C00-0x7BFF and can't use it for anything else after this.

The value of SP ends in an 0xF

You stole some code off the internet. Shame on you, and I hope you learnt not to use tutorials in the future. The stack should be a multiple of two in real mode (the number of bytes you PUSH and POP each time). Failure to do so makes your code slower - up to 25%!

The value of SP points to the beginning of the stack in memory

The stack grows downwards. SP should be the first byte after the stack's memory. If that would make it 0x10000, set it to zero, because in 16-bit numbers the -2 it will do before writing will automatically wrap it around back to 0xFFFE

The value of SP ends in an 0xE

SP is reduced by two for each push operation. Suddenly it writes two bytes at 0x..C and 0x..D wasting the bytes at 0x..E and 0x..F

There's a PUSH, CALL or INT before SS is being set

You just wrote over some memory SS:SP happened to point at. Now every BIOS call will fail because of this.

SP isn't set in the instruction immediately after SS

If an interrupt happens, it will use an old SP and a new SS. Setting SS prevents interrupts for the next instruction, so that has to be SP.

There's no reference to DS or ES

You can't use any data in your bootsector. Every piece of data is accessed by a segment and an offset, so you have to set the segment because it's not pointing anywhere yet. The assembler will deal with the offsets if you have the right ORG statement.

Several BIOS calls require you to supply a value in a segment register. Be sure to restore it again.

There's a save of the boot drive number before DS is set

To reiterate on the previous point, you cannot e.g. store the boot drive number (passed to your bootloader in the DL register) in a variable at some memory location before you've first correctly set up DS.

A segment register contains 9xxx or 8xxx

You're writing into the EBDA and you just broke the BIOS.

BIOS specific problems

Unfortunately, the BIOS is poorly standardized[how?] leading to various and sometimes poorly written implementations. Because of this BIOS specific situations can arise that will cause it not to load a bootloader. As an example some BIOSes may require a valid partition table to exist at byte address 0x1BE of the storage device. This is very common in the compatibility mode of UEFI machines, as the reference implementation attempts to parse the partition table before branching between the compatibility and UEFI modes. Furthermore, if you use older hardware for testing, the BIOS may not support INT 13h extensions - most notably LBA support.