2. Входим в защищённый режим

Задача второй главы - показать на простом примере, как составить программу для MS-DOS, переключающую процессор из реального режима в защищённый и возвращающую его обратно в реальный режим. Сам процесс описан во многих книгах, посвящённых i80286, однако при этом обычно опускаются многие технические детали, связанные с аппаратным обеспечением компьютера. Знание этих деталей совершенно необходимо для успешного использования защищенного режима на реальных компьютерах.

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

2.1. Подготовка к переключению в защищённый режим

Перед тем, как переключить процессор в защищённый режим, надо выполнить некоторые подготовительные действия, а именно:

Первый шаг, связанный с подготовкой GDT, мы уже описали, когда рассказывали о преобразовании адресов в защищённом режиме.

Что же касается возврата из защищённого режима в реальный, то он выполняется сбросом процессора, инициированного выводом определённого байта в процессор клавиатуры 8042. Это связано с тем, что разработчики процессора i80286 не предусмотрели никакой команды для переключения процессора из защищённого режима в реальный. Есть ещё один способ возврата в реальный режим, основанный на переводе процессора в состояние отключения, он будет описан в главе, посвящённой обработке прерываний в защищённом режиме.

После выполнения сброса (или после отключения) процессор переходит в реальный режим и управление передаётся в BIOS. BIOS анализирует содержимое ячейки CMOS-памяти с адресом 0Fh - байта состояния отключения. Дальнейшие действия определяются содержимым этой ячейки.

Байт состояния отключения 0Fh используется BIOS для определения способа возврата из защищённого режима в реальный после аппаратного сброса. В таблице 3 перечислены возможные значения для байта состояния отключения.

Таблица 3. Значения байта состояния отключения.

Значение  Причина отключения

0         Программный сброс при нажатии комбинации клавиш CTRL-ALT-DEL
          или неожиданный сброс. Выполняется обычный перезапуск системы,
          но процедуры тестирования при включении питания не выполняются.

1         Сброс после определения объёма памяти.

2         Сброс после тестирования памяти.

3         Сброс после обнаружения ошибки в памяти (контроль чётности).

4         Сброс с запросом перезагрузки.

5         После сброса перезапускается контроллер прерываний, затем
          управление передаётся по адресу, который находится в области
          данных BIOS 0040h:0067h.

6,7,8     Сброс после выполнения теста работы процессора в защищённом
          режиме.

9         Сброс после выполнения пересылки блока памяти из основной
          памяти в расширенную.
0Ah       После сброса управление немедленно передаётся по адресу,
          взятому из области данных BIOS 0040h:0067h.

Для обеспечения возврата в реальный режим после сброса по адресу, записанному в области данных BIOS 0040h:0067h можно использовать байты 5 и 0Ah.

Из за особенностей обработки прерываний в защищённом режиме, которые мы рассмотрим в третьей главе, перед переключением в защищённый режим необходимо перепрограммировать контроллер прерываний. Восстановить состояние контроллера после возврата в реальный режим можно автоматически, если использовать значение 5 для байта состояния отключения.

Если же вы не используете прерывания и, соответственно, не перепрограммируете контроллер прерываний, можно использовать значение 0Ah, при этом после сброса управление будет сразу передано по адресу, взятому из области данных BIOS 0040h:0067h. В этом случае затраченное на возврат в реальный режим время будет меньше.

В следующем фрагменте программы мы записываем в ячейку CMOS-памяти с адресом 0Fh значение 5.

Напомним, что для записи числа в ячейку CMOS-памяти необходимо вначале в порт с адресом 70h записать номер нужной ячейки, а затем в порт 71h - записываемые данные.

Не удивляйтесь, что в этом фрагменте программы вместо ячейки 0Fh указано значение 8Fh - это не ошибка. Напомним, что единственный способ замаскировать немаскируемые прерывания в компьютере IBM AT - это записать в порт 70h байт, в котором старший бит установлен в 1. Поэтому наш фрагмент программы не только записывает байт состояния отключения, но и маскирует немаскируемые прерывания (!). Нам необходимо также замаскировать обычные прерывания, поэтому мы выдаём команду CLI.

        cli
        mov     al,8f
        out     CMOS_PORT,al
        jmp     next1           ; небольшая задержка
