Войдите в защищенный режим от проблемы загрузчика - PullRequest
0 голосов
/ 06 января 2019

этот код сборки входит в защищенный режим из загрузчика, но не может сбросить сегмент CS (или выполнить дальний переход) после вызова дальнего перехода и перезагрузки. Если я удаляю дальний прыжок, он без проблем переходит в бесконечный цикл (0x66, jmp $) в защищенном режиме, без перезагрузки.

[bits 16]
[org 0x7c00]
xor ax,ax
xor eax,eax
add eax,ENTRY_POINT_32 ;address to plug to far jmp
mov [ENTRY_OFF],eax
xor eax,eax
mov eax,GDT                ;load GDT label address
mov [GDTR+2],eax ; load it into address space in GDTR
lgdt [GDTR]                   ;load GDTR
cli                                    ;turn off masked interrupts
in al,0x70
or al,0x80
out 0x70,al                     ;turn off nonmasked interrupts
in al,0x92
or al,2
out 0x92, al ;open line A20 (change address 20 to 32 bits)
mov eax,cr0
or al,1
mov cr0,eax                 ;switch to protected mode
db 0x66                        ;prefix of opcode to change bitness
db 0xEA                       ;opcode of jmp far
ENTRY_OFF dd 0x0 ;32 bit offset of 32 bit instructions
dw 00001000b ; selector 1st descriptor CODE_descr,=1
ENTRY_POINT_32:
db 0x66                      ;prefix of opcode to change bitness
jmp $                          ;infinite jump to the same location
GDT:
NULL_descr dd 0x0,0x0 ; must be present in GDT
CODE_descr db  0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0
;descriptor of 32 bit code segment, base 0, size ffffffff
DATA_descr db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0
;descriptor of 32 bit data segment, base 0, size ffffffff
VIDEO_descr 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
;descriptor of video buffer, base 0x000B8000, size ffff
GDT_size db $-GDT ;size of GDT table
GDTR dw GDT_size-1 ;next 3 words are size &
dd 0x0 ;address of beginning of GDT, loaded in code
times 510 - ($ - $$) db 0
dw 0xaa55

Оригинальный код от wasm.in, с небольшими изменениями.

1 Ответ

0 голосов
/ 07 января 2019

В реальном режиме подразумевается сегмент на всех операндах памяти. Если операнд памяти не содержит BP в качестве базы, то подразумеваемый сегмент равен DS . Если операнд памяти содержит BP, подразумеваемое основание - SS. Ваши операнды памяти не используют BP , поэтому подразумеваемый сегмент равен DS . Инструкции с операндом памяти:

mov [ENTRY_POINT_32],eax

эквивалентны:

mov [ds:ENTRY_POINT_32],eax

В реальном режиме используется сегмент : адресация смещения для получения адреса физической памяти. Если DS не так, вы будете записывать в неправильную ячейку памяти. 20-битный физический адрес = (сегмент << 4) + смещение. </p>

При этом при запуске загрузчика вы не можете полагаться на сегмент и регистры общего назначения, которые являются ожидаемыми значениями, за исключением DL , который содержит загрузочный диск, переданный BIOS. Вы можете прочитать мои Советы по загрузке для получения дополнительной информации о разработке загрузчика.

Вам необходимо явно установить регистр DS . Поскольку ваш код использует org 0x7c00, вам нужен сегмент DS , установленный на ноль. (0x0000 << 4) + 0x7c00 = 0x07c00 (физический адрес). Загрузчик всегда загружается BIOS на физический адрес 0x07c00. </p>

У вас также есть эти две строки:

xor ax,ax
xor eax,eax

Первое не нужно, поскольку вы устанавливаете все EAX на ноль с последним. Следующая строка не нужна, если вы используете директиву bits 32 NASM перед 32-битным кодом:

db 0x66                      ;prefix of opcode to change bitness

GDTR также настроен неправильно. Вы неправильно рассчитали размер. У вас есть этот код:

