The primary problem is that you are reading from the wrong place on the disk. If you place your second stage starting at the second sector of the disk right after the boot sector, that is Cylinder/Head/Sector (CHS) = (0,0,2). The boot sector is (0,0,1). Sector numbers start at 1, while cylinder and head numbering start at 0.
Other potential issues are (many of which can be found in my General Bootloader tips):
Your code relies on the fact that CS is set to 0000h (since you are using an ORG of 7c00h). You set DS=ES=SS=CS. You shouldn't assume the state of any of the segment registers or the general purposes registers except for the drive number in DL. If you need a segment register like DS set to 0000h then set it to zero.
You write the drive number in DL to memory address driveNumber
BEFORE you set the DS segment. It is possible to write the boot drive to one segment and then read from the wrong segment later on. If you need to save DL to memory then do it after you set the DS segment. mov driveNumber, dl
has an implicit use of DS when referencing driveNumber
(ie: it is similar to mov [ds:driveNumber], dl
.
You don't actually set SP in your code. You only update SS. Who knows where SP points to! The combination of SS:SP determines the current stack pointer. You could set the stack to grow down beneath the bootloader by setting SS:SP to 0000h:7c00h. This wouldn't interfere with the loading of stage2 at 1000h:0000h.
You don't need to place CLI/STI around the segment register updates. The one place that has to be atomic is when updating SS:SP. If you write to SS the CPU will disable interrupts until after the following instruction. If you update SP right after SS then it can be seen as an atomic operation and there is no need for CLI/STI. This is true on almost every processor with the exception of some defective 8088s produced in the early 1980s. If there is a chance you will boot on such a system then consider putting CLI/STI around the code that updates SS:SP.
You have a js reset
after attempting a disk reset. I believe you meant to use jc
to check the Carry Flag (CF) not the sign flag. In general you don't usually have to check for the reset failing. Do a reset and then reissue the drive access command (ie: Disk Read) and capture any drive errors there. On real hardware you'd usually reattempt the operation 3 times before giving up and aborting.
It appears you enabled .286
instruction set so that this code compiles:
push 1000h ; pushing the memory address into stack
push 0h ; pushing the offset
retf
You used retf
to perform the equivalent of a FAR JMP, something that early versions of MASM had no supporting JMP syntax. Your code is correct, but you need a least a .186
directive because the Intel 8088/8086 processors didn't support PUSH imm8
or PUSH imm16
encodings. This was added in the 80186. If you wanted your code to run on an 8088/8086 you could have done it this way:
; Version that uses a FAR RET to do same as FAR JMP that works on 8086
mov ax, 1000h ; On 8086 push imm16 doesn't exist
push ax ; Push the code segment (1000h) to execute
xor ax, ax ; Zero AX
push ax ; Push the offset (0) of code to execute
retf ; MASM may not understand a FAR JMP, do RETF instead
Although that solution works it is rather lengthy encoding. You can manually emit a FAR JMP (opcode 0EAh) with this code:
; Early versions of MASM don't support FAR JMP syntax like 'jmp 1000h:0000h'
; Manually encode the FAR JMP instruction
db 0eah ; 0EAh is opcode for a FAR JMP
dw 0000h, 1000h ; 0000h = offset, 1000h segment of the FAR JMP
You can emit the 0aa55h
boot signature and pad the boot code to 512 bytes by placing all the code and data into the .code
segment and use ORG
to do the padding and placement of the boot signature.
To fix the problems identified above, your code could look like this:
.8086
.model tiny
.code
org 7c00h
main PROC
jmp short start
nop
start:
xor ax, ax
mov ds, ax ; DS=0
cli ; Only need STI/CLI around SS:SP change on buggy 8088
mov ss, ax ; SS:SP = 0000h:7c00h grow down from beneath bootloader
mov sp, 7c00h
sti
mov driveNumber, dl ; Storing booting drive number
jmp read ; Jump to reading (don't need reset first time)
reset:
mov ah, 0h ; Reset the drive before retrying operation
mov dl, driveNumber
int 13h
read:
mov ax, 1000h ; Reading sectors into memory address 0x1000:0
mov es, ax
xor bx, bx
mov ah, 02h
mov al, 01h ; Reading 1 sector
mov ch, 00h ; Form cylinder #0
mov cl, 02h ; Dtarting from sector #2
mov dh, 00h ; Using head #0
mov dl, driveNumber ; On boot drive
int 13h
jc reset
; Early versions of MASM don't support FAR JMP syntax like 'jmp 1000h:0000h'
; Manually encode the FAR JMP instruction
db 0eah ; 0EAh is opcode for a FAR JMP
dw 0000h, 1000h ; 0000h = offset, 1000h segment of the FAR JMP
; Error - end with HLT loop or you could use 'jmp $' as an infinite loop
error:
cli
endloop:
hlt
jmp endloop
main ENDP
; Boot sector data between code and boot signature.
; Don't put in data section as the linker will place that section after boot sig
driveNumber db ?
org 7c00h+510 ; Pad out boot sector up to the boot sig
dw 0aa55h ; Add boot signature
END main
Other Observations
Int 13h/AH=2 (read) and Int 13h/AH=0 (reset) only clobber the AX register (AH/AL). There is no need to set up all the parameters to do another read after a disk failure.
As previously noted earlier, retrying disk operations 3 times was common place on real hardware. You can use SI as a retry count for the disk operations, as SI isn't used by disk read and reset BIOS calls.
It is unnecessary to start with:
main: jmp short start
nop
start:
unless you are inserting a BIOS parameter block (BPB) for use as a Volume Boot Record (VBR). Having a BPB is a good idea on real hardware when booting on USB devices using Floppy Drive Emulation (FDD).
If updating the upper and lower 8-bit registers of a 16-bit register like this:
mov ah,02h
mov al,01h
You can combine them into a single instruction this way:
mov ax, 0201h
Implementing what is identified in the additional observations, the code could look like:
boot.asm:
DISK_RETRIES EQU 3
.8086
.model tiny
IFDEF WITH_BPB
include bpb.inc
ENDIF
.code
org 7c00h
main PROC
IFDEF WITH_BPB
jmp short start
nop
bpb bpb_s<>
ENDIF
start:
xor ax, ax
mov ds, ax ; DS=0
; cli ; Only need STI/CLI around SS:SP change on buggy 8088
mov ss, ax ; SS:SP = 0000h:7c00h
mov sp, 7c00h
; sti
mov ax, 1000h ; Reading sectors into memory address (ES:BX) 1000h:0000h
mov es, ax ; ES=1000h
xor bx, bx ; BX=0000h
mov cx, 0002 ; From cylinder
; Starting from sector
mov dh, 00h ; Using head
mov si, DISK_RETRIES+1 ; Retry count
jmp read ; Jump to reading (don't need reset first time)
reset:
dec si ; Decrement retry count
jz error ; If zero we reached the retry limit, goto error
mov ah, 0h ; If not, reset the drive before retrying operation
int 13h
read:
mov ax, 0201h ; BIOS disk read function
; Reading 1 sector
int 13h ; BIOS disk read call
; This call only clobbers AX
jc reset ; If error reset drive and try again
; Early versions of MASM don't support FAR JMP syntax like 'jmp 1000h:0000h'
; Manually encode the FAR JMP instruction
db 0eah ; 0EAh is opcode for a FAR JMP
dw 0000h, 1000h ; 0000h = offset, 1000h segment of the FAR JMP
; Error - end with HLT loop or you could use 'jmp $' as an infinite loop
error:
cli
endloop:
hlt
jmp endloop
main ENDP
; Boot sector data between code and boot signature.
; Don't put in data section as the linker will place that section after boot sig
org 7c00h+510 ; Pad out boot sector up to the boot sig
dw 0aa55h ; Add boot signature
END main
bpb.inc:
bpb_s STRUCT
; Dos 4.0 EBPB 1.44MB floppy
OEMname db "mkfs.fat" ; mkfs.fat is what OEMname mkdosfs uses
bytesPerSector dw 512
sectPerCluster db 1
reservedSectors dw 1
numFAT db 2
numRootDirEntries dw 224
numSectors dw 2880
mediaType db 0f0h
numFATsectors dw 9
sectorsPerTrack dw 18
numHeads dw 2
numHiddenSectors dd 0
numSectorsHuge dd 0
driveNum db 0
reserved db 0
signature db 29h
volumeID dd 2d7e5a1ah
volumeLabel db "NO NAME "
fileSysType db "FAT12 "
bpb_s ENDS
An example stage2.asm that displays a string when run:
.8086
.model tiny
.data
msg_str db "Running stage2 code...", 0
.code
org 0000h
main PROC
mov ax, cs
mov ds, ax
mov es, ax
cld
mov si, offset msg_str
call print_string
; End with a HLT loop
cli
endloop:
hlt
jmp endloop
main ENDP
; Function: print_string
; Display a string to the console on display page 0
;
; Inputs: SI = Offset of address to print
; Clobbers: AX, BX, SI
print_string PROC
mov ah, 0eh ; BIOS tty Print
xor bx, bx ; Set display page to 0 (BL)
jmp getch
chloop:
int 10h ; print character
getch:
lodsb ; Get character from string
test al,al ; Have we reached end of string?
jnz chloop ; if not process next character
ret
print_string ENDP
END main
To assemble and link the code and to create a disk image you can use these commands if using ML.EXE and LINK16.EXE from the MASM32 SDK:
ml.exe /Fe boot.bin /Bl link16.exe boot.asm
ml.exe /Fe stage2.bin /Bl link16.exe stage2.asm
copy /b boot.bin+stage2.bin disk.img
If you wish to include a BPB then you can assemble and link it this way:
ml.exe /DWITH_BPB /Fe boot.bin /Bl link16.exe boot.asm
ml.exe /Fe stage2.bin /Bl link16.exe stage2.asm
copy /b boot.bin+stage2.bin disk.img
Both methods create a disk image called disk.img
. When disk.img
is booted in BOCHS it should appear something like:

cs
to initializeds
andes
after explicitly wanting execution from0:7c00
, simply set them to0
(It won't matter for this exact example though). The cylinder/head/sector thing is confusing, but IIRC only sectors start from1
the others start from0
(but look this up). But you should really get some simple debug output up and running (and/or use a debugger e.g. Bochs). And if you don't already know about it, I really recommend the OSDEV wiki. – user786653