next1:
        mov     al,5
        out     CMOS_PORT+1,al




Следующий шаг - открытие адресной линии A20 - необходим в том случае, если ваша программа будет обращаться к оперативной памяти, лежащей за пределами первого мегабайта.

Приведём процедуру, которую можно использовать для этой цели:

; ------------------------------------------------------------
; Процедура открывает адресную линию A20
; ------------------------------------------------------------

PROC    enable_a20      NEAR
        mov     al,A20_PORT
        out     STATUS_PORT,al
        mov     al,A20_ON
        out     KBD_PORT_A,al
        ret
ENDP    enable_a20




Эта магическая последовательность команд выдаёт команду A20_ON клавиатурному процессору 8042, к которому подключены схемы управления адресной линией A20. После начального сброса линия A20 закрыта, и расширенная память за границами первого мегабайта недоступна.

Следующий этап - запоминание содержимого сегментных регистров, которые будут нужны при возврате в реальный режим. Это сегментные регистры SS и ES:

        mov     [real_ss],ss    ; запоминаем указатель стека
        mov     [real_es],es    ; для реального режима



На последнем перед переключением в защищённый режим этапе мы загружаем регистр GDTR адресом подготовленной заранее GDT:

lgdt [QWORD gdt_gdt]



Всё! Можно переключаться в защищённый режим!

2.2. Переключение в защищённый режим

Это самый простой этап. Для перевода процессора i80286 из реального режима в защищённый можно использовать специальную команду LMSW, загружающую регистр состояния процессора (Mashine Status Word). Младший бит этого регистра указывает режим работы процессора. Значение, равное 0, соответствует реальному режиму работы, а значение 1 - защищённому.

Если установить младший бит регистра состояния процессора в 1, процессор переключится в защищённый режим:

        mov     ax, 1
        lmsw    ax



К сожалению, с помощью команды LMSW невозможно переключить процессор обратно в реальный режим. Для этого необходимо использовать другой способ.

2.3. Возврат в реальный режим

Для того, чтобы вернуть процессор 80286 из защищённого режима в реальный, необходимо выполнить аппаратный сброс (отключение) процессора. Это можно сделать следующим образом:

mov     ax, 0FEh        ; команда отключения
out     64h, ax



Перед выдачей команды отключения необходимо запомнить содержимое регистра SP, так как после передачи управления по адресу, записанному в области данных BIOS 0040h:0067h, регистры SS:SP будет указывать на стек BIOS.

После выдачи команды отключения надо подождать, когда произойдёт сброс процессора. Это можно сделать, выдавая в цикле команду HLT.

Вот фрагмент программы, возвращающий процессор в реальный режим:

; Запоминаем содержимое указателя стека, так как после
; сброса процессора оно будет потеряно

        mov     [real_sp],sp

; Выполняем сброс процессора

        mov     al,SHUT_DOWN
        out     STATUS_PORT,al

; Ожидаем сброса процессора

wait_reset:
        hlt
        jmp     wait_reset



Далее необходимо восстановить содержимое сегментных регистров, записанное в оперативную память на этапе подготовки к переключению в защищённый режим, закрыть адресную линию A20 и размаскировать прерывания.

Для закрытия линии A20 можно воспользоваться следующей процедурой:

; ------------------------------------------------------------
; Процедура закрывает адресную линию A20
; ------------------------------------------------------------

PROC    disable_a20     NEAR
        mov     al,A20_PORT
        out     STATUS_PORT,al
        mov     al,A20_OFF
        out     KBD_PORT_A,al
        ret
ENDP    disable_a20



Следующая последовательность команд размаскирует все прерывания:

        mov     ax,000dh        ; разрешаем немаскируемые прерывания
        out     CMOS_PORT,al

        in      al,INT_MASK_PORT ; разрешаем маскируемые прерывания
        and     al,0
        out     INT_MASK_PORT,al
        sti



