Переход в защищенный режим i386

После того как я накатил FreeDOS [1] на Pocket 386 [0] и поэкспериментировал с ним какое-то время, мне захотелось написать для этой машины собственную операционную среду.

Но раз уж у нас целых 8 мегабайт оперативной памяти, то хотелось бы иметь возможность использовать её всю. В 16-битном режиме работы работы процессора это возможно, но жутко неудобно. Я вообще на дух не переношу всю эту адресную арифметику, основанную на сегментной модели памяти. Поэтому было принято решение перевести камень в защищенный режим (Protected Mode) с плоской (flat) моделью памяти.

Скачал datasheet на 386-ой процессор, за несколько вечеров освежил знания по нему. А затем уже начались эксперименты на qemu [4]. Для нашей задачи всего-навсего нужно сделать две вещи:

GDT формируем в соответствии с плоской моделью памяти (base=0, limit=0xfffff). Первый дескриптор в GDT должен быть нулевым, второй (то есть со смещением 8) у нас будет код, а за ним (со смещением 16) - данные. Формат GDT и её элементов выглядит очень криво: базовый адрес (base) и предел (limit) размазаны кусками по этим несчастным восьми байтам, требование первого пустого дескриптора. Складывается ощущение, что инженеры Intel решили отомстить системным программистам за что-то :)

Но все эти мелкие препятствия преодолены, загрузочный сектор сформирован, на qemu всё работает. Супер, заливаем загрузочный сектор на Pocket 386 и... бесконечная перезагрузка! Любопытно: первое расхождение с эмулятором. Оказалось все довольно банально: я забыл проинициализировать сегментный регистр ds для реального (16-битного) режима. Эмулятор qemu заносит туда нужное значение за нас, а вот реальное железо таких вольностей не прощает. Но пара ассемблерных инструкций и все исправлено.

Собирается содержимое загрузочного сектора ассемблером nasm [2][3] из файла boot.asm:

$ nasm -f bin -l boot.lst -o boot.bin boot.asm

Протестировать собранный boot.bin можно в эмуляторе qemu:

$ qemu-system-i386 -hda boot.bin

В качестве "полезной нагрузки" в защищённом режиме код ждёт нажатие на клавиатуру и выводит шестнадцатеричное представление scan-кода нажатой клавиши на экран.

Содержимое файла boot.asm:

ORG 0x7c00
bootsec_start:
BITS 16
    mov     ax, cs
    mov     ds, ax

    cli
    cld

    ; Enable A20
%macro wait42 0
%%wait42_loop:
    in      al, 0x64
    and     al, 0x02
    jnz     %%wait42_loop
%endmacro
    wait42
    mov     al, 0xd1
    out     0x64, al
    wait42
    mov     al, 0xdf
    out     0x60, al
    wait42

    ; Load GDT
    lgdt    [LGDT_op]

    ; Enter to Protected Mode
    mov     eax, cr0
    or      al, 1
    mov     cr0, eax

    ; clear prefetch queue & load cs CODE descriptor (8)
    jmp     dword 8:bits32

BITS 32
bits32:
    mov     bx, 0x10 ; DATA descriptor
    mov     ds, bx
    mov     ss, bx
    mov     es, bx

    ; "payload"

    mov     edi, 0x000b8000 ; stosw -> putchar
%macro put_al_ascii 0
    mov     ah, 0x13
    cmp     edi, 0x000b8000 + 4000
    jb      %%in_range
    mov     edi, 0x000b8000
%%in_range:
    stosw
%endmacro

%macro put_al_4bit_hex  0
    add     al, 0x30
    cmp     al, 0x3A
    jb      %%_below_ten
    add     al, 0x07
%%_below_ten:
    put_al_ascii
%endmacro

    mov     al, 0x40
    put_al_ascii

read_key:
    in      al, 0x64
    test    al, 0x01
    jnz     _kbd_output_ready
    pause
    jmp     read_key
_kbd_output_ready:
    in      al, 0x60
    test    al, 0x80
    jnz     read_key

    mov     bl, al
    shr     al, 4
    put_al_4bit_hex
    mov     al, bl
    and     al, 0x0F
    put_al_4bit_hex
    mov     al, 0x20
    put_al_ascii

    jmp     read_key

GDT:
    ; NULL descriptor
    dd 0
    dd 0
    ; CODE descriptor
    dw  0xFFFF  ; Limit (0-15)
    dw  0       ; Base (0-15)
    db  0       ; Base (16-23)
    db  0x9e    ; Access Byte (present, privilege level 0, code, <= DPL, readable)
    db  0xcf    ; Flags (4 KiB granularity, 32-bit protected), Limit (16-19)
    db  0       ; Base (24-31)
    ; DATA descriptor
    dw 0xFFFF   ; Limit (0-15)
    dw 0        ; Base (0-15)
    db 0        ; Base (16-23)
    db 0x92     ; Access Byte (present, privilege level 0, data, <= DPL, writeable)
    db 0xcf     ; Flags (4 KiB granularity, 32-bit protected), Limit (16-19)
    db 0        ; Base (24-31)
GDT_end:

LGDT_op:
    dw  GDT_end - GDT
    dd  GDT

bootsec_sign:
    resb    510-(bootsec_sign-bootsec_start)
    db      0x55
    db      0xAA
[0] Pocket 386
[1] FreeDOS
[2] NASM
[3] Расширенный ассемблер: NASM (русскоязычное руководство на OpenNET)
[4] QEMU