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.

15
  • 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). Commented Aug 10, 2023 at 14:41
  • 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 Commented Aug 10, 2023 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. Commented Aug 10, 2023 at 14:56
  • 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. Commented Aug 10, 2023 at 16:00
  • 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: stackoverflow.com/a/45495410/3857942 Commented Aug 10, 2023 at 22:19

1 Answer 1

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
    
    
Sign up to request clarification or add additional context in comments.

17 Comments

Oh wait... Missing message "Booting SSoS..."... need to fix something ;)
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.
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 55aawhich 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 be7e00 and everything should work fine.
Much better now, I removed my downvote.
Your answer does still contain lots of obsolete parts though, you should clean those up.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.