Теперь, когда мы знаем всё, что нужно для переключения процессора в защищённый режим и возврата в реальный режим, можно приступить к практической работе в защищённом режиме.

2.4. Пример простой программы переключения режима

Данная программа запускается в реальном режиме в среде MS-DOS, переключает процессор в защищённый режим, выдаёт сообщение и через некоторое время возвращает процессор в реальный режим, стирает экран и завершает своё выполнение.

Программа подготовлена с помощью транслятора Borland Turbo Assembler и использует режим IDEAL. Для её трансляции был использован следующий командный файл:

tasm %1.asm /l /zi
tlink %1.obj /v



Вы можете запускать эту программу на любой машине, совместимой с IBM AT и оборудованной процессорами i80286, i80386, i80486. Но вы не должны запускать эту программу, если у вас компьютер на базе процессоров i80386 или i80486 и активны драйверы QEMM, EMM386 - отключите эти драйверы. Кроме того, эту программу нельзя запускать на виртуальной машине в среде Microsoft WINDOWS в режиме Enchanced Mode или на виртуальной машине в среде OS/2 версии 2.0.

Это связано с тем, что в перечисленных выше случаях процессор работает не в реальном режиме, а в так называемом режиме виртуального процессора 8086. Этот режим возможен только для процессоров i80386 или i80486.

Кроме того, не следует запускать эту программу на компьютерах серии PS/2, так как в них используется другой способ управления линией A20 и другой способ сброса процессора для возврата в реальный режим.

Вы можете использовать эту программу для первых экспериментов с защищённым режимом.

Обратим ваше внимание на некоторые ограничения защищённого режима. Эти ограничения связаны в основном с использованием прерываний и сегментных регистров.

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

Поэтому в приведённой ниже программе вывод текстовых строк на экран производится при помощи непосредственной записи в память видеоконтроллера. Что же касается ввода данных с клавиатуры, то здесь не обойтись без обработки прерывания от клавиатуры. В следующей главе мы приведём пример программы, которая не только выводит данные на экран дисплея (также через запись в видеопамять), но и умеет работать с клавиатурой и таймером.

Если ваша программа составлена на языках высокого уровня (Си или Паскаль), ей недоступна практически вся библиотека стандартных функций. Особенно это касается функций ввода/вывода и управления памятью. Причина очевидна - большинство функций стандартных библиотек трансляторов вызывает те или иные прерывания BIOS или DOS.

Это ограничение связано с механизмом преобразования логического адреса в физический.

Итак, наша первая программа для защищённого режима процессора 80286:

Листинг 1. Демонстрация переключения в защищённый режим
и возврата обратно в реальный режим

-----------------------------------------------------------


IDEAL
RADIX   16
P286

; Используем модель памяти LARGE, при этом мы организуем
; несколько отдельных сегментов и для каждого сегмента
; создадим дескриптор в таблице GDT.

MODEL   LARGE

; ------------------------------------------------------------
; Определения структур данных и констант
; ------------------------------------------------------------

STRUC   desc_struc              ; структура дескриптора
        limit   dw      0       ; предел
        base_l  dw      0       ; мл. слово физического адреса
        base_h  db      0       ; ст. байт физического адреса
        access  db      0       ; байт доступа
        rsrv    dw      0       ; зарезервировано
ENDS    desc_struc

; Биты байта доступа

ACC_PRESENT     EQU     10000000b ; сегмент есть в памяти
ACC_CSEG        EQU     00011000b ; сегмент кода
ACC_DSEG        EQU     00010000b ; сегмент данных
ACC_EXPDOWN     EQU     00000100b ; сегмент расширяется вниз
ACC_CONFORM     EQU     00000100b ; согласованный сегмент
ACC_DATAWR      EQU     00000010b ; разрешена запись

; Типы сегментов

; сегмент данных
DATA_ACC = ACC_PRESENT OR ACC_DSEG OR ACC_DATAWR

