Page 1 of 1

int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Wed Aug 31, 2016 10:21 pm
by virtualuk
I've been going through some tutorials and putting a bootloader that prints a message is pretty straightforward, no issues there. Next, I'm trying to read the next sector on the image which contains a similar instruction to print another message, but when I try to reset my drive (int 13h ah=00h) or attempt to get read the second sector (int 13h ah=02h) I always get back the status code ah=0eh. This is supposed to mean "Control data address mark detected", which I can't find any explanation at all after hours of scouring the net of what that actually means.

Here's my setup:

Windows 10 Pro 64-bit (Version 1511 OS Build 10586.545)
Cygwin (uname -a -> CYGWIN_NT-10.0 DESKTOP-T7AC450 2.5.2(0.297/5/3) 2016-06-23 14:29 x86_64 Cygwin)
NASM version 2.10.07
dd (coreutils) 8.25

I've tried both VirtualBox (Version 5.1.2 r108956) and bochs (2.6.8) and both yield the same result. The code is pretty small so I'll post it here.

bootloader.asm

Code: Select all

BITS 16

start:

        mov ax, 07C0h           ; Set up 4K stack space after this bootloader
        add ax, 288             ; (4096 + 512) / 16 bytes per paragraph
        mov ss, ax
        mov sp, 4096
        mov ax, 07C0h           ; Set data segment to where we're loaded
        mov ds, ax

reset: ; Reset the floppy drive
        mov ax, 0
        mov dl, 0 ; Drive=0 (=A)
        int 13h
        jc reset ; ERROR => reset again

        mov si,status_string
        call print_string
        push ax
        mov al,ah
        call print_hex_byte
        pop ax

read:
        mov ax, 0900h ;
        mov es, ax
        mov bx, 0
        mov ah, 2 ; Load disk data to ES:BX
        mov al, 1 ; Load 5 sectors
        mov ch, 0 ; Cylinder=0
        mov cl, 2 ; Sector=2
        mov dh, 0 ; Head=0
        mov dl, 0 ; Drive=0
        int 13h ; Read!
        Jc read ; ERROR => Try again

        mov si,status_string
        call print_string
        push ax
        mov al,ah
        call print_hex_byte
        pop ax

        mov si,read_string
        call print_string
        call print_hex_byte
        mov si,sectors_string
        call print_string

        jmp 0900h ; Jump to the program

fail_string db 13,10,'Fail',0
read_string db 13,10,"Read ",0
sectors_string db " sectors",0
status_string db 13,10,"Status ",0

print_string:                   ; Routine: output string in SI to screen
        mov ah, 0Eh             ; int 10h 'print char' function

.repeat:

        lodsb                   ; Get character from string
        cmp al, 0
        je .done                ; If char is zero, end of string
        int 10h                 ; Otherwise, print it
        jmp .repeat

.done:
        ret

print_hex_byte:                 ; Routine : output the value of AL on the screen in hex
        pusha
        mov bl, 11110000b       ; MSB mask
        and bl, al
        shr bl, 4               ; Shift right 4 bits
        push ax
        mov al,bl
        call fourbit_to_ascii
        mov al,bl
        mov ah, 0Eh
        int 10h
        pop ax
        mov bl, 00001111b
        and al,bl
        call fourbit_to_ascii
        mov al,bl
        int 10h
        mov al,104
        int 10h
        popa
        ret

fourbit_to_ascii:               ; Routine : Puts the ASCII representation of the 4 LSB of AL into BL
        cmp al, 9
        jle .number
        add al,55
        mov bl,al
        ret
.number:
        add al,48
        mov bl,al
        ret

times 510-($-$$) db 0   ; Pad remainder of boot sector with 0s

dw 0xAA55               ; The standard PC boot signature
Although the code in the 2nd sector prints a message like the function above I labelled it something different just to make it distinct for the purposes of testing. The 2nd sector code is below.

helloworld.asm

Code: Select all

BITS 16

hello_world_start:
        mov si, secondstage_string      ; Put string position into SI
        call pprint_string      ; Call our string-printing routine

