My Bootloader Does Not Work
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.
GAS will not be discussed here as of yet. If you use it, you're a masochist for making things unnecessarily more complicated, and we'll happily suggest other assemblers on request.
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.
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 standardised 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 adress 0x1BE of the storage device. Furthermore, if you use older hardware for testing, the BIOS may not support int 13h extensions - most notably LBA support.