; сегмент кода
CODE_ACC = ACC_PRESENT OR ACC_CSEG OR ACC_CONFORM

; сегмент стека
STACK_ACC = ACC_PRESENT OR ACC_DSEG OR ACC_DATAWR OR ACC_EXPDOWN


; Константы

STACK_SIZE      EQU     0400    ; размер стека
B_DATA_SIZE     EQU     0300    ; размер области данных BIOS
B_DATA_ADDR     EQU     0400    ; адрес области данных BIOS
MONO_SEG        EQU     0b000   ; сегмент видеопамяти 
                                ;  монохромного видеоадаптера
COLOR_SEG       EQU     0b800   ; сегмент видеопамяти
                                ; цветного видеоадаптера
CRT_SIZE        EQU     4000    ; размер сегмента видеопамяти
                                ;  цветного видеоадаптера
MONO_SIZE       EQU     1000    ; размер сегмента видеопамяти
                                ;  монохромного видеоадаптера

CRT_LOW         EQU     8000    ; мл. байт физического адреса
                                ;  сегмента видеопамяти
                                ;  цветного видеоадаптера
MONO_LOW        EQU     0000    ; мл. байт физического адреса
                                ;  сегмента видеопамяти
                                ;  монохромного видеоадаптера

CRT_SEG         EQU     0Bh     ; ст. байт физического адреса
                                ;  сегмента видеопамяти

; Селекторы, определённые в таблице GDT

DS_DESCR        =       (gdt_ds - gdt_0)
CS_DESCR        =       (gdt_cs - gdt_0)
SS_DESCR        =       (gdt_ss - gdt_0)
BIOS_DESCR      =       (gdt_bio - gdt_0)
CRT_DESCR       =       (gdt_crt - gdt_0)
MDA_DESCR       =       (gdt_mda - gdt_0)

CMOS_PORT       EQU     70h     ; порт для доступа к CMOS-памяти
PORT_6845       EQU     0063h   ; адрес области данных BIOS,
                                ; где записано значение адреса
                                ; порта контроллера 6845
COLOR_PORT      EQU     03d4h   ; порт цветного видеоконтроллера
MONO_PORT       EQU     03b4h   ; порт монохромного видеоконтроллера
STATUS_PORT     EQU     64h     ; порт состояния клавиатуры
SHUT_DOWN       EQU     0feh    ; команда сброса процессора
VIRTUAL_MODE    EQU     0001h   ; бит перехода в защищённый режим
A20_PORT        EQU     0d1h    ; команда управления линией A20
A20_ON          EQU     0dfh    ; открыть A20
A20_OFF         EQU     0ddh    ; закрыть A20
KBD_PORT_A      EQU     60h     ; адреса клавиатурных
KBD_PORT_B      EQU     61h     ;   портов
INT_MASK_PORT   EQU     21h     ; порт для маскирования прерываний

STACK   STACK_SIZE      ; сегмент стека

DATASEG                 ; начало сегмента данных

DSEG_BEG        =       THIS WORD

; Память для хранения регистров SS, SP, ES. Содержимое
; этих регистров будет записано здесь перед входом в
; защищённый режим и восстановлено отсюда после возврата
; из защищённого режима в реальный.

        real_ss dw      ?
        real_sp dw      ?
        real_es dw      ?

; Глобальная таблица дескрипторов GDT,
; содержит следующие дескрипторы:
;
;       gdt_0   - дескриптор для пустого селектора
;       gdt_gdt - дескриптор для GDT
;       gdt_ds  - дескриптор для сегмента, адресуемого DS
;       gdt_cs  - дескриптор для сегмента кода
;       gdt_ss  - дескриптор для сегмента стека
;       gdt_bio         - дескриптор для области данных BIOS
;       gdt_crt         - дескриптор для видеопамяти цветного дисплея
;       gdt_mda         - дескриптор для видеопамяти монохромного дисплея

