Задача второй главы - показать на простом примере, как составить программу для MS-DOS, переключающую процессор из реального режима в защищённый и возвращающую его обратно в реальный режим. Сам процесс описан во многих книгах, посвящённых i80286, однако при этом обычно опускаются многие технические детали, связанные с аппаратным обеспечением компьютера. Знание этих деталей совершенно необходимо для успешного использования защищенного режима на реальных компьютерах.
Сейчас мы не будем рассматривать процесс обработки прерываний в защищённом режиме - это материал следующей главы. Там же будет приведён и соответствующий пример программы. В программе, которую представим в этой главе, мы запретили прерывания на всё то время, пока процессор находится в защищённом режиме.
Перед тем, как переключить процессор в защищённый режим, надо выполнить некоторые подготовительные действия, а именно:
Первый шаг, связанный с подготовкой 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]
Всё! Можно переключаться в защищённый режим!
Это самый простой этап. Для перевода процессора i80286 из реального режима в защищённый можно использовать специальную команду LMSW, загружающую регистр состояния процессора (Mashine Status Word). Младший бит этого регистра указывает режим работы процессора. Значение, равное 0, соответствует реальному режиму работы, а значение 1 - защищённому.
Если установить младший бит регистра состояния процессора в 1, процессор переключится в защищённый режим:
mov ax, 1 lmsw ax
К сожалению, с помощью команды LMSW невозможно переключить процессор обратно в реальный режим. Для этого необходимо использовать другой способ.
Для того, чтобы вернуть процессор 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
Теперь, когда мы знаем всё, что нужно для переключения процессора в защищённый режим и возврата в реальный режим, можно приступить к практической работе в защищённом режиме.
Данная программа запускается в реальном режиме в среде 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