hang:
        jmp hang                        ; Jump here - infinite loop!

        secondstage_string db 13,10,'Hello World!', 0

pprint_string:                  ; Routine: output string in SI to screen
        mov ah, 0Eh             ; int 10h 'print char' function

.repeat:
        lodsb                   ; Get character from string
        cmp al, 0
        je .done                ; If char is zero, end of string
        int 10h                 ; Otherwise, print it
        jmp .repeat

.done:
        ret

times 510-($-$$) db 0   ; Pad remainder of boot sector with 0s
dw 0x0000               ; The standard PC boot signature
I use the following bash script to compile, create a blank 1.44MB floppy image and copy the code into the two first sectors.

build.sh

Code: Select all

#!/bin/bash

/usr/bin/nasm -f bin -l bootloader.lst bootloader_s.asm -o bootloader.bin
/usr/bin/nasm -f bin -l helloworld.lst helloworld.asm -o helloworld.bin
echo "Compilation complete"

dd bs=512 count=2880 if=/dev/zero of=disk.img
dd if=bootloader.bin of=disk.img conv=notrunc
dd if=helloworld.bin of=disk.img seek=512 conv=notrunc
echo "Disk image complete"
But regardless of which VM I run it on, I get the following output:

Code: Select all

Status 0Eh
Status 0Eh
Read 00h sectors
Any help in helping me understand that cause of my problem would be appreciated.

Cheers,

L

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Wed Aug 31, 2016 11:49 pm
by BenLunt
virtualuk wrote:"I always get back the status code ah=0eh"
We will come back to this...

Code: Select all

BITS 16

start:
        mov ax, 07C0h           ; Set up 4K stack space after this bootloader
        mov ds, ax
        add ax, 288             ; (4096 + 512) / 16 bytes per paragraph
        mov ss, ax     ;;; stack now at 1200h + 4096
        mov sp, 4096
  ; I moved this up a few lines.  No need to reload ax again.
  ;      mov ax, 07C0h           ; Set data segment to where we're loaded
  ;      mov ds, ax

reset: ; Reset the floppy drive
  ;     mov ax, 0
        xor  ax,ax                  ;;;  sorry, can't help it.   ;;;
  ;      mov dl, 0 ; Drive=0 (=A)
        xor dl,dl                     ;;;  it's a disease, really   ;;;
        int 13h
        jc reset ; ERROR => reset again

;;; you should really put a count down in that.  If the drive is faulty or non-existant,
;;;  you will never continue on... 

        push ax
        mov si,status_string
        call print_string
;;; look at your print_string function below.  It modifies AX...
;        push ax
        pop ax
        mov al,ah
        call print_hex_byte
;        pop ax

read:
        mov ax, 0900h ;
        mov es, ax
  ;      mov bx, 0
        xor  bx,bx            ;;; I should see a doctor about it ;;;
        mov ah, 2 ; Load disk data to ES:BX
        mov al, 1 ; Load 1 sector(s)
        mov ch, 0 ; Cylinder=0    ;;; okay, I refrained from changing this one, and the two below....
        mov cl, 2 ; Sector=2
        mov dh, 0 ; Head=0
        mov dl, 0 ; Drive=0
        int 13h ; Read!
        Jc read ; ERROR => Try again

;;; here your print status never gets called unless the read succeeds, not fails.
;;; second, your status string modifies AX.
;;; And again, you should count down to a few (3) tries and abort if unsuccessful.

        push ax
        mov si,status_string
        call print_string
        pop ax
;        push ax
        mov al,ah
        call print_hex_byte
;        pop ax

        mov si,read_string
        call print_string
        call print_hex_byte
        mov si,sectors_string
        call print_string

;;; You jump to 07C0:0900h (+/- relative offset) here, not 0900:0000h as you probably wish you had ;;;
;;; try a far jump or a retf instruction instead.  ;;;
        jmp 0900h ; Jump to the program

print_string:                   ; Routine: output string in SI to screen
        push ax  ;;;
        mov ah, 0Eh             ; int 10h 'print char' function
