Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bootloader - load 2nd stage - qemu works, real machine doesn't

As a learning exercise, I wrote a little 16 bit bootloader for x86 bios systems. It seemed to work fine on QEMU. I dd'ed it to a drive for an old amd-turion computer (x86_64), and when I tried to boot that computer, it would do the BIOS screen, then I just get a blinking cursor on a black screen.

My question is, what could be different between QEMU's x86 emulator and a real x86 (64 bit) computer that definitely uses BIOS and not UEFI? Do I have to write my code differently for a real computer than than that of QEMU? Is it the way I'm copying the information to the drive? Is the computer employing some hardware-level security provision?

I have learned that it doesn't work on VirtualBox either.

It appears that loading the 2nd stage is problematic (by printing a char from the first stage successfully on real hardware).

My first stage bootloader uses code in these files:

stage_one.asm

[bits 16]
[org 0x7c00]
LOAD_ADDR: equ 0x9000   ; This is where I'm loading the 2nd stage in RAM.
start:
    xor ax, ax          ; nullify ax so we can set 
    mov ds, ax          ; ds to 0
    mov sp, bp          ; relatively out of the way
    mov bp, 0x8000      ; set up the stack
    call disk_load      ; load the new instructions
                        ; at 0x9000
    jmp LOAD_ADDR
%include "disk_load.asm"
times 510 - ($ - $$) db 0
dw 0xaa55 ;; end of bootsector

disk_load.asm

; load DH sectors to ES:BX from drive DL
disk_load :

    mov ah, 0x02            ;read from disk
    mov al, [num_sectors]   ;read sector(s)

    mov bx, 0
    mov es, bx

    mov ch, 0x00    ;track 0
    mov cl, 0x02    ;start from 2nd sector
    mov dh, 0x00    ;head 0
    mov dl, 0x80    ;HDD 1

    mov bx, LOAD_ADDR  ;Where we read to in RAM.
    int 0x13        ; BIOS interrupt

    jc disk_error   ; Jump if error ( i.e. carry flag set )

    cmp al, [num_sectors]   ; if num read != num expected
        jne disk_error      ; display error message

    mov ax, READ_SUCCESS
    call print_string
    ret

disk_error :
    mov ax, DISK_ERROR_MSG
    call print_string

    ; Get the status of the last operation.
    xor ax, ax      ; nullify ax
    mov ah, 0x01    ; status fxn
    ;mov dl, 0x80    ; 0x80 is our drive
    int 0x13        ; call fxn

    ;mov ah, 0       ; when we print ax, we only care about the status, 
                    ; which is in al. So, we probably want to nullify
                    ; 'ah' to prevent confusion.

    call print_hex  ; print resulting status msg.
    jmp $

status_error:
    mov ax, STATUS_ERROR
    call print_string
    jmp $

num_sectors: db 0x01

; Variables
DISK_ERROR_MSG: db "Disk read error: status = " , 0
STATUS_ERROR: db 'status failed.', 0
READ_SUCCESS: db 'Read success! ', 0

;AH  02h
;AL  Sectors To Read Count
;CH  Cylinder
;CL  Sector
;DH  Head
;DL  Drive
;ES:BX   Buffer Address Pointer

%include "print.asm"

print.asm

print_char:
    pusha
        mov ah, 0x0e
        int 0x10
    popa
    ret

print_string:
    pusha           ; preserve the registers 
                    ; on the stack.
    mov bx, ax
    print_string_loop:
        mov al, [bx]            ;move buffer index to al
        cmp al, 0               ;if [[ al == 0 ]]; then
        je print_string_end    ;   goto print_string_end
        inc bx                  ;else bx++
        call print_char
        jmp print_string_loop  ;   goto print_string_loop

    print_string_end:
    popa
    ret

print_hex_char:
    pusha
    print_hex_loop:

        ;print a single hex digit
        cmp al, 0x9
        jg a_thru_f
        zero_thru_nine:
            add al, '0'
            call print_char
            jmp print_hex_char_end
        a_thru_f:
            add al, 'A'-0xA
            call print_char

    print_hex_char_end:
    popa
    ret

print_hex:

    pusha

        ;note on little-endianness:
        ;   If you store 1234 in AX,
        ;   4 is the LSB, therefore:
        ;   AH = 12
        ;   AL = 34
        ;
        ;   Moral of the story --
        ;   If you print, you need to
        ;   print AH first.


        mov bl, al
        and bl, 0xF

        mov bh, al
        shr bh, 4
        and bh, 0xF

        mov cl, ah
        and cl, 0xF

        mov ch, ah
        shr ch, 4
        and ch, 0xF

        mov al, '0'
        call print_char

        mov al, 'x'
        call print_char

        mov al, ch
        call print_hex_char

        mov al, cl
        call print_hex_char

        mov al, bh
        call print_hex_char

        mov al, bl
        call print_hex_char

        mov al, ' '
        call print_char

    popa
    ret

I generate my kernel like this:

nasm -f bin -o stage1.bin stage_one.asm && \
nasm -f bin -o stage2.bin stage_two.asm && \
cat stage1.bin stage2.bin > raw.bin && \
#(mkdir cdiso || :) && \
#cp stage1.bin cdiso && cp stage2.bin cdiso && \
#mkisofs -o raw.iso -b stage1.bin cdiso/
qemu-system-x86_64 raw.bin || \
echo "COULD NOT FINISH ASSEMBLING." &>/dev/stderr
like image 494
James M. Lay Avatar asked Sep 26 '22 05:09

James M. Lay


1 Answers

I discovered the bug that was causing my code to fail.

The problem was when using int 13h to read from the hard drive. In QEMU, the disk's device number is always 0x80, so I was hard-coding that into register dl, thinking that was the standard hard-drive designation. As it turns out, BIOS conveniently automatically sets the dl register to the drive number that it is trying to boot from, so by commenting out the hard-coded part, I was able to get it working both on QEMU and real hardware.

To fix the issue I removed(commented out) this line in the file disk_load.asm:

mov dl, 0x80    ;HDD 1
like image 141
James M. Lay Avatar answered Oct 11 '22 15:10

James M. Lay