I'm trying to create a small operating system for x86 machines and started writing the code for a fairly minimal bootloader. The bootloader I created is quite simple, it loads a small second bootloader from the sector located directly after the master boot record and jumps to that code. The bootloader code in the master boot record seems to run fine, the problem occurs when it tries to jump to the second stage bootloader. This second stage bootloader is supposed to output a letter indicating success (the letter S) so I can tell the code is being executed. The problem is nothing ever appears on the screen, so I suspect the second stage bootloader was never executed. The code I used was the following:
Bootloader in the master boot record:
[BITS 16] ; 16 bit mode
[ORG 0x7C00] ; Boot loader start address
Boot:
; Initial, dl contains drive number
; Set data segment to code segment
mov ax, cs
mov ds, ax
mov es, ax
; Set the stack segment to 0xA000
add ax, 0xA000
mov ss, ax
mov sp, 0x00
; Reset the drive, dl contains drive number
mov ah, 0x00
int 0x13
; Read from drive, dl contains drive number
; Set up output location to 0x7E00: 0x00
mov ax, 0x7E00
mov es, ax ; Load to 0x7E00 : 0x00
mov bx, 0x00
ReadDrive:
mov ah, 0x02
mov al, 0x01 ; Read 1 sector
mov ch, 0x00 ; Read on cylinder 0
mov cl, 0x02 ; Read sector 2
mov dh, 0x00 ; Head number 0
int 0x13
jnc Success
; Print error (character F)
mov al, 0x46
call PrintChar
jmp ReadDrive ; Retry
PrintChar: ; Prints a single character
pusha
mov ah, 0x09
mov bh, 0x00
mov bl, 0x0F
mov cx, 0x01
int 0x10
popa
ret
Success:
jmp 0x7E00:0x00 ; Jump to 2nd stage bootloader
TIMES 510 - ($ - $$) db 0
DW 0xAA55 ; Boot signature
The code for the second stage bootloader:
[BITS 16]
[ORG 0x7E00]
Boot2:
; Prints the character S to the screen
mov al, 0x53
mov ah, 0x09
mov bh, 0x00
mov bl, 0x0F
mov cx, 0x01
int 0x10
jmp $ ; Loop forever
TIMES 512 - ($ - $$) db 0 ; Fill rest of block
This code was compiled and written to a drive using the following code:
nasm boot.asm -o boot.bin -f bin
nasm boot2.asm -o boot2.bin -f bin
dd if=boot.bin of=/dev/sd3 bs=512
dd if=boot2.bin of=/dev/sd3 bs=512 seek=1
The device this code was written to was a 16GB USB drive. The computer I used to boot this code supports booting from USB and boots them like any other hard drive. What is the reason the code does not seem to execute?
Second-stage boot loaders, such as GNU GRUB, rEFInd, BOOTMGR, Syslinux, NTLDR or iBoot, are not themselves operating systems, but are able to load an operating system properly and transfer execution to it; the operating system subsequently initializes itself and may load extra device drivers.
The second stage boot loader reads a configuration file (/boot/grub/grub. conf when using the grub boot loader) to determine what kernel should be started. The kernel is located in /boot and loaded into memory.
U-Boot is both a first-stage and second-stage bootloader. It is loaded by the system's ROM (e.g. onchip ROM of the ARM CPU) from a supported boot device, such as an SD card, SATA drive, NOR flash (e.g. using SPI or I²C), or NAND flash.
The primary bootloader is used for application reprogramming and the secondary bootloader is used for bootloader reprogramming. Both the codes are stored in the program memory of the microcontroller.
Two-Stage Bootloader. A two-stage bootloader actually consists of two bootloaders after each other. The first being small with the sole purpose of loading the second one. The second one can then contain all the code needed for loading the kernel.
The next decision is to choose a mode. Once the individual components in the motherboard start working together and give us a “computer system”, the second stage of the bootloader starts. In this 2nd stage of the booting process, a decision needs to be made by the user of the system.
In the first-stage bootloader, we must do the following: Setup the memory segments and stack used by the bootloader code Display a string saying “Loading OS…” Find the second-stage boot loader in the FAT directory Read the second-stage boot loader image into memory at 1000:0000
The bootloader is usually composed of three programs: 1 A boot sector program directly loaded by the BIOS at boot time 2 A second stage program loaded by the boot sector program to complete the booting process 3 A bootloader installer to install the bootloader and the second stage program in the boot disk.
There seem to be a number of issues in your code. I'll try to identify some of them. Some useful reference material can be found in some answers I have written for Stackoveflow.
You do set up a stack but it potentially overlaps video memory. Although this likely isn't related to your problems, it is a potential issue. With this code:
add ax, 0xA000
mov ss, ax
mov sp, 0x00
You set SS =0xa000, and SP =0x0000 . This sets up the stack, but unfortunately the first value pushed on the stack will be at 0xa000:(0x0000-2)= 0xa000:0xfffe . 0xa000:0xfffe happens to possibly fall within video memory. Maybe you intended to do ss=0x9000 so first value on stack would be at 0x9000:0xfffe . There is a snag there too. The Extended Bios Data Area (EBDA) can be in that region. Some BIOSes incorrectly return the wrong size for this area. In most cases it is 0k to 4k in size just below physical address 0xa0000. If you account for most worst case scenarios I'd go with a stack just below that.
add ax, 0x9000
mov ss, ax
mov sp, 0xF000 ; Bottom of stack at 0x9000:0xF000
There are 2 problems here. In your question you suggest you are trying to read the second stage into the area just above the bootloader. That would be at physical address 0x7e00. Your code does this:
; Read from drive, dl contains drive number
; Set up output location to 0x7E00: 0x00
mov ax, 0x7E00
mov es, ax ; Load to 0x7E00 : 0x00
mov bx, 0x00
16-bit Segment:Offset pairs use this calculation to map to a physical memory address: (segment<<4)+offset (<<4 is the same as multiplying by 16). That means that 0x7E00:0x00 is physical memory address (0x7E00<<4)+0=0x7e000 . That clearly is wrong. I believe what you intended was this:
mov ax, 0x07E0
mov es, ax ; Load to 0x07E0:0x00
mov bx, 0x00
0x07E0:0x00 is physical memory address (0x07E0<<4)+0=0x7e00 . That is the area just above the bootloader loaded into memory at physical address 0x7c00. A similar issue arises when you the FAR JMP to the second stage with this code:
jmp 0x7E00:0x00 ; Jump to 2nd stage bootloader
Should be:
jmp 0x07E0:0x00 ; Jump to 2nd stage bootloader
If you make the suggested change (jmp 0x07E0:0x00
) mentioned previously then the FAR JMP will change CS:IP to CS =0x07E0(segment), IP= 0x0000(offset) and continue execution there. You need your ORG directive to match the offset (IP) that you jump to from your first stage. Since the offset (IP) is 0x0000 your ORG directive should match:
[ORG 0x0000]
You need to also make sure that when your second stage starts loading that DS is also set up to match. One way of achieving this is to explicitly copy the Code Segment CS to the Data Segment DS . That can be done with code at the top of the second stage like this:
mov ax, cs
mov ds, ax
Without a properly set up data segment DS all the references to the variables will use the wrong segment and likely won't point to where they really are in memory. Your code doesn't have variables at the moment so you don't notice the issue.
In my General Bootloader Tips mentioned in this answer's prolog, tip #1 is very important:
- When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts. You can only be guaranteed that your bootloader will be loaded and run from physical address 0x00007c00 and that the boot drive number is loaded into the DL register.
In your code your bootloader has this:
[BITS 16] ; 16 bit mode
[ORG 0x7C00] ; Boot loader start address
Boot:
; Initial, dl contains drive number
; Set data segment to code segment
mov ax, cs
mov ds, ax
mov es, ax
[ORG 0x7C00]
is fine, but there is an assumption being made that the CS segment is set to 0x0000 when it reached our bootloader. We then set DS=CS. Conventional wisdom for a naive bootloader is that the BIOS jumps to 0x0000:0x7c00 (CS:IP). ORG should match the offset (in this case IP). The problem is that in reality the BIOS jumps to physical address 0x00007c00 but it can do it with a variety of CS:IP pairs.
The BIOS could have FAR JMP'ed (or equivalent) to our code with jmp 0x07c0:0x0000
, and some emulators and real hardware do it this way. 0x07c0:0x0000 is a physical address of (0x07c0<<4)+0=0x7c00 . This is perfectly fine, but notice that IP = 0x0000. We've set [ORG 0x7c00]
. That would be a mismatch! How do we get around this if we don't actually know what CS:IP pair the BIOS calls us with? Simple - don't copy CS to DS in the first stage of a bootloader. Since we need an offset of 0x7c00, DS needs to be 0x0000 to work. We should explicitly place 0x0000 in our Data Segment (DS). The code could look like this:
[BITS 16] ; 16 bit mode
[ORG 0x7C00] ; Boot loader start address
Boot:
; Initial, dl contains drive number
; Set data segment to code segment
xor ax, ax ; AX=0
mov ds, ax ; DS=0
mov es, ax ; ES=0
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With