GDT_size db $-GDT ;size of GDT table
GDTR dw GDT_size-1 ;next 3 words are size &

Вы создаете ячейку памяти с байтом, содержащим размер GDT. GDTR dw GDT_size-1 берет смещение метки GDT_size и вычитает одну из нее. Это работает только потому, что смещение метки GDT_size больше, чем размер GDT. Вы можете сделать что-то вроде:

GDT:
    NULL_descr: dd 0x0,0x0      ; must be first entry in GDT

    ; descriptor of 32 bit code segment, base 0, size ffffffff
    CODE_descr: db  0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0

    ; descriptor of 32 bit data segment, base 0, size ffffffff
    DATA_descr: db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0

    ; descriptor of video buffer, base 0x000B8000, size ffff
    VIDEO_descr: db 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
GDT_END:

GDTR dw GDT_END-GDT-1            ; Size of GDT (minus 1)
     dd 0x0                      ; address of beginning of GDT, loaded in code

При создании самоизменяющегося кода вам также нужно позаботиться о очистке очереди предварительной выборки команд , чтобы процессор увидел изменения в коде. Возможно, процессор уже предварительно прочитал инструкцию FAR JMP, которую вы изменяете, и не знает об изменениях, внесенных вами в код. Это можно исправить, просто вставив JMP в код после изменения инструкции. После обновления инструкции с вычисленным адресом вы можете сделать что-то вроде:

    mov [ENTRY_OFF],eax
    jmp clear_prefetch          ; Clear the instruction prefetch queue
                                ;     by jumping to next instruction
clear_prefetch:

Рабочий код (я немного очистил форматирование) может выглядеть так:

bits 16
org 0x7c00

start:
    xor eax,eax
    mov ds, ax                  ; Explicitly set DS to zero

    add eax,ENTRY_POINT_32      ; address to plug to far jmp
    mov [ENTRY_OFF],eax
    jmp clear_prefetch          ; Clear the instruction prefetch queue
                                ;     by jumping to next instruction
clear_prefetch:

    xor eax,eax
    mov eax,GDT                 ; load GDT label address
    mov [GDTR+2],eax            ; load it into address space in GDTR
    lgdt [GDTR]                 ; load GDTR

    cli                         ; turn off masked interrupts
    in al,0x70
    or al,0x80
    out 0x70,al                 ; turn off nonmasked interrupts
    in al,0x92
    or al,2
    out 0x92, al                ; enable A20 line
    mov eax,cr0
    or al,1
    mov cr0,eax                 ; switch to protected mode

    db 0x66                     ; prefix of opcode to change bitness
    db 0xEA                     ; opcode of jmp far
ENTRY_OFF:
    dd 0x0                      ; 32 bit offset of 32 bit instructions
    dw 00001000b                ; selector 1st descriptor CODE_descr,=1

bits 32
ENTRY_POINT_32:
    jmp $                       ; infinite jump to the same location

GDT:
    NULL_descr: dd 0x0,0x0      ; must be first entry in GDT

    ; descriptor of 32 bit code segment, base 0, size ffffffff
    CODE_descr: db  0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0

    ; descriptor of 32 bit data segment, base 0, size ffffffff
    DATA_descr: db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0

    ; descriptor of video buffer, base 0x000B8000, size ffff
    VIDEO_descr: db 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
GDT_END:

GDTR dw GDT_END-GDT-1            ; Size of GDT (minus 1)
     dd 0x0                      ; address of beginning of GDT, loaded in code

times 510 - ($ - $$) db 0
dw 0xaa55

Нет необходимости во время выполнения вычислений FAR JMP в загрузчике

Ваш код слишком сложен для этой ситуации. Устаревшие BIOS на x86 всегда загружают загрузчик по физическому адресу 0x07c00. Преимущество использования ORG 0x7c00 и установки сегментов в 0x0000 заключается в том, что 0x0000: 0x7c00 и линейный адрес (такой же, как физический адрес в реальном режиме) имеют одинаковое смещение 0x07c00 от начала памяти. Вы можете использовать это в своих интересах и избежать ненужных вычислений во время выполнения. Код может выглядеть так:

