assemblyoperating-systemnasm

BIOS int 0x13 sector read works, but jump to loaded second stage code at 0x0800:0x0000 does not transfer control


I’m writing a simple bootloader that uses BIOS interrupt int 0x13 to read a second-stage loader from disk and then jump to it.

; boot\boot.asm

[bits 16]
[org 0x7C00]


jmp strict short start          
nop                             
                    

OEM_LABEL db "DAYS0.1 "         


; BIOS Parameter Block (BPB)          
bpb_bytes_per_sector    dw 512        ; 512 bytes per sector

bpb_sector_per_cluster  db 1          ; 1 sector per cluster

bpb_reserved_sectors    dw 1          ; 1 reserved sector (boot sector)

bpb_fat_count           db 2    ; 2 FAT tables (1 default, 1 reserved)

bpb_root_entries_count  dw 512        ; Number of entries in root directory

bpb_total_sectors_small dw 32768      ; 16 * 1024*1024 (16MB)/512 = 32768
                                      ; sectors

bpb_media_descriptor    dw 0xf8       ; 0xF8 = fixed disk(hard disk)

bpb_sectors_per_fat     dw 128        ; 1 FAT-table = 128 sectors

bpb_sectors_per_track   dw 32         ; dummy geometry for virtual disk

bpb_head_count          dw 16         ; dummy geometry for virtual disk 

bpb_hidden_sectors      dd 0          ; no hidden sectors before this volume

bpb_total_sectors_large dd 0          ; 0 = use bpb_total_sectors_small



; Extended Boot Record (FAT16)
ebr_drive_number        db 0          ; BIOS drive number: 0x80 = hard disk 
                                      ; Set by bootloader using DL register

ebr_reserved            db 0          ; Reserved byte, must be zero

ebr_boot_signature      db 0x29       ; Extended boot signature (must be 0x29)
                                      ; Indicates that the following fields are
                                      ; valid: 
                                      ; ebr_volume_id, ebr_volume_label, 
                                      ; ebr_filesystem_type

ebr_volume_id           dd 0x7E73B1BE ; Volume serial number
                                      ; (any 32-bit value, usually random) 
                                      ; Used by the OS

ebr_volume_label        db "DAYS_OS    " ; Volume label (must be 11 bytes,
                                         ; padded with spaces) 

ebr_filesystem_type     db "FAT16   " ; File system type label (8 bytes, padded
                                      ; with spaces) 

; Calculated constants from BPB
bpb_root_dir_sectors    equ (512 * 32) / 512
bpb_first_root_sector   equ 1 + (2 * 128)
bpb_first_data_sector   equ bpb_first_root_sector + bpb_root_dir_sectors



; Bootloader start
start:
    cmp dl, 0x80    ; Compare BIOS-loaded boot drive number (in DL) with 0x80   

    je main     ; Jump to main code if dl = 0x80 (correct drive device) 

    ; Error: Booted from unexpected device
    mov si, wrong_drive_error_msg
    call puts           
    jmp halt     

main:
    cli                 
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax          ; Set Stack Segment (SS) to 0x0000 
    mov sp, 0x7c00          ; Set Stack Pointer (SP) to 0x7c00  

    mov ax, cs              ; Load current Code Segment (CS) into AX

    
    mov si, no_error_msg
    call puts        
    call load_second_stage      ; Load second stage boot sector and jump to in  
    
load_second_stage:
    ; CH = cylinder number
    mov ch, 0x00 

    ; CL = sector number   
    mov cl, 0x02    

    ; DH = head number
    mov dh, 0x09      

    ; DL already contain the boot drive number (0x80 for HDD),
    ; so no need to modify it unless you're switching drives.
    
    mov ax, 0x0800      ; Set ES:BX as destination memory address for sector
    mov es, ax      ; Physical address = ES * 16 + BX =  
    xor bx, bx      ; 0x0800 * 16 + 0x0000 = 0x8000

    mov ah, 0x02    ; AH = 0x02 -> BIOS function: Read sector
    mov al, 0x01        ; AL = number of sectors to read (1 sector)  

    int 0x13        ; BIOS disk interrupt - reads sector into ES:BX 
    
    ; If Carry Flag (CF) set, jump to .disk_read_error label 
    ; (BIOS sets flag (CF) if reading with "int 0x13" failed
    jc .disk_read_error 

    ; Success: jump to the entry point of the second loading stage
    jmp 0x0800:0x0000
    
.disk_read_error:           
    mov si, disk_error_msg
    call puts
    jmp halt


; Function puts - prints a null-terminated string 
puts:
    push si         
    push ax         

.loop:
    lodsb           
                    
    or al, al       
    jz .done        

    mov ah, 0x0e    
    mov bh, 0       
    int 0x10        
    
    jmp .loop       

.done:
    pop ax          
    pop si          
    ret             


halt:               
   hlt              
   jmp halt         

; data
wrong_drive_error_msg   db "error: Booted from unexpected device", 0

disk_error_msg          db "error: Disk read error", 0 
no_error_msg            db "Booted coorectly", 0
new_line                db 0x0d, 0x0a, 0

times 510 - ($ - $$) db 0       

dw 0xAA55                   
; loader\loader.asm
[org 0x0000]   
[bits 16]

start:  
    mov ax, 0x0800 
    mov ds, ax
    mov es, ax

    ; stack
    mov ax, 0x0900
    mov ss, ax
    mov sp, 0x9200

    ; print 'S'
    mov ah, 0x0E
    mov al, 'S'
    int 0x10
.loop
    jmp .loop


times 512 - ($ - $$) db 0
; Makefile
BOOT = build/boot.bin
KERNEL = build/kernel.bin
LOADER = build/loader.bin
IMAGE = os.img

NASM = nasm
DD = dd
MTOOLS = mcopy

all: $(IMAGE)

# creating build directory if it's not exists
build:
    mkdir -p build

# building boot.asm
$(BOOT): boot/boot.asm | build
    $(NASM) -f bin boot/boot.asm -o $(BOOT)

# building kernel.asm
#$(KERNEL): kernel/kernel.asm | build
#   $(NASM) -f bin kernel/kernel.asm -o $(KERNEL)

#building loader.asm
$(LOADER): loader/loader.asm | build
    $(NASM) -f bin loader/loader.asm -o $(LOADER)

# creating FAT16-image with boot-loader and file
$(IMAGE): $(BOOT) $(LOADER) 
    # 1. creating empty file with 16MB size
    $(DD) if=/dev/zero of=$(IMAGE) bs=1M count=16

    # 2. formating image like FAT16 not touching first sector
    mkfs.fat -F 16 -n DAYS_OS $(IMAGE)
    
    # 3. Loads boot-loader sector
    $(DD) if=$(BOOT) of=$(IMAGE) bs=512 count=1 conv=notrunc

    # 4. Copying loader into the image
    $(DD) if=$(LOADER) of=$(IMAGE) bs=512 seek=289 count=1 conv=notrunc


run:
    qemu-system-x86_64 -drive format=raw,file=$(IMAGE)

clean:
    rm -rf build $(IMAGE)

; hexdump os.img

00000000  eb 3d 90 44 41 59 53 30  2e 31 20 00 02 01 01 00  |.=.DAYS0.1 .....|
00000010  02 00 02 00 80 f8 00 80  00 20 00 10 00 00 00 00  |......... ......|
00000020  00 00 00 00 00 00 00 29  be b1 73 7e 44 41 59 53  |.......)..s~DAYS|
00000030  5f 4f 53 20 20 20 20 46  41 54 31 36 20 20 20 80  |_OS    FAT16   .|
00000040  fa 80 74 08 be 9a 7c e8  3b 00 eb 4b fa 31 c0 8e  |..t...|.;..K.1..|
00000050  d8 8e c0 8e d0 bc 00 7c  8c c8 be d6 7c e8 25 00  |.......|....|.%.|
00000060  e8 00 00 b5 00 b1 02 b6  09 b8 00 08 8e c0 31 db  |..............1.|
00000070  b4 02 b0 01 cd 13 72 05  ea 00 00 00 08 be bf 7c  |......r........||
00000080  e8 02 00 eb 12 56 50 ac  08 c0 74 08 b4 0e b7 00  |.....VP...t.....|
00000090  cd 10 eb f3 58 5e c3 f4  eb fd 65 72 72 6f 72 3a  |....X^....error:|
000000a0  20 42 6f 6f 74 65 64 20  66 72 6f 6d 20 75 6e 65  | Booted from une|
000000b0  78 70 65 63 74 65 64 20  64 65 76 69 63 65 00 65  |xpected device.e|
000000c0  72 72 6f 72 3a 20 44 69  73 6b 20 72 65 61 64 20  |rror: Disk read |
000000d0  65 72 72 6f 72 00 42 6f  6f 74 65 64 20 63 6f 6f  |error.Booted coo|
000000e0  72 65 63 74 6c 79 00 0d  0a 00 00 00 00 00 00 00  |rectly..........|
000000f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000800  f8 ff ff ff 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000810  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00004800  f8 ff ff ff 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00004810  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00008800  44 41 59 53 5f 4f 53 20  20 20 20 08 00 00 ea 82  |DAYS_OS    .....|
00008810  04 5b 04 5b 00 00 ea 82  04 5b 00 00 00 00 00 00  |.[.[.....[......|
00008820  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00024200  b4 0e b0 53 cd 10 b8 00  08 8e d8 8e c0 b8 00 09  |...S............|
00024210  8e d0 bc 00 92 b4 0e b0  53 cd 10 fa f4 eb fe 00  |........S.......|
00024220  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
01000000

The first stage bootloader loads successfully from sector 1, and I confirmed it works — reading the sector with int 0x13 succeeds and I can output a character via int 0x10.

The second stage loader should be loaded from sector 289 (calculated using disk geometry: 16 heads, 32 sectors per track, cylinder = 0, head = 9, sector = 2).

I set segment register ES = 0x0800 and BX = 0, call int 0x13 to read one sector. The Carry Flag (CF) is clear, so no disk read error.

I check the first byte loaded at ES:[BX] — it differs from what I expect, but if I read sector 1 instead, the byte matches correctly.

After reading the second stage, I do a far jump jmp 0x0800:0x0000 to the loaded code, which was assembled with [org 0x0000].

In the second stage, I properly initialize segment registers (DS, ES).

I also placed code at the start of the second stage to output a character using int 0x10 to verify it runs, but no output appears and control does not seem to transfer.

I verified that sector 289 in my disk image (disk.img) contains the expected second-stage code at the offset 289 * 512 = 0x24200.


Solution

  • Thanks to @Nassau for pointing out the correct CHS values using sfdisk -g. I had been using BPB values for calculating CHS, which led me to load the wrong sector.

    Correcting the seek= in dd to match C:0 H:4 S:38 (i.e., sector 289) fixed the problem.