I am trying to learn some x86 assembly. I have successfully created a MBR with a bootloader, loaded another sector, switched to Protected Mode and performed a far jump into the loaded sector.
I am using NASM on a 64-bit Windows installation and assembling with nasm -s -f bin bootloader.asm -o test.img
. I load the test.img
file into a VirtualBox VM as a floppy.
I have written a subroutine (subroutine definition in code section) that uses lodsb
to load characters from ESI which I have previously pointed to a label using mov esi, _LOW_KERNEL_MESSAGES_TEST
(where _LOW_KERNEL_MESSAGES_TEST
is a label defined to db "Hello, World!", 0
). However, this does not print anything.
Neither does mov al, [_LOW_KERNEL_MESSAGES_TEST]
which was expected to print the first character 'H' (character printing subroutine takes AL
as character input)
Directly moving a character into AL
by mov al, 't'
and then calling the one character printing subroutine works fine, I have also tested the offset and successfully printed the word 'Test', the problem lies only in the "move label into esi and then load it into al" part of the printing
mov al, [_LOW_KERNEL_MESSAGES_TEST]
]popad
instead of popa
(thought the AX
register was being modified?)As per the request of the kind commenters below, I have attempted to create a minimal reproducible example, code for which can be found here:
This can further be assembled by using:
nasm -s -f bin <name of the code file here>.asm -o floppy.img
By binding this floppy.img file to a floppy controller in a VirtualBox 6.1 VM
and running it, a completely black screen appeared with a white cursor in the top left corner and the VM stalled, with a "critical error has occured" message, the error log does not make much sense to me but might be helpful, it can be found at https://pastebin.com/g1C2xf7V
My files:
bootloader.asm:
%define KERNEL_LOAD_POSITION 0x1000
[bits 16]
[org 0x0600]
;***********************************************************;
; ENTRY SECTION ;
;***********************************************************;
__ENTRY: ; ENTRY POINT
cli ; Clear Interrupts
xor ax, ax ; Zero out AX (set to 0)
mov ds, ax ; Set Data Segment to 0 (AX)
mov es, ax ; Set Extra Segment to 0 (AX)
mov ss, ax ; Set Stack Segment to 0 (AX)
mov sp, ax ; Set Stack Pointer to 0 (AX)
__SET__LOWER__ENTRY:
mov cx, 0x0100 ; 256 WORDs in MBR
mov si, 0x7C00 ; Current MBR Address
mov di, 0x0600 ; New MBR Address
rep movsw ; Copy MBR
jmp 0:__LOWER_ENTRY ; Jump to new Address
__LOWER_ENTRY:
sti
mov al, 1
call _BOOTLOADER_READSECTORS
call _BOOTLOADER_ENTER_PROTECTED
;***********************************************************;
; FUNCTION SECTION ;
;***********************************************************;
_BOOTLOADER_READSECTORS:
pusha
mov ah, 02h
mov ch, 0x0
mov cl, 0x02
mov dh, 0x0
mov dl, 0x0
mov bx, KERNEL_LOAD_POSITION
int 13h
jc _BOOTLOADER_HANG
popa
ret
_BOOTLOADER_HANG:
jmp $
ret
_BOOTLOADER_ENTER_PROTECTED:
cli
lgdt [GDTR]
pusha
mov eax, cr0
or al, 1
mov cr0, eax
popa
jmp 0x08:_BOOTLOADER_PERFORM_PROTECTED_FAR_JUMP
[bits 32]
_BOOTLOADER_PERFORM_PROTECTED_FAR_JUMP:
mov ax, 0x10
mov ds, ax
mov ss, ax
mov fs, ax
mov es, ax
mov gs, ax
jmp KERNEL_LOAD_POSITION
;***********************************************************;
; DATA SECTION ;
;***********************************************************;
;***********************************************************;
; GDT SECTION ;
;***********************************************************;
GDT:
GDT_NULL_DESC:
dd 0 ; null descriptor
dd 0
GDT_CODE_DESC:
dw 0xFFFF ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
GDT_DATA_DESC:
dw 0xFFFF ; data descriptor
dw 0 ; limit low
db 0 ; base low
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
GDTR:
Limit dw 24 ; length of GDT
Base dd GDT_NULL_DESC ; base of GDT
;***********************************************************;
; PADDING ;
;***********************************************************;
times (440 - ($-$$)) db 0 ; Pad For MBR Partition Table
;***********************************************************;
; NON-CODE SECTION ;
;***********************************************************;
UID: times 4 db 0 ; Unique Disk ID
Reserved: times 2 db 0 ; Reserved, either 0x0000 (normal) or 0x5a5a (readonly)
;***********************************************************;
; PARTITION TABLE SECTION ;
;***********************************************************;
PT1: ; First Partition Entry ( KERNEL HERE )
db 0x80 ; Bootable ( Active ) ( 8 bits )
db 0x00 ; Starting Head ( Start at head 0 ) ( 8 bits )
db 0b00000010 ; Starting Sector ( Start at Sector 2 ) ( 6 bits ) NOTE: 2 LSB are for the following cylinder field
db 0b00000000 ; Starting Cylinder ( Start at Cylinder 0 ) ( 10 bits )
db 0x21 ; System ID ( Filysystem ) ( 21, Reserved )
db 0x00 ; Ending Head ( End at head 0 ) ( 8 bits )
db 0b00000100 ; Ending Sector ( End at Sector 4 ) ( 6 bits ) NOTE: 2 LSB are for the following cylinder field
db 0b00000000 ; Ending Cylinder ( End at Cylinder 0 ) ( 10 bits )
db 0x00 ; | Relative sector ( 32 bits )
db 0x00 ; |
db 0x00 ; |
db 0x01 ; |
db 0x00 ; | Total sectors in partition ( 32 bits )
db 0x00 ; |
db 0x00 ; |
db 0x02 ; |
PT2: times 16 db 0 ; Second Partition Entry
PT3: times 16 db 0 ; Third Partition Entry
PT4: times 16 db 0 ; Fourth Partition Entry
dw 0xAA55 ; Magic Word
%define INCLUDE_OFFSET 512
%include "low_kernel.asm"
low_kernel.asm:
[bits 32]
jmp __KERNEL__ENTRY ; JUMP TO KERNEL ENTRY POINT
;***********************************************************;
; MACRO/DEFINE SECTION ;
;***********************************************************;
%define _LOW_KERNEL_VIDEO_MEMORY_ADDRESS 0xb8000
%define SECTOR_OFFSET INCLUDE_OFFSET
%macro PAD_SECTOR 0
times (512 - ($-$$) + SECTOR_OFFSET) db 0 ; Pad out sector
%define SECTOR_OFFSET SECTOR_OFFSET+512
%endmacro
;***********************************************************;
; KERNEL SECTION ;
;***********************************************************;
__KERNEL__ENTRY:
mov esi, _LOW_KERNEL_MESSAGES_TEST
mov ah, 0x0F
call _LOW_KERNEL_PRINT
call _LOW_KERNEL_HANGKERNEL
;; LOW KERNEL SUBROUTINE
; Desc: Prints one character to the designated offset
;
; _IN_ AL ( CHARACTER )
; _IN_ AH ( COLOR )
; _IN_ ECX ( VIDEO MEMORY OFFSET )
_LOW_KERNEL_CHPRINT:
pushad
mov ebx, _LOW_KERNEL_VIDEO_MEMORY_ADDRESS
add ebx, ecx
mov [ebx], eax
popad
ret
;; LOW KERNEL SUBROUTINE
; Desc: Prints strings from ESI
;
; _IN_ AH ( STRING COLOR )
; _IN_ ESI ( STRING ADDRESS )
; _IN_ ECX ( STARTING VIDEO MEMORY OFFSET )
_LOW_KERNEL_PRINT:
pushad
.internal_loop:
lodsb
cmp al, 0
je .internal_end
call _LOW_KERNEL_CHPRINT
add ecx, 0x02
jmp .internal_loop
.internal_end:
mov ah, 0x0F
mov al, 'T'
call _LOW_KERNEL_CHPRINT
popad
ret
_LOW_KERNEL_HANGKERNEL:
jmp $
ret
;***********************************************************;
; KERNEL DATA SECTION ;
;***********************************************************;
_LOW_KERNEL_MESSAGES_TEST:
db "Hello, World!", 0
PAD_SECTOR
those are assembled by nasm -s -f bin bootloader.asm -o test.img
, the .img result of which is then also bound as a floppy into a VM, by running it I got a completely black screen with no cursor
The GDT and far jump were scrambled together from what I found online, and I therefore suspect my low understanding of them the root of my problem
Please take into consideration that this is my first question on StackOverflow and I lack the understanding on how to format the question so that it's readable and everyone understands it. You're more than welcome to post some criticism in the comments so I can make better questions in the future.
You load the kernel (LBA-numbered sector 1 and following) to KERNEL_LOAD_POSITION
which is 1000h (4 KiB). However, your kernel immediately follows the boot sector loader in your assembly, so its labels are evaluated as if the kernel was to be placed at 600h + 512 = 800h (2 KiB). The call
instructions are not affected by this bug because they are relative, that is, position-independent. You can set the load position to 800h.
A different way to solve this is by using NASM's multi-section bin format. This is what that would look like:
org 600h
section LOADER start=600h
; loader here
times 510 - ($ - $$) db 0 ; pad to where sector's signature goes
dw 0AA55h ; sector signature
section KERNEL follows=LOADER vstart=KERNEL_LOAD_POSITION
; kernel here
This should work even with the load position set to 1000h.