.repeat:
        lodsb                   ; Get character from string
        cmp al, 0
        je .done                ; If char is zero, end of string
        int 10h                 ; Otherwise, print it
        jmp .repeat
.done:
        pop ax ;;;   now you can get rid of the push/pop's above...
        ret

;;; here is where you are getting the 0Eh returned in AL.  First of all, you need to save the
;;;  AX register either above as I have mentioned, or within this function.
;;;  Second, you place 0Eh in AH to print a char, find the end of the string and return.
;;;  Hence, AH = 0Eh...., and AL will be zero (0), the count of sectors you read, so it seams.
Hence, you have the correct values below due to the fact that print_string destroys
(or better yet, doesn't preserve) AX (AH and AL).

Code: Select all

Status 0Eh
Status 0Eh
Read 00h sectors
Preserved AX and you should see that the function actually succeeded and not failed...

Ben

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Wed Aug 31, 2016 11:51 pm
by FallenAvatar

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Thu Sep 01, 2016 3:26 am
by Brendan
Hi,

Some more notes (not related to the bug, and not including bugs already mentioned by Ben):

Code: Select all

start:
        mov ax, 07C0h           ; Set up 4K stack space after this bootloader
        add ax, 288             ; (4096 + 512) / 16 bytes per paragraph
        mov ss, ax
        mov sp, 4096
This makes SS:SP equal to "0x07C0+288:4096", or "0x08E0:0x1000", which means the stack top begins at physical address 0x00009E00. Later you load 1 sector at 0x0900:0x000 (physical address 0x00009000). This gives you physical memory usage like this:

Code: Select all

0x00000000 to 0x000007FF = BIOS data
0x00000800 to 0x00007BFF = unused
0x00007C00 to 0x00007DFF = your code & data
0x00007E00 to 0x00008FFF = unused
0x00009000 to 0x000091FF = the sector you loaded
0x00009200 to 0x00009DFF = 3072 bytes of stack space
This is currently "fine", but it's also very unlikely that it's what you intended (and your "4K stack space" comment is wrong), and it will cause problems when you want to load more than 1 sector later.

Note that for simplicity, I'd recommend using "CS = DS = ES = SS = 0" (with "org 0x7C00") for most of your code because this avoids lots of confusion and makes it easier to switch to/from "flat protected mode".

Code: Select all

reset: ; Reset the floppy drive
You never need to reset the floppy drive (almost) immediately after the BIOS has successfully used it to load your code (and should only reset it after you get some kind of disk error, as part of your "retry N times" loop).

Code: Select all

        mov dl, 0 ; Drive=0
There shouldn't be any reason why the user can't boot from the second floppy disk (e.g. by using any chainloader). You want to use the "device number" that the BIOS (or whatever chainloaded your code) told you to use (that was left in DL when your code was jumped to) and shouldn't assume "device number 0x00".

Code: Select all

        jmp 0900h ; Jump to the program
The sector you loaded is at 0x0900:0x000 (physical address 0x00009000). You are using a "near jump" that doesn't change CS, and your CS is still "random whatever" (possibly 0x0000, possibly 0x07C0, and possibly something else); so this means that it will jump to 0x0000:0x0900 (physical address 0x00000900), or 0x07C0:0x0900 (physical address 0x8500), or somewhere else. It's extremely unlikely that it will jump to 0x0900:0x0000 (physical address 0x00009000).

Code: Select all

        lodsb                   ; Get character from string
The "lobsb" instruction either increments SI or decrements SI, depending on how the CPU's "direction flag" is set. You haven't cleared the direction flag (e.g. with a "cld" instruction) and can't assume the BIOS left it in any particular state, so this can cause problems (like not printing strings and/or printing garbage backwards).


Cheers,

Brendan

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Thu Sep 01, 2016 4:26 am
by Antti
When execution is passed from BIOS to Master Boot Record ("hard disks") or Volume Boot Record ("floppies", "partitionless"), the de facto standards compliant systems most reliably support the layouts I attached to this post. No need to change anything for now but I just wanted to share this general information.

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Thu Sep 01, 2016 5:46 am
by Antti
Brendan wrote:The sector you loaded is at 0x0900:0x000 (physical address 0x00009000). You are using a "near jump" that doesn't change CS, and your CS is still "random whatever" (possibly 0x0000, possibly 0x07C0, and possibly something else); so this means that it will jump to 0x0000:0x0900 (physical address 0x00000900), or 0x07C0:0x0900 (physical address 0x8500), or somewhere else. It's extremely unlikely that it will jump to 0x0900:0x0000 (physical address 0x00009000).
Unfortunately this is not true because in OSDev the devil is in the details. In this context I think we can safely assume that the code section started to execute at physical address 0x00007C00. The near jump "jmp 0900h" is fully deterministic, although not correct. NASM thinks the ORG is zero, so the near jump lands at physical address (0x00007C00 + 0x00000900 = 0x00008500) even if register CS ranges from 0x0000 to 0x07C0.

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Thu Sep 01, 2016 11:18 am
by mikegonta
Antti wrote:
Brendan wrote:The sector you loaded is at 0x0900:0x000 (physical address 0x00009000). You are using a "near jump" that doesn't change CS, and your CS is still "random whatever" (possibly 0x0000, possibly 0x07C0, and possibly something else); so this means that it will jump to 0x0000:0x0900 (physical address 0x00000900), or 0x07C0:0x0900 (physical address 0x8500), or somewhere else. It's extremely unlikely that it will jump to 0x0900:0x0000 (physical address 0x00009000).
Unfortunately this is not true because in OSDev the devil is in the details. In this context I think we can safely assume that the code section started to execute at physical address 0x00007C00. The near jump "jmp 0900h" is fully deterministic, although not correct. NASM thinks the ORG is zero, so the near jump lands at physical address (0x00007C00 + 0x00000900 = 0x00008500) even if register CS ranges from 0x0000 to 0x07C0.
Actually, a relative jmp is to the address of the offset from the instruction following the jump (in this case 0x855D - assuming that the
code was loaded to 0x0000:0x7C00 - which is most likely) and has nothing to do with the assembler directive.
On a related note, I can personally verify (although I no longer have the machine) that a Compaq PC BIOS loaded the boot sector to
0x07C0:0000 probably because it was Compaq that reverse engineered the original PC BIOS.

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Thu Sep 01, 2016 11:37 am
by Antti
What? The ORG directive is very relevant. In assembly, having a "jmp 0900h" does not mean 0x0900 is the offset.

Re: int 13h ah=00h / ah=02h always returns ah=0eh

Posted: Thu Sep 01, 2016 12:15 pm
by Brendan
Hi,
Antti wrote:What? The ORG directive is very relevant. In assembly, having a "jmp 0900h" does not mean 0x0900 is the offset.
Sure - it means "ip = address of byte after instruction +/- the offset", where the assembler has to know "address of byte after instruction" (the ORG directive) to achieve what the source code asked for.
Antti wrote:
Brendan wrote:The sector you loaded is at 0x0900:0x000 (physical address 0x00009000). You are using a "near jump" that doesn't change CS, and your CS is still "random whatever" (possibly 0x0000, possibly 0x07C0, and possibly something else); so this means that it will jump to 0x0000:0x0900 (physical address 0x00000900), or 0x07C0:0x0900 (physical address 0x8500), or somewhere else. It's extremely unlikely that it will jump to 0x0900:0x0000 (physical address 0x00009000).
Unfortunately this is not true because in OSDev the devil is in the details. In this context I think we can safely assume that the code section started to execute at physical address 0x00007C00. The near jump "jmp 0900h" is fully deterministic, although not correct. NASM thinks the ORG is zero, so the near jump lands at physical address (0x00007C00 + 0x00000900 = 0x00008500) even if register CS ranges from 0x0000 to 0x07C0.
You're right - but (at least in theory) it's not quite deterministic (there's actually 2 cases - e.g. imagine if BIOS jumped to 0xF7E0:0xFE00). ;)


Cheers,

Brendan