3
\$\begingroup\$

I am writing my own x86_64 operating system named Lavender. This is a sample of the bootloader, a two-stage Real Mode loader that first loads the rest of itself into memory and then does the standard things like setup the GDT, IDT, get native video mode information, etc.

This post, however, is JUST about the first stage of the bootloader. As it is literally THE first thing that runs on my operating system's boot, I want to get it right.

Is the below NASM assembly satisfactory? I am accepting any kind of criticism.

; This file provides the stage zero bootloader that will basically just load the
; second stage bootloader, which will load the kernel and other information.
; Author: Michael Sermir
;
; Copyright (c) 2026 the LavenderOS Project
; This program is free software: you can redistribute it and/or modify it under
; the terms of the GNU General Public License as published by the Free Software
; Foundation, either version 3 of the License, or (at your option) any later
; version.
;
; This program is distributed in the hope that it will be useful, but WITHOUT
; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
; FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
;
; You should have received a copy of the GNU General Public License along with
; this program.  If not, see <https://www.gnu.org/licenses/>.

; The BIOS will spit us out at address 0x7C00 in the wonderful 16-bit Real Mode.
org 0x7C00
bits 16

start:
    .reset:
        mov ah, 0
        mov dh, 0
        jnc .load

        ; Use AL as a failure counter so we can give the hardware some grace on
        ; this error-prone operation.
        inc al
        cmp al, 0x05
        jne .reset

        mov si, reset_failure_string
        jmp fail
    .load:
        xor ax, ax     ; Load the disk at 0x0000 (the current segment).
        mov bx, 0x7E00 ; Load the disk at offset 0x7E00 (right after this program).
        mov es, ax     ; Move our immediate address (AX) into the Extra Segment.
        
        mov ah, 0x02 ; "Read disk".
        mov al, 0x05 ; Number of sectors to read.
        mov ch, 0    ; Cylinder number.
        mov cl, 0x02 ; Sector number to begin at.
        mov dh, 0    ; Head number.
        mov dl, 0    ; Drive number.
        int 0x13
        jnc .execute

        mov si, load_failure_string
        jmp fail
    .execute:
        jmp 0x0000:0x7E00 ; Jump to the second stage.

; NOTE: This routine prints the return code in reverse order, beginning with the
; smallest digit.
fail:
    mov dl, ah  ; Save the return code so we can print it in a second.
    mov cl, 0xA ; Move 10 into CL so we can divide by it to print the error.

    mov ah, 0x0E                ; Teletype character.
    xor bx, bx                  ; Page 0 and no background color.
    .write_loop:
        lodsb     ; Load character into AL.
        or al, al ; Check AL for NUL terminator.
        jz .write_error_loop
        int 0x10
        jmp .write_loop
    .write_error_loop:
        xor ax, ax   ; Clear AX to remove garbage data.
        mov al, dl   ; Move our return code to the low operand register.
        div cl       ; Divide by 10 (0xA).
        add ah, 0x30 ; Add '0' to the produced digit to convert to ASCII.
        mov dl, al   ; Move our return back into storage.
        
        mov al, ah   ; Move the digit back into the character output register.
        mov ah, 0x0E
        int 0x10

        ; Check to see if we've consumed the whole return code.
        or dl, dl 
        jnz .write_error_loop
    cli
    hlt

load_failure_string:  db "DISK READ FAIL ", 0
reset_failure_string: db "DISK RESET FAIL ", 0

; Ensure the boot signature bookends the bootloader so the bootloader recognizes
; the sector as bootable.
times 510-($-$$) db 0
dw 0xAA55
New contributor
Sermir is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$
        mov ah, 0
        mov dh, 0
        jnc .load

I'm pretty sure you intended this to have an int 13h so it would actually attempt to reset the disk subsystem:

  .reset
        mov ah, 0
        mov dh, 0
        int 13h
        jnc .load
; ...

As it stands now, if the carry flag was set when your bootloader was loaded, it'll always print the failure string and exit. Otherwise, it'll jump directly to trying the load the bootsector, without resetting the disks.

I don't believe the BIOS is supposed to modify the registers, so I'd probably do something more like this:

    mov ax, 5
    xor dx, dx

    .reset
        int 13h
        jnc .load
        dec al
        jne .reset
    .load

Other than that, I can't say I like this very well:

; NOTE: This routine prints the return code in reverse order, beginning with the
; smallest digit.

Printing it out in the correct order is obviously preferable--and for this purpose, hexadecimal is probaby preferable to decimal (e.g., lists of BIOS error codes normally show hex numbers).

    ; warning: untested code
    mov bx, offset digits
.write_error_loop:
    xor ax, ax
    mov al, dl
    shr ax, 4 ; or mov cl, 4 and shr ax, cl, if you care about 8088/8086
    xlat
    mov ah, 0x0E
    int 10h
    mov al, dl
    and al, 0x0f
    xlat
    mov ah, 0x0E
    int 10h

; Down by load_failure_string and such:
digits db '0123456789abcdef'

The logic here is pretty simple. One byte (in AL, in this case) can hold 256 values, or 2 hexadecimal digits. 4 bits translates to one hex digit. So, to print out the two digits, we use 4 bits at a time to look up the printable digit in the digits table, then print it out.

The XLAT instruction is basically equivalent to mov AL, [BX+AL]. Before we start the translation, we load the address of digits into BX, then as we print out each digit, we use the 4 bits in AL to look up a printable digit from digits.

For the first digit, we want the 4 most significant bits, so we shift the 8 bits of AL right by 4 bits, to get only (what started out as) the 4 upper bits. After we print that out, we AND the original number with 0x0F to get the 4 least significant bits, which we use to look at the second digit (then print it out).

It wouldn't hurt to add a "BIOS Error code " label before printing out the code either.

\$\endgroup\$
3
  • \$\begingroup\$ It seems I'm missing a chunk of knowledge in x86_64 assembly, because that algorithm to print out the return code doesn't make a ton of sense to me. You're shifting ax by 4 and then looking up something in a table? To be honest, I've never even seen the XLAT instruction before, and SHR only a handful of occasions. Thank you so much for your other tips, but could you explain that code segment a little bit more? \$\endgroup\$ Commented 5 hours ago
  • \$\begingroup\$ And yes, I definitely missed the int 0x13. Not really sure how, but thank you for catching that. :) \$\endgroup\$ Commented 5 hours ago
  • \$\begingroup\$ @Sermir: I've edited to give a little more detai about how the hex digits are printed out. \$\endgroup\$ Commented 4 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.