bits 16
org 0x7c00

start:
    xor ax,ax
    mov ds,ax                   ; Explicitly set DS to zero

    lgdt [GDTR]                 ; load GDTR

    cli                         ; turn off masked interrupts
    in al,0x70
    or al,0x80
    out 0x70,al                 ; turn off nonmasked interrupts
    in al,0x92
    or al,2
    out 0x92, al                ; enable A20 line

    ; Enter protected mode
    mov eax,cr0
    or al,1
    mov cr0,eax                 ; switch to protected mode
    jmp CODE32_SEL:ENTRY_POINT_32

bits 32
ENTRY_POINT_32:
    mov eax, DATA32_SEL         ; Set the protected mode selector
    mov ds, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x9C000            ; Set protected mode stack below EBDA

    mov eax, VIDEO32_SEL        ; Set the video memory selector
    mov es, ax

    ; Print some characters to top left of the screen in white on magenta
    xor ebx, ebx
    mov word [es:ebx],   0x57 << 8 | 'M'
    mov word [es:ebx+2], 0x57 << 8 | 'D'
    mov word [es:ebx+4], 0x57 << 8 | 'P'

    jmp $                       ; infinite jump to the same location

GDT:
    NULL_descr: dd 0x0,0x0      ; must be first entry in GDT

    ; descriptor of 32 bit code segment, base 0, size ffffffff
    CODE_descr: db  0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0

    ; descriptor of 32 bit data segment, base 0, size ffffffff
    DATA_descr: db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0

    VIDEO_descr: db 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
    ; descriptor of video buffer, base 0x000B8000, size ffff
GDT_END:
CODE32_SEL  equ CODE_descr-GDT
DATA32_SEL  equ DATA_descr-GDT
VIDEO32_SEL equ VIDEO_descr-GDT

GDTR dw GDT_END-GDT-1            ; Size of GDT (minus 1)
     dd GDT                      ; address of beginning of GDT

times 510 - ($ - $$) db 0
dw 0xaa55

Этот код вычисляет селекторы CODE и DATA во время сборки. Он также вычисляет GDTR во время сборки и жестко кодирует FAR JMP. Следует отметить, что, поскольку загрузчик и 32-разрядная точка входа полностью находятся внутри первых 64 КБ памяти, вы можете использовать 16-разрядное смещение, а не 32-разрядное в FAR JMP для защищенного режима. Нет необходимости самостоятельно модифицировать код.

Примечание : Создание селектора для видеопамяти не требуется. Вы всегда можете обратиться к этой памяти, используя 32-битный 4GiB селектор данных.


Когда использовать код, который вычисляет адреса во время выполнения?

Концепция создания FAR JMP и генерации записи GDTR во время выполнения не совсем бесполезна. В средах, где код может быть помещен в память в разных сегментах, вам потребуется вычислить FAR JMP и линейный адрес GDT для GDTR во время выполнения. Это будет иметь место, если вы пытаетесь войти в защищенный режим из DOS через программу COM или EXE. Загрузчик DOS решает, в какой сегмент помещать вещи. В этом случае вам придется вычислять адреса во время выполнения. Я написал код пару лет назад для кого-то из IRC, который делает именно это. Мой код не отключает NMI (он должен), и он не изменяет FAR JMP. Я создаю адрес FAR JMP в стеке, а затем выполняю непрямой FAR JMP через адрес в стеке. Принцип такой же, как и в случае самоизменяющегося кода.

Пример программы COM для DOS, которая во время выполнения генерирует адрес для FAR JMP в стеке и генерирует адрес GDT в GDTR:

; Assemble with NASM as
;     nasm -f bin enterpm.asm -o enterpm.com