GDT_BEG         = $
LABEL   gdtr            WORD

        gdt_0   desc_struc      <0,0,0,0,0> 
        gdt_gdt desc_struc      <GDT_SIZE-1,,,DATA_ACC,0>
        gdt_ds  desc_struc      <DSEG_SIZE-1,,,DATA_ACC,0>
        gdt_cs  desc_struc      <CSEG_SIZE-1,,,CODE_ACC,0>
        gdt_ss  desc_struc      <STACK_SIZE-1,,,DATA_ACC,0>
        gdt_bio         desc_struc      <B_DATA_SIZE-1,B_DATA_ADDR,0,DATA_ACC,0>
        gdt_crt         desc_struc      <CRT_SIZE-1,CRT_LOW,CRT_SEG,DATA_ACC,0>
        gdt_mda         desc_struc      <MONO_SIZE-1,MONO_LOW,CRT_SEG,DATA_ACC,0>

GDT_SIZE        = ($ - GDT_BEG) ; размер таблицы дескрипторов

CODESEG         ; сегмент кода

PROC    start

; Инициализируем регистр сегмента данных
; для реального режима

        mov     ax,DGROUP
        mov     ds,ax

; Определяем базовый адрес видеопамяти

        call    set_crt_base

; Стираем экран дисплея (устанавливаем серый фон)

        mov     bh, 77h
        call    clrscr

; Выполняем все подготовительные действия для перехода
; в защищённый режим и обеспечения возможности возврата
; в реальный режим

        call    init_protected_mode

; Переключаемся в защищённый режим

        call    set_protected_mode

; --------- * Программа работает в защищённом режиме! * ---------

        call    write_hello_msg ; выводим сообщение на экран
        call    pause           ; ждём некоторое время

; Возвращаемся в реальный режим

        call    set_real_mode

; --------- * Программа работает в реальном режиме! * ---------

; Стираем экран и возвращаемся в DOS
        mov     bh, 07h
        call    clrscr
        mov     ah,4Ch
        int     21h

ENDP    start


; ------------------------------------------------------------
; Макрокоманда для записи в дескриптор 24-битового
; базового адреса сегмента
; ------------------------------------------------------------

MACRO setgdtentry
        mov     [(desc_struc bx).base_l],ax
        mov     [(desc_struc bx).base_h],dl
ENDM

; ------------------------------------------------------------
; Процедура подготовки процессора к переходу в защищённый
; режим с последующим возвратом в реальный режим
; ------------------------------------------------------------

PROC    init_protected_mode     NEAR

; Заполняем глобальную таблицу дескрипторов GDT

; Вычисляем 24-битовый базовый адрес сегмента данных

        mov     ax,DGROUP
        mov     dl,ah
        shr     dl,4
        shl     ax,4

; Регистры dl:ax содержат базовый адрес, сохраняем его в di:si

        mov     si,ax
        mov     di,dx

; Подготавливаем дескриптор для GDT

        add     ax,OFFSET gdtr
        adc     dl,0
        mov     bx,OFFSET gdt_gdt
        setgdtentry

; Подготавливаем дескриптор для сегмента ds

        mov     bx,OFFSET gdt_ds
        mov     ax,si
        mov     dx,di
        setgdtentry

; Подготавливаем дескриптор для сегмента cs

        mov     bx,OFFSET gdt_cs
        mov     ax,cs
        mov     dl,ah
        shr     dl,4
        shl     ax,4
        setgdtentry

; Подготавливаем дескриптор для сегмента стека

        mov     bx,OFFSET gdt_ss
        mov     ax,ss
        mov     dl,ah
        shr     dl,4
        shl     ax,4
        setgdtentry

; Записываем адрес возврата в реальный режим в область
; данных BIOS по адресу 0040h:0067h

        push    ds
        mov     ax,40
        mov     ds,ax
        mov     [WORD 67],OFFSET shutdown_return
        mov     [WORD 69],cs
        pop     ds

