Switch from protected mode to real mode in a DOS EXE program

后端 未结 1 1518
执念已碎
执念已碎 2021-01-18 11:05

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

1条回答
  •  小鲜肉
    小鲜肉 (楼主)
    2021-01-18 11:59

    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:

    0 讨论(0)
提交回复
热议问题