STACK32_TOP EQU 0x200000
CODE32_REL  EQU 0x110000
VIDEOMEM    EQU 0x0b8000

use16
; COM program CS=DS=SS
org 100h

    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
    ret

not_prot_mode:
    call a20_on         ; Enable A20 gate (uses Fast method as proof of concept)
    cli

    ; Compute linear address of label gdt_start
    ; Using (segment << 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 label code_32bit
    ; Using (segment << 4) + offset
    add ebx,code_32bit  ; EBX = (CS << 4) + code_32bit

    push dword 0x08     ; CS 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

    jmp dword far [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 checkign 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

in_pmode_str: db "Processor already in protected mode - exiting",0x0a,0x0d,"$"

align 4
gdtr:
    dw gdt_end-gdt_start-1
    dd gdt_start

gdt_start:
    ; First entry is always the Null Descriptor
    dd 0
    dd 0

gdt_code:
    ; 4gb flat r/w/executable code descriptor
    dw 0xFFFF           ; limit low
    dw 0                ; base low
    db 0                ; base middle
    db 0b10011010       ; access
    db 0b11001111       ; granularity
    db 0                ; base high

gdt_data:
    ; 4gb flat r/w data descriptor
    dw 0xFFFF           ; limit low
    dw 0                ; base low
    db 0                ; base middle
    db 0b10010010       ; access
    db 0b11001111       ; granularity
    db 0                ; base high
gdt_end:

; Code that will run in 32-bit protected mode
; Align code to 4 byte boundary. code_32bit label is
; relative to the origin point 100h
align 4
code_32bit:
use32
; Set virtual memory address of pm code/data to CODE32_REL
; We will be relocating this section from low memory where DOS
; originally loaded it.
section protectedmode vstart=CODE32_REL, valign=4
start_32:
    cld                 ; Direction flag forward
    mov eax,0x10        ; 0x10 is flat selector for data
    mov ds,eax
    mov es,eax
    mov fs,eax
    mov gs,eax
    mov ss,eax
    mov esp,STACK32_TOP ; Should set ESP to a usable memory location
                        ; Stack will be grow down from this location

    mov edi,start_32    ; EDI = linear address where PM code will be copied
    mov esi,ebx         ; ESI = linear address of code_32bit
    mov ecx,PMSIZE_LONG ; ECX = number of DWORDs to copy
    rep movsd           ; Copy all code/data from code_32bit to CODE32_REL
    jmp 0x08:.relentry  ; Absolute jump to relocated code

.relentry:
    mov ah, 0x57        ; Attribute white on magenta

    ; Print a string to display
    mov esi,str         ; ESI = address of string to print
    mov edi,VIDEOMEM    ; EDI = base address of video memory
    call print_string_attr

    cli
endloop:
    hlt                 ; Halt CPU with infinite loop
    jmp endloop

print_string_attr:
    push ecx
    xor ecx,ecx         ; ECX = 0 current video offset
    jmp .loopentry
.printloop:
    mov [edi+ecx*2],ax  ; Copy attr and character to display
    inc ecx             ; Next word position
.loopentry:
    mov al,[esi+ecx]    ; Get next character to print
    test al,al
    jnz .printloop      ; If it's not NUL continue
.endprint:
    pop ecx
    ret

str: db "Protected Mode",0

PMSIZE_LONG equ ($-$$+3)>>2
                        ; Number of DWORDS that the protected mode
                        ;    code and data takes up (rounded up)

Этот код немного сложнее, чем я мог бы предположить. Интересной частью будут вычисления указателя в not_prot_mode, которые похожи на типы вычислений, которые выполняет ваш код. После входа в защищенный режим код перемещается выше DOS на 0x00110000. Это было требование человека, который первоначально спросил меня о переходе в защищенный режим.

Примечание : этот код выполняется только в среде, в которой защищенный режим еще не включен. Он будет отображать ошибку и завершать работу при запуске внутри задачи VM8086.

...