I learned to switch to protected mode with a simple DOS based bootloader. This loader loads kernel.bin
into a buffer and copies the buffer to 100000h (kernel is
Observations of your code:
use16
should be after jmp far 0x20:entry16
and before the entry16
label. You have placed it too early in the code. entry16
label has to be fixed up (converted to a linear address) just like pmode_entry
. jmp dword 0:rmode
uses a hard coded real mode segment of 0. This will cause problems since DOS likely didn't load your program at CS=0x0000. You need to store the original CS segment register before entering protected mode so you can use it to perform a FAR JMP (or equivalent) with the proper real mode segment.This code is an adaptation of some code I wrote for a question posed on the #OSDev IRC channel. It incorporates your idea of loading kernel.bin
using DOS int 21h and copying it to memory beyond 0x100000. I put the kernel at 0x110000 in the event that DOS is using the memory between 0x100000 and 0x10FFEF as the DOS High Memory Area (HMA).
; Assemble to pmdos.exe using FASM with:
; fasm pmdos.asm
format MZ
STACK32_TOP EQU 0x200000
KERNEL_START EQU 0x110000
KERNEL_SIZE EQU 8192
use16
main:
push cs
pop ds ; DS=CS
; Read KERNEL.BIN into 8kb buffer
mov ah, 0x3d
mov dx, kernel_file
int 0x21 ; Open KERNEL.BIN
mov bx, ax ; BX = file handle
jnc .read ; No Error? Read file
mov dx, file_not_fnd ; Print an error and exit back to DOS
mov ah, 0x9
int 0x21 ; Print Error
jmp exit
.read:
mov ah, 0x3f ; Read file
mov cx, KERNEL_SIZE ; Maximum 8192 bytes
mov dx, kernel_mem ; Buffer to read into
int 0x21
jnc .close ; No Error? Finished, close file
mov dx, file_read_err ; Print an error and exit back to DOS
mov ah, 0x9
int 0x21 ; Print Error
jmp exit
.close:
mov ah, 0x3e ; Close file
int 0x21
call check_pmode ; Check if we are already in protected mode
; This may be the case if we are in a VM8086 task.
; EMM386 and other expanded memory manager often
; run DOS in a VM8086 task. DOS extenders will have
; the same effect
jz not_prot_mode ; If not in protected mode proceed to switch
mov dx, in_pmode_str ; otherwise print an error and exit back to DOS
mov ah, 0x9
int 0x21 ; Print Error
jmp exit ; Exit program
not_prot_mode:
call a20_on ; Enable A20 gate (uses Fast method as proof of concept)
cli ; Disable interrupts
; Compute linear address of label gdt_start
; Using (segment SHL 4) + offset
mov eax, cs ; EAX = CS
shl eax, 4 ; EAX = (CS << 4)
mov ebx, eax ; Make a copy of (CS << 4)
add [gdtr+2], eax ; Add base linear address to gdt_start address
; in the gdtr
lgdt [gdtr] ; Load gdt
; Compute linear address of labels using (segment << 4) + offset.
; EBX already is (segment << 4). Add it to the offsets of the labels to
; convert them to linear addresses
lea edi, [pm16_entry+ebx] ; EDI = 16-bit protected mode entry (linear address)
lea esi, [kernel_mem+ebx] ; ESI = Kernel memory buffer (linear address)
add ebx, code_32bit ; EBX = code_32bit (linear address)
push ds ; Save real mode segments on real mode stack
push es
push fs
push gs
mov ecx, cs ; ECX = DOS real mode code segment
push dword CODESEL32 ; CS = 32-bit PM code selector
push ebx ; Linear offset of code_32bit
mov bp, sp ; m16:32 address on top of stack, point BP to it
mov eax, cr0
or eax, 1
mov cr0, eax ; Set protected mode flag
; We are in quasi 16-bit protected mode at this point
; JMP to 32-bit protected mode
jmp far fword [bp] ; Indirect m16:32 FAR jmp with
; m16:32 constructed at top of stack
; DWORD allows us to use a 32-bit offset in 16-bit code
; 16-bit functions that run in real mode
; Check if protected mode is enabled, effectively checking if we are
; in in a VM8086 task. Set ZF to 1 if in protected mode
check_pmode:
smsw ax
test ax, 0x1
ret
; Enable a20 (fast method). This may not work on all hardware
a20_on:
cli
in al, 0x92 ; Read System Control Port A
test al, 0x02 ; Test current a20 value (bit 1)
jnz .skipfa20 ; If already 1 skip a20 enable
or al, 0x02 ; Set a20 bit (bit 1) to 1
and al, 0xfe ; Always write a zero to bit 0 to avoid
; a fast reset into real mode
out 0x92, al ; Enable a20
.skipfa20:
sti
ret
; 16-bit protected mode entry point and code
pm16_entry:
mov ax, DATASEL16 ; Set all data segments to 16-bit data selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0 ; Disable protected mode
and eax, NOT 0x80000001 ; Disable paging (bit 31) and protected mode (bit 0)
; The kernel will have to make sure the GDT is in
; a 1:1 (identity mapped page) as well as lower memory
; where the DOS program resides before returning
; to us with a RETF
mov cr0, eax
push cx ; Return to the real_mode code
push real_mode_entry ; with the original CS value (in CX)
retf
; 16-bit real mode entry point
real_mode_entry:
xor esp, esp ; Clear all bits in ESP
mov ss, dx ; Restore the real mode stack segment
lea sp, [bp+8] ; Restore real mode SP
; (+8 to skip over 32-bit entry point and Selector that
; was pushed on the stack in real mode)
pop gs ; Restore the rest of the real mode data segment
pop fs
pop es
pop ds
lidt [idtr] ; Restore the real mode interrupt table
sti ; Enable interrupts
exit:
mov ax, 0x4c00 ; Return to DOS with value 0
int 0x21
; Code that will run in 32-bit protected mode
;
; Upon entry the registers contain:
; EDI = 16-bit protected mode entry (linear address)
; ESI = Kernel memory buffer (linear address)
; EBX = code_32bit (linear address)
; ECX = DOS real mode code segment
align 4
use32
code_32bit:
mov ebp, esp ; Get current SS:ESP
mov edx, ss
cld ; Direction flag forward
mov eax, DATASEL32 ; Set protected mode selectors to 32-bit 4gb flat
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, STACK32_TOP ; Should set ESP to a usable memory location
; Stack will be grow down from this location
; Build linear address and selector on stack for RETF to return to
push CODESEL16 ; Put 16-bit protected mode far entry point 0x18:pm16_entry
push edi ; on stack as a return address (linear address)
push edx ; Save EDX, EBP, ECX
push ebp
push ecx
mov edi, KERNEL_START ; EDI = linear address where PM code will be copied
mov ecx, KERNEL_SIZE ; ECX = number of bytes to copy
rep movsb ; Copy all code/data from kernel buffer to KERNEL_START
call CODESEL32:KERNEL_START ; Absolute jump to relocated code
pop ecx ; ECX = Real mode code segment
pop ebp ; Recover old SS:SP into EDX:EBP
pop edx
retf ; Switch to 16-bit protected mode, goto pm16_entry
kernel_file: db "kernel.bin", 0
file_not_fnd: db "KERNEL.BIN not found - exiting", 0x0a, 0x0d, "$"
file_read_err: db "Can't read KERNEL.BIN - exiting", 0x0a, 0x0d, "$"
in_pmode_str: db "Processor already in protected mode - exiting", 0x0a, 0x0d, "$"
align 4
; Real mode interrupt vector table (IDTR)
idtr:
dw 0x3ff
dd 0
align 4
gdtr:
dw gdt_end-gdt_start-1
dd gdt_start
align 4
gdt_start:
; First entry is always the Null Descriptor
dd 0
dd 0
gdt_code:
; 32-bit 4gb flat r/w/executable code descriptor
dw 0xFFFF ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt_data:
; 32-bit 4gb flat r/w data descriptor
dw 0xFFFF ; limit low
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt16_code:
; 16-bit 4gb flat r/w/executable code descriptor
dw 0xFFFF ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 10001111b ; granularity
db 0 ; base high
gdt16_data:
; 16-bit 4gb flat r/w data descriptor
dw 0xFFFF ; limit low
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 10001111b ; granularity
db 0 ; base high
gdt_end:
CODESEL32 = gdt_code - gdt_start
DATASEL32 = gdt_data - gdt_start
CODESEL16 = gdt16_code - gdt_start
DATASEL16 = gdt16_data - gdt_start
kernel_mem: TIMES KERNEL_SIZE db 0x0
In order for the kernel to return when finished you must use an RETF
(Far return) instruction. A sample kernel.asm
file that prints some letters on the display and returns could look something like:
; Assemble to kernel.bin using FASM with:
; fasm kernel.asm
format binary
VIDEO_MEM EQU 0x0b8000
org 0x110000 ; Set ORG to address kernel is loaded at
use32
kernel_entry:
; Write MDP in white on magenta starting on second row, column 0
mov eax, 0x5f SHL 8 OR 'M'
mov [VIDEO_MEM+80*2], ax
mov eax, 0x5f SHL 8 OR 'D'
mov [VIDEO_MEM+80*2+2], ax
mov eax, 0x5f SHL 8 OR 'P'
mov [VIDEO_MEM+80*2+4], ax
retf ; Exit with a FAR return
Assemble with:
fasm pmdos.asm
fasm kernel.asm
to produce pmdos.exe
and kernel.bin
. The output when run in DOSBox should look similar to: