Observations of your code:
- To disable paging clear bit 31 of CR0. It will be the kernel's responsibility to properly identity map the pages holding the GDT and the pages containing the instructions where the code transitions from protected mode to real mode.
- The
use16
should be after jmp far 0x20:entry16
and before the entry16
label. You have placed it too early in the code.
- The
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.
- You hard code the SP to 16000. You should be saving the original SS:SP and restoring them to their previous values after returning back to real mode. Address 16000 could be in the middle of memory used by DOS. This could lead to corruption.
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: