2
votes

I'm trying to build a multistage bootloader, but I'm stuck in the 1st stage code that is supposed to read the 2nd stage into memory, the code uses int 13h to read sectors form a virtual floppy disk (.img file). Here's the code (MASM syntax) :

        .286
        .model tiny
        .data
    org 07c00h
    driveNumber db ?
        .code
main:  jmp short start
       nop
start: mov driveNumber,dl  ;storing booting drive number
       cli
       mov ax,cs
       mov ds,ax
       mov es,ax
       mov ss,ax
       sti
reset: mov ah,0h              ;resetting the drive to the first sector
       mov dl,driveNumber
       int 13h
       js reset
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,01h             ;form cylinder #1
       mov cl,02h             ;starting from sector #2
       mov dh,01h             ;using head #1
       mov dl,driveNumber     ;on booting drive
       int 13h
       jc read

       push 1000h             ;pushing the memory address into stack
       push 0h                ;pushing the offset
       retf

end main

This code is put on the first sector of the virtual disk along with the 0x55AA signature in the last two bytes, and the 2nd stage code is put on the following sector.

And, as I'm here, it didn't work !

I tried it on both vmware and bochs , and both give the same thing : nothing!

So I ran some tests :

  1. I thought the problem may be about how the cylinders, heads, and sectors are indexed. So I tried various combinations of cylinders, heads, and sectors numbers but it did me no good.
  2. I checked the returns of the int 13h and I got: status code (ah ==00h -> successful), actual sectors read count (al = 01h -> 1 sector has been actually read).
  3. Before the reading process, I've put some value into es:bx and then ran the reading process, after it had finished, I checked the value at es:bx and found that it's still the value I've put before, not the value that should be read form the sector.

So, I have a test that tells me that the sector was actually read , and a test that tells me that there is nothing read into memory ... therefore , I'm stuck !

Any thoughts?

2
A few quick comments: You shouldn't use cs to initialize ds and es after explicitly wanting execution from 0:7c00, simply set them to 0 (It won't matter for this exact example though). The cylinder/head/sector thing is confusing, but IIRC only sectors start from 1 the others start from 0 (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

2 Answers

3
votes

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 #0
                               ; Starting from sector #2
    mov dh, 00h                ; Using head #0
    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:

enter image description here

0
votes

Take a look at Ralf Brown's interrupt list for int 13h.

IIRC, it even had some code to show you what to do to read/write specific pieces of data (such as bootsector) etc.