; Маскируем все прерывания, в том числе немаскируемые.
; Записываем в CMOS-память в ячейку 0Fh код 5,
; этот код обеспечит после выполнения сброса процессора
; передачу управления по адресу, подготовленному нами
; в области данных BIOS по адресу 0040h:0067h.
; Для того, чтобы немаскируемые прерывания были запрещены,
; устанавливаем в 1 старший бит при определении ячейки CMOS.

        cli
        mov     al,8f
        out     CMOS_PORT,al
        jmp     next1           ; небольшая задержка
next1:

        mov     al,5
        out     CMOS_PORT+1,al  ; код возврата

        ret

ENDP    init_protected_mode

; ------------------------------------------------------------
; Процедура переключает процессор в защищённый режим
; ------------------------------------------------------------

PROC    set_protected_mode      NEAR

        mov     ax,[rl_crt]     ; записываем в es сегментный
        mov     es,ax           ; адрес видеопамяти

        call    enable_a20      ; открываем адресную линию A20

        mov     [real_ss],ss    ; запоминаем указатель стека
        mov     [real_es],es    ; для реального режима

; Загружаем регистр GDTR

        lgdt    [QWORD gdt_gdt]

; Устанавливаем защищённый режим работы процессора

        mov     ax,VIRTUAL_MODE
        lmsw    ax

; Мы находимся в защищённом режиме

; Очищаем внутреннюю очередь команд процессора
; Выполняем команду межсегментного прерхода,
; в качестве селектора указываем селектор текущего
; сегмента кода, в качестве смещения - метку flush

;       jmp     far flush
        db      0ea
        dw      OFFSET flush
        dw      CS_DESCR

LABEL   flush   FAR

; Загружаем сегментные регистры SS и DS селекторами

        mov     ax,SS_DESCR
        mov     ss,ax
        mov     ax,DS_DESCR
        mov     ds,ax
        ret

ENDP    set_protected_mode

; ------------------------------------------------------------
; Процедура возвращает процессор в реальный режим
; ------------------------------------------------------------

PROC    set_real_mode   NEAR

; Запоминаем содержимое указателя стека, так как после
; сброса процессора оно будет потеряно

        mov     [real_sp],sp

; Выполняем сброс процессора

        mov     al,SHUT_DOWN
        out     STATUS_PORT,al

; Ожидаем сброса процессора

wait_reset:
        hlt
        jmp     wait_reset

; ------->> В это место мы попадём после сброса процессора,
; теперь мы снова в реальном режиме

LABEL   shutdown_return FAR

; Инициализируем ds адресом сегмента данных

        mov     ax,DGROUP
        mov     ds,ax
        assume  ds:DGROUP

; Восстанавливаем указатель стека

        mov     ss,[real_ss]
        mov     sp,[real_sp]

; Восстанавливаем содержимое регистра es

        mov     es,[real_es]

; Закрываем адресную линию A20

        call    disable_a20

; Разрешаем все прерывания

        mov     ax,000dh        ; разрешаем немаскируемые прерывания
        out     CMOS_PORT,al

        in      al,INT_MASK_PORT ; разрешаем маскируемые прерывания
        and     al,0
        out     INT_MASK_PORT,al
        sti

        ret
ENDP    set_real_mode

; ------------------------------------------------------------
; Процедура открывает адресную линию A20
; ------------------------------------------------------------

PROC    enable_a20      NEAR
        mov     al,A20_PORT
        out     STATUS_PORT,al
        mov     al,A20_ON
        out     KBD_PORT_A,al
        ret
ENDP    enable_a20

; ------------------------------------------------------------
; Процедура закрывает адресную линию A20
; ------------------------------------------------------------

PROC    disable_a20     NEAR
        mov     al,A20_PORT
        out     STATUS_PORT,al
        mov     al,A20_OFF
        out     KBD_PORT_A,al
        ret
ENDP    disable_a20

; ------------------------------------------------------------
; Процедура выполняет небольшую временную задержку
; ------------------------------------------------------------

PROC    pause           NEAR
        push    cx
        mov     cx,50
ploop0:
        push    cx
        xor     cx,cx
ploop1:
        loop    ploop1
        pop     cx
        loop    ploop0

        pop     cx
        ret
