2

I am building an operating system and am experiencing a problem where the bootloader successfully prints its message, but the kernel's message isn't displayed. Here's a breakdown:

Setup:

Assembler and Environment:

  • NASM with -f bin option
  • Tested on qemu-system-i386
  • Environment: Windows Subsystem for Linux (WSL) with Xlaunch (though I suspect this isn't relevant)

Quick Note

push ax
push bx
mov bl, dl                  ; This is to preserve dl
push bx

This code snippet is part of a bootloader, and it might seem confusing initially.

The requirement here is to preserve the dl register throughout the function. You might ask:

  1. Why not just push dl onto the stack?
    • The reason is the stack only works with 16-bit values. dl is an 8-bit register, so pushing it directly isn't possible.
  2. Why not push dx?
    • This is because dh serves as a return value in this context.

To work around these constraints, I took this approach:

  • Pushed bx onto the stack.
  • Moved the value of dl into the lower bits of bx (i.e., bl).
  • Pushed bx (now holding the value of dl) onto the stack again.
  • Later, to restore the value, I simply popped bx and then mov dl, bl.

Files:

bootloader.asm

[org 0x7c00]
[bits 16]

jmp main

_message_read_err:  db 'Error in reading floppy!', 0
_message_boot:      db 'Booting SSoS...', 0
_sectors_per_track: dw 18
_head_count:        dw 2
_kernel_start_LBA:  dw 1
_kernel_size:       dw 1
_stack_ptr_addr:    dw 0x7c00
_es_start:          dw 0x7c0

; Arguments:
;     None
; Returns:
;     None
main:

    mov ax, 0 ; can't write to ds and ss directly
    mov ds, ax 
    mov ss, ax 
    mov sp, [_stack_ptr_addr] ; stack pointer

    mov ax, [_es_start] ; address * 16 = real address -> 0x7c0 * 16 (dec) = 0x7c00
    mov es, ax 

    mov si, _message_boot
    call puts

    ; Converting LBA to CHS
    ;; Parameters
    mov si, 1
    call LBA_to_CHS ; after this call, CH, CL, and DH will have cylinder, sector, and head values
    
    mov bx, 0x200
    mov al, [_kernel_size] ; number of sectors to read
    call read_disk

    jmp 0:7e00h

; Arguments:
;     si - Points to the string to be printed
; Returns:
;     None
puts:
    push ax
.begin:
    
    mov ah, 0x0E
    lodsb
    
    cmp al, 0
    je .done
    
    int 10h
    jmp .begin
.done:

    pop ax
    
    ret

; Arguments:
;     si - LBA adress 
; Returns:
;     ch, dh, cl - CHS adress
;; dx and cx will have to be modified
LBA_to_CHS:

    push ax
    push bx

    mov bl, dl                  ; This is to preserve dl

    push bx
    
    mov ax, [_head_count]
    mul word [_sectors_per_track]

    mov bx, ax
    mov ax, si
    mul word bx
    mov ch, al                  ; Put lower bits of ax as the cylinder address
    
    mov ax, si
    div word [_sectors_per_track] ; result in dx:ax
    div word [_head_count]        ; result also in dx:ax (dx is remainder)
    mov dh, dl                    ; since dx is composed of dh (higher bits) and dl (lower bits) we want to move the lower bits into dh

    push dx
    
    mov ax, si
    div word [_sectors_per_track] ; remainder in dx
    inc dx

    mov cl, dl

    pop dx

    pop bx
    mov dl, bl
    pop bx
    pop ax
    
    ret


; Arguments:
;     bx - Address to load the data
;     ch, cl, dh - CHS values
; Returns:
;     None

read_disk:
    push ax
    push si
    
    mov si, 4
    
.retry:
    dec si

    cmp si, 0
    je .read_error
    
    mov ah, 02h
    mov al, [_kernel_size]

    int 13h

    jc .retry

    pop si
    pop ax
    
    ret 

.read_error:
    
    mov si, _message_read_err
    call puts

    cli
    hlt

times 510-($-$$) db 0

dw 0aa55h

kernel.asm

[org 0x7E00]
[bits 16]

jmp main

_message: db 'Hello from kernel!', 0

main:
    mov si, _message
    call puts

    cli
    hlt

puts:
    mov ah, 0x0E
.begin:
    lodsb 
    cmp al, 0
    je .done
    int 10h
    jmp .begin
.done:
    ret

Makefile

BUILD_DIR = build
SRC_DIR = src
IMG_NAME = dev

dev: image
    qemu-system-i386 -fda $(BUILD_DIR)/$(IMG_NAME).img

image: $(BUILD_DIR)/bootloader.bin $(BUILD_DIR)/kernel.bin
    @dd if=$(BUILD_DIR)/bootloader.bin of=$(BUILD_DIR)/$(IMG_NAME).img bs=512
    @dd if=$(BUILD_DIR)/kernel.bin of=$(BUILD_DIR)/$(IMG_NAME).img bs=512 seek=1
    @truncate -s 1440k $(BUILD_DIR)/$(IMG_NAME).img
    @hexdump -C $(BUILD_DIR)/$(IMG_NAME).img
    @objdump -b binary -m i8086 -M intel -D build/dev.img

clean:
    rm -f $(BUILD_DIR)/bootloader.bin $(BUILD_DIR)/kernel.bin

$(BUILD_DIR)/%.bin: $(SRC_DIR)/%.asm
    nasm $^ -f bin -o $@

Specifics on Constants in bootloader.asm:

  • _stack_ptr_addr: Set to 0x7b00 because it's 256 bytes under the bootloader. I believe this is a safe location since the stack grows downwards.
  • _es_start: Set to 0x7c0 so the actual physical address (in real mode) equals 0x7c00 — the bootloader's load address. I offset by 0x200 when jumping to the kernel, which matches the kernel's load address. Disassembly of the image for clarity shows that the first instruction appears at byte 0x200.

The issue:

Upon booting, the message "Booting SSoS..." is displayed, but the kernel's "Hello from kernel!" message isn't.

Observations & Assumptions:

Someone also had the same problem here. I tried removing the mov dl, 0 in the read_disk function but it still didin't work. I suspect the issue might be in the read_disk function within the bootloader. Even though no error message displays, the qemu-system-i386 command seems unusual. Constants like _sectors_per_track and drive number (register dl in read_disk function) seem accurate. However, even when I attempted different disk-reading methods, the problem was still occurring.

Seeking Help:

I know this is a complex and low-level topic which requires a deep understanding of operating systems. That's why I thank everyone who will try to help me. Thanks a lot!

I tried different methods of reading the floppy disk (like reading directly with CHS adressing) however none of them worked. I also double-checked my constants and looked on the disassebly of the image but everything seems to be right. I looked into others code but mine seems (almost except it's not that perfect) identical.

ThatGuy. exe
  • 35
  • 1
  • 5
  • 1
    `jmp far [es:bx]` is wrong as it is an indirect jump. Also even if it were direct that would jump to `7c0h:200h` but kernel.asm has `org 7e00h`. Just do `jmp 7e00h` (if you are happy with a hardcoded address). – Jester Aug 10 '23 at 14:41
  • `jmp far [es:bx]` is certainly wrong. Try `push es` \ `push bx` \ `retf` or hardcode a far jump immediate `jmp 0:200h` (But you also have to change `[org 0x7E00]` in the kernel to `org 200h`.) ETA: No, you want `jmp 0:7E00h` (I missed the `es` re-init.) – ecm Aug 10 '23 at 14:44
  • 1
    @Jester I wouldn't recommend a near jump like `jmp 7E00h` because `cs` can be 7C0h. Better harden it to a far immediate jump like `jmp 0:7E00h` – ecm Aug 10 '23 at 14:48
  • 1
    "_stack_ptr_addr: Set to 0x7b00 because it's 256 bytes under the bootloader. I believe this is a safe location since the stack grows downwards." Setting `ss:sp` to 0:7C00h is fine, though your choice also works. And if you have no other use for it, the variable `_stack_ptr_addr` isn't needed, just write `mov sp, 7C00h`. You did get the stack init right so as to load `ss` and then in the very next instruction set `sp`. `LBA_to_CHS` does not preserve `dl` so in `read_disk` you must restore or reset `dl`. Forcing it to zero will hardcode the first diskette unit, fda, to load from. – ecm Aug 10 '23 at 14:56
  • `LBA_to_CHS` also seems super wrong. Try to step through it in a debugger, eg [the bootable lDebug](https://pushbx.org/ecm/web/#projects-ldebug) ETA: The results of the function may randomly happen to be correct but the calculation is nonsense. – ecm Aug 10 '23 at 15:09
  • 1
    The `jc .retry` loop in `read_disk` doesn't make any sense. You try to use `ax` as a retry counter but you do not preserve it across the function call. And your attempt to initialise the retry counter always corrupts the kernel size in sectors that you passed in `al`. Are you aware that the 8-bit `al` and `ah` combine to the 16-bit `ax`? If you write to `ax` you're also writing to `al` and `ah`. For the retry counter, use another register not used by the `int 13h` call (eg `si`, `di`, `bp`), or a memory variable, or properly preserve the counter value using `push` and `pop`. – ecm Aug 10 '23 at 16:00
  • I think `_kernel_start_LBA` should be set to 0 according to this conversion `LBA = (C × HPC + H) × SPT + (S − 1)`. If `LBA = 1` code reads `C:0 H:0 S:2` not `mbr C:0 H:0 S:1`. – Nassau Aug 10 '23 at 20:54
  • 2
    The LBA to CHS equation can be simplified for use with the DIV instruction of x86. I discuss that in this question along with some code that contains an `lba_to_chs` function in assembler: https://stackoverflow.com/a/45495410/3857942 – Michael Petch Aug 10 '23 at 22:19
  • The boot loader will need a rework because (as @ecm pointed out) the 8bit registers are a subset of the 16bit register so I was overwriting them. I’ll also rewrite the LBA to CHS function and fix the jump to the kernel. The retry function also needs a fix. I’ll update the post by today and let you know. Thanks y’all! – ThatGuy. exe Aug 11 '23 at 08:06
  • @ThatGuy.exe You can make a new post as an answer to your question, fixing all the errors. – ecm Aug 11 '23 at 08:10
  • 1
    @ecm, @Jester and @Michael Petch, I rewrote the bootloader and simplified/rewrote the `lba_to_chs` function. I also fixed the issue where I was overwriting registers accidentally. Also fixed the jump to the kernel. Thank you for your feedback, however `hello form kernel` still doesn't get printed. – ThatGuy. exe Aug 12 '23 at 08:57
  • @ThatGuy.exe Your `lba_to_chs` still seems wrong. Stepping through it [in my debugger](https://pushbx.org/ecm/web/#projects-ldebug) booted from a HDD image, the return values from the function are `dx=0000` and `cx=2402`. If I manually run a `r cx = 2` command then the kernel message is displayed correctly. – ecm Aug 12 '23 at 10:34
  • Hmmm… that’s weird. I used a formula from Wikipedia, but I might have implemented it wrong. @ecm , do you have an idea how to implement the LBA to CHS function? I can’t do it now, because I’m not on my computer. I’ll try to find a solution next week. Thanks for the feedback! – ThatGuy. exe Aug 12 '23 at 11:25
  • @ThatGuy.exe Here's an example: https://hg.pushbx.org/ecm/ldosboot/file/9c4923acc2be/boot.asm#l1011 (However, this is optimised a lot and may be more difficult to read. It does work, though.) Reviewing yours, the `mul word bx` is super wrong (you never want to *multiply* the LBA (in `ax` here) by anything). The two `div word` instructions are also wrong, as the first will write result to `ax` and remainder to `dx`, but the second one will use `dx:ax` as the implicit one of its inputs, which includes the remainder of the prior `div` as the input high word. – ecm Aug 12 '23 at 13:06
  • Thx @ecm. I’ll try it when i have time. – ThatGuy. exe Aug 12 '23 at 14:21

1 Answers1

1

Update:

Below is Your code:

  1. I added few xor dx,dx here and there
  2. mul word bx should be div word bx
  3. Changed in kernel.asm first line to old value 0x7e00.
  4. push and pop instructions do nothing special. You can remove them.
  5. I don't know if I understand conversion LBA -> CHS correctly. What take into account? Values from ax or dx?

Source:
https://datacadamia.com/io/drive/lba
https://en.wikipedia.org/wiki/Logical_block_addressing

But code seems to work fine!

Result is:

enter image description here

Bootloader.asm

[org 0x7c00]
[bits 16]

jmp main

_message_read_err:  db 'Error in reading floppy!', 0
_message_boot:      db 'Booting SSoS...', 0
_sectors_per_track: dw 18
_head_count:        dw 2
_kernel_start_LBA:  dw 1
_kernel_size:       dw 1
_stack_ptr_addr:    dw 0x7c00                                   ;prev value 0x7b00
_es_start:          dw 0x7c0

; Arguments:
;     None
; Returns:
;     None
main:
    mov ax, 0 ; can't write to ds and ss directly
    mov ds, ax 
    mov ss, ax 
    mov sp, [_stack_ptr_addr] ; stack pointer

    mov ax, [_es_start] ; address * 16 = real address -> 0x7c0 * 16 (dec) = 0x7c00
    mov es, ax 

    mov si, _message_boot
    call puts

    ; Converting LBA to CHS
    ;; Parameters
    
;(* 1 *)
     ;; before call to LBA_to_CHS
     ;; ax = 0x07c0
     ;; bx = unknown value, could be anything from 16-bit range (0x0000 - 0xFFFF)
     ;; cx = same cx, not initialized
     ;; dx = same here
     
     ;; unknown values could cause unexpected results ;)
     
     ;; Inside LBA_to_CHS You push ax / push bx / mov bl, dl / push bx
     ;; and before return from LBA_to_CHS You pop those values
     ;; So after procedure:
     
     ;; ax = popped value 0x07c0, then al = _kernel_size = 1, ah = 0x07 changed inside read_disk to 0x02
     ;; bx = unknown value but set later to 0x0200
     ;; cx = ch - cylinder, cl - sector
     ;; dx = dh - head, dl - popped value, are you sure this is drive number (0 = A:)
     
    mov si, 1               ;LBA = si = 1
    call LBA_to_CHS ; after this call, CH, CL, and DH will have cylinder, sector, and head values
    
    mov bx, 0x200
    mov al, [_kernel_size] ; number of sectors to read
    call read_disk
    
    jmp 0:7e00h

; Arguments:
;     si - Points to the string to be printed
; Returns:
;     None
puts:
    push ax
.begin:
    
    mov ah, 0x0E
    lodsb
    
    cmp al, 0
    je .done
    
    int 10h
    jmp .begin
.done:

    pop ax
    
    ret

; Arguments:
;     si - LBA adress 
; Returns:
;     ch, dh, cl - CHS adress
;; dx and cx will have to be modified
LBA_to_CHS:

    push ax                           ; push ax and push bx won't destroy any necessary data 
    push bx                             

    mov bl, dl                        ; dl = trash values

    push bx                           ; delete this too
    
    ; C = LBA ÷ (HPC × SPT)
    mov ax, [_head_count]               ; ax = 2  
    mul word [_sectors_per_track]         ; ax = 2 * 18 = 36

    mov bx, ax                        ; bx = 36 
    mov ax, si                        ; ax = 1  
    xor dx,dx                         ; dx = 0
;(* 2 *)
; here You have mul word bx so I changed this to div word bx
; we divide LBA mod (Heads * Sectors) not mul
    ;mul word bx
    ;should be div word bx
    div word bx                       ; ax = 0 dx = 1
    mov ch, al                        ; Put lower bits of ax as the cylinder address
;(* 3 *)    
    ; I used xor dx, dx before div instructions because in 16-bit division dividend is in dx:ax
    ; so we have to clear dx or our result maybe wrong value
    ; H = (LBA ÷ SPT) mod HPC
    xor dx, dx                        ; dx = 0            
    mov ax, si                        ; ax = 1
    div word [_sectors_per_track]     ; 18 ; result in dx:ax, ax = 0 dx = 1
    xor dx, dx
    div word [_head_count]            ; 2  ; result also in dx:ax (dx is remainder) ax = 0 dx = 0
    mov dh, dl                        ; since dx is composed of dh (higher bits) and dl (lower bits) we want to move the lower bits into dh 
    
    push dx                           ; save dx
    ; S = (LBA mod SPT) + 1
    xor dx, dx                        ; dx = 0   
    mov ax, si                        ; ax = 1
    div word [_sectors_per_track]     ; 18 ; remainder in dx, ax = 0 dx = 1 
    inc dx                            ; dx = 2

    mov cl, dl      

    pop dx  

    pop bx                            ; delete
    mov dl, bl                         ; dl = trash
    pop bx                            ; delete
    pop ax                            ; delete
 
    ret


; Arguments:
;     bx - Address to load the data
;     ch, cl, dh - CHS values
; Returns:
;     None

read_disk:
    push ax
    push si
    
    mov si, 4
    
.retry:
    dec si

    cmp si, 0
    je .read_error
    
    mov ah, 02h
    mov al, [_kernel_size]

    int 13h

    jc .retry

    pop si
    pop ax
    
    ret 

.read_error:
    
    mov si, _message_read_err
    call puts

    cli
    hlt

times 510-($-$$) db 0

dw 0aa55h

Kernel.asm

[org 0x7e00]
[bits 16]

jmp main

_message: db 'Hello from kernel!', 0

main:
    mov si, _message
    call puts

    cli
    hlt

puts:
    mov ah, 0x0E
.begin:
    lodsb 
    cmp al, 0
    je .done
    int 10h
    jmp .begin
.done:
    ret
    
    
Nassau
  • 377
  • 1
  • 1
  • 8
  • Oh wait... Missing message "Booting SSoS..."... need to fix something ;) – Nassau Aug 11 '23 at 08:36
  • 1
    This is a completely different program that does not fix the problems the question's approach had. I don't think this even qualifies as an answer. Besides, if you think you need `.begin1` and `.done1` when you already changed `puts` to `puts1`, then you demonstrate you do not know how NASM local labels work. – ecm Aug 11 '23 at 08:39
  • Bootloader is loaded under `0:7c00`, and we have `jmp far [es:bx]`, `es = 7c00` and `bx = 200h` but this is wrong instruction as You already mentioned. Jmp should to offset `0:7e00` where kernel code is present I assume. – Nassau Aug 11 '23 at 09:03
  • `es` = 7C0h actually (one zero, not two zeroes) and I agree with the rest of your comment, but your answer does not address any of that. – ecm Aug 11 '23 at 09:21
  • Ok. This is real mode code so `es = 07c0` and `bx = 0200` and using `16 * segment + offset` result will be `7e00`. – Nassau Aug 11 '23 at 09:28
  • Still agree with that last comment but that does not improve the answer. – ecm Aug 11 '23 at 09:29
  • I agree with @ecm. – ThatGuy. exe Aug 11 '23 at 09:53
  • Answer updated. – Nassau Aug 12 '23 at 07:42
  • I haven't read through your whole code yet, but the first red flag I see is the `;%include "kernel.asm"`. The bootloader can only be _512 bytes_ (max) in size because it gets loaded by the BIOS. If the *kernel + bootloader* were more than 512 bytes in size (which it will surpass soon), the (whole) code wouldn't execute (because the signature was overwritten by the assembly code). Additionaly it isn't a true kernel when it's a part of the bootloader. – ThatGuy. exe Aug 12 '23 at 08:26
  • 1
    Line `;%include "kernel.asm"` is commented out. I tried few things at the beginning, to see what is what and this line just stayed there. I used Your makefile to create dev.img. I checked what this code does. Mbr code is executed then we have `read_disk`, which starts reading from 0 0 1 + additional sectors. Result is `mbr 0 0 1` and `mbr+kernel starts at 0 0 2`. Then code jumps to first byte after signature `55aa`which should be kernel but it's mbr. See my images. So maybe this variable should be `kernel_start_LB = 2`. Kernel will be where it should be`7e00` and everything should work fine. – Nassau Aug 12 '23 at 08:57
  • Hey @0Signal, this looks very promising. However, could you update the anwser with my new bootloader code (because I updated the `LBA_to_CHS` function and fixed some issues). I `can't try your code now` because I don't have time however I'll test it next week if you rewrite the code. Thaks a lot! – ThatGuy. exe Aug 12 '23 at 10:16
  • Ok, all done. See Update. – Nassau Aug 12 '23 at 11:31
  • 1
    Much better now, I removed my downvote. – ecm Aug 12 '23 at 13:19
  • 1
    Your answer does still contain lots of obsolete parts though, you should clean those up. – ecm Aug 12 '23 at 13:22
  • Hey, @0Signal your anwser seems valid but I still don't understand which part you changed in my code (only the `LBA_to_CHS function`?). From what I understand the `LBA_to_CHS function` was the only wrong part in my code. – ThatGuy. exe Aug 19 '23 at 14:06
  • @0Signal your comments are also a bit unclear. Wdym with `; dl = trash` and `; delete`? – ThatGuy. exe Aug 19 '23 at 14:14
  • @0Signal I tried it and it works. If you respond to my previous comments and (if you can) explain what the issue was in my code (from what I see different in your code, it's the `LBA_to_CHS` function), `I'll accept your anwser`. Thanks a lot! – ThatGuy. exe Aug 19 '23 at 14:19
  • Yes ,I modified only `LBA_to_CHS`. I try to explain this in comments `(* 1 *)`, `(* 2 *)`, `(* 3 *)`. See `bootloader.asm` code. I hope this will clarify a bit. – Nassau Aug 19 '23 at 15:34
  • Preserving `dl` is intentional. The prior loader will initialise it to the int 13h disk unit number that we're being loaded from. Usually 00h for first diskette, 80h for first hard disk, but can be 01h or 81h or other values. – ecm Aug 19 '23 at 16:54