ENDP    pause

; ------------------------------------------------------------
; Сегмент данных для процедур обслуживания видеоадаптера
; ------------------------------------------------------------

DATASEG
        columns db      80d     ; количество столбцов на экране
        rows    db      25d     ; количество строк на экране

        rl_crt  dw      COLOR_SEG       ; сегментный адрес видеобуфера
        vir_crt dw      CRT_DESCR       ; селектор видеобуфера

        curr_line       dw      0d      ; номер текущей строки

CODESEG

; ------------------------------------------------------------
; Определение базового адреса видеобуфера
; ------------------------------------------------------------

PROC    set_crt_base    NEAR

; Определяем количество столбцов на экране и записываем 
; в переменную columns

        mov     ax,40
        mov     es,ax
        mov     bx,[WORD es:4a]
        mov     [columns],bl

; То же для количества строк, записываем в переменную rows

        mov     bl,[BYTE es:84]
        inc     bl
        mov     [rows],bl

; Для того чтобы определить тип видеоконтроллера (цветной
; или монохромный), считываем адрес микросхемы 6845

        mov     bx,[WORD es:PORT_6845]
        cmp     bx,COLOR_PORT
        je      set_crt_exit

; Если видеоконтроллер монохромный, изменяем адрес сегмента
; и селектор, заданные по умолчанию

        mov     [rl_crt],MONO_SEG
        mov     [vir_crt],MDA_DESCR

set_crt_exit:
        ret
ENDP    set_crt_base

; ------------------------------------------------------------
; Вывод строки на экран
; Параметры:
;       (ax, bx) - координаты (x, y) выводимой строки
;       ds:si   - адрес выводимой строки
;       cx      - длина выводимой строки
;       dh      - атрибут выводимой строки
;       es      - сегмент или селектор видеопамяти
; ------------------------------------------------------------

PROC    writexy         NEAR
        push    si
        push    di

; Вычисляем смещение в видеобуфере для записи строки,
; используем формулу ((y * columns) + x) * 2

        mov     dl,[columns]
        mul     dl
        add     ax,bx
        shl     ax,1
        mov     di,ax
        mov     ah,dh   ; записываем в ah байт атрибута

; Выполняем запись в видеобуфер

wxy_write:
        lodsb   ; очередной символ в al
        stosw   ; записываем его в видеопамять
        loop    wxy_write       ; цикл до конца строки

        pop     di
        pop     si
        ret
ENDP    writexy

; ------------------------------------------------------------
; Процедура стирания экрана
;       Параметр: bh - атрибут для заполнения экрана
; ------------------------------------------------------------


PROC    clrscr          NEAR
        xor     cx,cx
        mov     dl,[columns]
        mov     dh,[rows]
        mov     ax,0600h        
        int     10h
        ret
ENDP    clrscr

DATASEG

hello_msg db " Protected mode monitor *TINY/OS*, 
                v.1.0 for CPU 80286  ¦ © Frolov A.V., 1992 "

CODESEG

; ------------------------------------------------------------
; Процедура выводит сообщение в защищённом режиме
; ------------------------------------------------------------

PROC    write_hello_msg NEAR

        mov     ax,[vir_crt]    ; загружаем селектор видеопамяти 
        mov     es,ax           ; в регистр es

; Выводим сообщение в верхний левый угол экрана (x=y=0)

        mov     bx,0            ;(X,Y) = (AX,BX)
        mov     ax,[curr_line]
        inc     [curr_line]     ; увеличиваем номер текущей строки

; Загружаем адрес выводимой строки и её длину

        mov     si,OFFSET hello_msg
        mov     cx,SIZE hello_msg

        mov     dh,30h  ; аттрибут - черный текст на голубом фоне

        call    writexy ; выводим строку

        ret
ENDP    write_hello_msg


CSEG_SIZE       = ($ - start) ; размер сегмента кода

DATASEG

DSEG_SIZE       = ($ - DSEG_BEG) ; размер сегмента данных

        END     start