Assembler для начинающих
Глава 10
Раширения системы и подпрограммы на языке Ассемблера
В этой главе рассказывается о способе использования программ на
языке ассемблера в больших программах. Приведенные ранее примеры
были автономными программами на языке ассемблера. Ни один из других
языков программирования не позволяет так, как язык ассемблера,
управлять техническими средствами. Однако во многих случаях выбор
языка ассемблера в качестве языка программирования может оказаться
неправильным. Часто лучше всего бывает применять язык высокого
уровня в сочетании с подпрограммами на языке ассемблера.
В настоящей главе рассматриваются две области применения
программ на языке ассемблера. В первом случае мы напишем на языке
ассемблера программы для расширения встроенной базовой системы
ввода-вывода (ROM BIOS). Эти программы добавляют новые функции для
технического обеспечения. Перед использованием такие программы
необходимо поместить в память на постоянное хранение. Затем ваша
программа привлекает эти новые функции как расширение стандартного
набора функций BIOS. Будут приведены два примера для двух различных
способов загрузки программ в память.
Вторая область - использование процедур на языке ассемблера
в программах, написанных на языке высокого уровня. Здесь
подпрограмма на машинном языке выполняет ту функцию, которую было
бы сложно или невозможно реализовать на языке высокого уровня.
Будет приведен ряд примеров, иллюстрирующих различные способы
применения таких подпрограмм. Примеры относятся в основном к
дополнению функций технического обеспечения, однако те же принципы
можно использовать для любой программы на машинном языке.
Компилируемые языки высокого уровня
Компилируемые языки высокого уровня
В предыдущих примерах рассматривалась программа на языке
ассемблера, используемая совместно с интерпретатором Бейсика.
Версия языка Бейсик, входящая в поставку IBM PC, является
интерпретируемым языком. Это означает, что программа хранится в
ЭВМ в виде, очень похожем на исходный текст. Интерпретатор не
преобразует операторы языка Бейсик в команды машинного языка.
Интерпретатор Бейсика во время выполнения просматривает каждый
оператор программы и делает все, что необходимо для выполнения
этого оператора.
По-другому работает компилятор. Он преобразует операторы языка
высокого уровня в команды машинного языка. Фирма IBM предлагает
компиляторы для персональной ЭВМ с языков Бейсик, Паскаль, Фортран
и Кобол. Выходом компилятора является программа на машинном языке
(файл *.OBJ), т.е. он во многом аналогичен выходу ассемблера.
Запуск программы, написанной на компилируемом языке высокого уровня
состоит из двух этапов. Сначала программа должна быть
скомпилирована, и должны быть отредактированы связи. Затем она
может быть выполнена. Интерпретируемая программа может выполняться
непосредственно, минуя этап компиляции.
Компилируемые языки на персональной ЭВМ аналогичны языку Бейсик
в том смысле, что не дают возможности делать с техническим
обеспечением все, что вздумается. На самом деле интерпретатор
Бейсика еще позволяет программисту при помощи операторов программы
считывать и записывать информацию с портов ввода-вывода и ячеек
памяти. Другие языки не всегда предоставляют даже эту возможность.
Поэтому применение подпрограмм на языке ассемблера в программе на
Паскале или Фортране может оказаться даже более необходимым.
Возможно, вам придется заняться этим, если вы захотите
воспользоваться всеми возможностями технического обеспечения.
К счастью, включить процедуру на языке ассемблера в программу
на компилируемом языке высокого уровня довольно просто, так как
выходом компилятора является объектный файл, готовый к
редактированию связей. Выход ассемблера - тоже объектный файл.
Следовательно, достаточно лишь связать программу на языке высокого
уровня и программу на языке ассемблера при помощи редактора связей
DOS. Нет необходимости соединять программы в процессе выполнения,
как это делалось для интерпретатора Бейсика.
Построим пример на языке Фортран (Фиг. 10.11). Для языка
Паскаль все очень похоже. Подобный пример приведен в приложении D
справочника к компилятору Фортрана. В примере головная программа,
написанная на Фортране, объединена с программой на языке
ассемблера, которая считывает текущее время, используя программное
прерывание базовой системы ввода-вывода. Подпрограмма на языке
ассемблера обращается к BIOS для определения текущего времени и
возвращает соответствующее значение в программу на Фортране.
Головная программа преобразует кванты таймера, в текущее время,
выраженное в часах, минутах и секундах.
На Фиг. 10.11 представлена головная программа на Фортране.
Эта программа вызывает внешнюю процедуру TIMER, имеющую один
параметр A - четырехбайтовое целое значение. Возрващаемое
процедурой TIMER значение представляет собой текущее время,
выраженное в квантах таймера и отсчитываемое от полуночи.
Программа на Фортране по полученному из процедуры TIMER значению
вычисляет время в часах(HOURS), минутах(MINS), секундах(SECS) и
сотых долях секунды(HSECS). Отметим, насколько проще реализовать
умножение и деление на языке Фортран, чем на языке ассемблера.
Можно убедиться, что выполнение всех подобных операций на Фортране
существенно упрощает программирование. Чрезвычайно удобен и способ
преобразования целых переменных в выдаваемые на печать символы при
помощи операторов Фортрана WRITE и FORMAT. На языке ассемблера для
выполнения тех же самых действий потребовалось бы несколько сот
строк. Вспомним пример для сопроцессора 8087, где программа
преобразовывала число с плавающей точкой в код ASCII. В этой
программе содержалось значительное число команд, и, кроме того,
использовался сопроцессор 8087.
$STORAGE=4
INTEGER A,HOURS,MINS,SECS,HSECS
CALL TIMER(A)
HOURS=A/65543
A=A-HOURS*65543
MINS=A/1092
A=A-MINS*1092
SECS=A/18
HSECS=(100*(A-SECS*18))/18
WRITE(*,10)HOURS,MINS,SECS,HSECS
10 FORMAT(1X,'THE TIME IS: ',I2,':',I2,':',I2,'.',I2)
END
Фиг. 10.11 Программа определения времени дня на Фортране
Microsoft (R) Macro Assembler Version 5.00 4/2/89 16:07:35
Фиг. 10.12 Подпрограмма для программы на ФОРТРАНе Page 1-1
PAGE ,132
TITLE Фиг. 10.12 Подпрограмма для программы на ФОРТРАНе
FRAME STRUC
0000 ???? SAVEBP DW ?
0002 ???????? SAVERET DD ?
0006 ???????? A DD ? ; Указатель на параметр
000A FRAME ENDS
0000 CODE SEGMENT 'CODE'
DGROUP GROUP DATA
ASSUME CS:CODE,DS:DGROUP,ES:DGROUP,SS:DGROUP
0000 TIMER PROC FAR
PUBLIC TIMER ; Указание программе LINK на расположение
; программы TIMER
0000 55 PUSH BP
0001 8B EC MOV BP,SP ; Загрузка адреса стека
0003 B4 00 MOV AH,0
0005 CD 1A INT 1Ah ; Вызов BIOS для получения даты и времени
0007 C4 5E 06 LES BX,[BP].A ; Загрузка адреса поля параметров
000A 26: 89 17 MOV ES:[BX],DX ; Сохранение младшей части времени
000D 26: 89 4F 02 MOV ES:[BX+2],CX ; Сохранение старшей части времени
0011 5D POP BP
0012 CA 0004 RET 4 ; Возврат с удалением параметров из стека
0015 TIMER ENDP
0015 CODE ENDS
END
Фиг. 10.12 Ассемблерная процедура для программы на Фортране
На Фиг. 10.12 представлена подпрограмма на языке ассемблера -
процедура TIMER. В этой несложной программе для считывания
текущего времени и сохранения полученного значения в двойном слове
используется обращение к BIOS. Здесь нам необходимо рассмотреть
способ передачи параметров из программы на Фортране в подпрограмму
на языке ассемблера.
На Фиг.10.13 показано содержимое стека в начальный момент
выполнения подпрограммы на языке ассемблера. Точно так же, как
интерпретатор Бейсика, программа на Фортране помещает адрес
параметра в стек. Однако компиляторы Фортрана и Паскаля передают
указатель длиной в два слова, а не одно только смещение параметра.
Это означает, что программа на языке ассемблера, прежде чем
получить доступ к параметру, должна установить как сегментный
регистр, так и адрес смещения. Если бы параметров было более
одного, то программа на Фортране перед вызовом поместила бы в стек
значения адресов и остальных параметров.
ГДДДДДДДДДДДДґ
SPДДДД>і Смещение і
і возврата і
ГДДДДДДДДДДДДґ
і Сегмент і
і возврата і
ГДДДДДДДДДДДДґ
і Смещение і
і аргумента і
ГДДДДДДДДДДДДґ
і Сегмент і
і аргумента і
ГДДДДДДДДДДДДґ
Фиг. 10.13 Стек для вызова процедуры в Фортране
Подпрограмма TIMER на Фиг. 10.12 адресует стек, помещая в него
регистр BP и устанавливая его на вершину стека.Структура FRAME
помогает идентифицировать разные значения в стеке после того как
программа сохранит в нем значение BP. Команда LES BX,[BP]+A
помещает адрес параметра в пару регистров ES:BX. Используя этот
адрес, программа помещает четырехбайтовое значение текущего времени
в четырехбайтовую целую переменную.
Заметим, что процедура TIMER извлекает адрес параметра из стека
при выполнении команды возврата точно так же, как это делалось в
программах на языке Бейсик. Заметим также, что в этой ассемблерной
программе для идентификации имени TIMER используется оператор
PUBLIC. Делается это для того, чтобы редактор связей мог найти
подпрограмму и правильно связать ее с программой на Фортране. Для
интерпретатора Бейсика такой необходимости не было, поскольку
программа на Бейсике не редактировалась совместно с программой на
языке ассемблера.
Процедура Бэйсика BLOAD
Процедура Бэйсика BLOAD
При первом способе подпрограмма на машинном языке добавляется к
программе для интерпретатора Бэйсика. Пожалуй, использование
интерпретатора Бейсика является наиболее распространенным методом
написания программ для персональной ЭВМ. Подпрограмма на языке
ассемблера, которую предполагается включить в разрабатываемую
программу, довольно велика - более 100 байт. Ассемблерную процедуру
такой длины трудно вставить в текст программы на языке Бейсик,
способ, позволяющий сделать это будет рассмотрен в следующем
примере.
Функция, которую мы добавляем к программе на языке Бейсик
позволяет выводить на принтер графические изображения. IBM PC
снабжена графическими средствами. Графические команды позволяют
программно управлять отдельными точками, выводимыми на принтер, во
многом подобно тому, как в графическом режиме адаптер цветного
дисплея дает программисту возможность управлять отдельными точками
растра. На Фиг. 10.4 представлены графические команды, которые
потребуются в рассматриваемом примере. Практически, графические
функции реализуются на принтере через управляющие последователь-
ности символов. Вместо символа в коде ASCII программа выдает на
принтер служебный символ (27 в коде ASCII). Следующие за ним
символы задают уже не символы для вывода на печать, а определенные
действия принтера. Как видно из Фиг. 10.4, существуют команды для
вывода на принтер изображения точки, в результате выполнения
которых печатается определенная точка изображения.
Команда Действие
---------------------------------------------------------
ESC + "3" + n Установка промежутка между
строками n/216
Esc + "K" +n1+n2+v1...vk Печатать образы точек v1...vk
(k = n1 + 256*n2) как 480 точек поперек страницы
---------------------------------------------------------
Фиг.10.4 Графические команды для принтера
Приведенная на Фиг. 10.5 программа использует указанные команды
для вывода на принтер образа экрана графического дисплея размером
320*200 точек. Каждая точка растра передается на принтер. Если
точка на экране имеет цвет фона, то соответствующая точка на печать
не выдается. Если точка окрашена в один из трех основных цветов, то
программа выводит на печать черную точку. Эта программа не
масштабирует изображение, поэтому окружность на экране может
отобразиться в эллипс на принтере. Между тремя основными цветами не
проводится различий. Цветное изображение превращается в
черно-белое.
Подпрограмма PRINT_SCREEN является процедурой типа FAR. Вызов
ее из языка Бейсик является вызовом типа FAR, поэтому и возврат в
программу должен быть соответствующего типа. Последовательность ESC
+ "3" + 24 устанавливает такой интервал между строками печати, что
один ряд точек вплотную примыкает к другому. В печатающей головке
имеется восемь иголок, расстояние между которыми равно 1/72 дюйма.
Если сделать интервал между строками равным 8/72 дюйма (или 24/216
дюйма), то ряды точек соединятся. Приведенный фрагмент программы
показывает способ пересылки на устройство печати последовательности
служебных символов. Управляющая последовательность символов и чисел
пересылается на принтер как обычные символы. Остальное обеспечивает
устройство печати.
При каждом проходе печатающей головки на бумаге остается по
восемь рядов точек (по одному на каждую иголку печатающей головки)
в каждой из 320 колонок. От метки NEXT_ROW в программе
последовательность ESC + "K" + 64 + 1 пересылается на принтер. Это
означает, что последующие 320 байт (64 + 1*256) являются образами
точек для получения графического изображения на принтере. В
графическом режиме "K" на принтере можно получить изображение
шириной до 480 точек.
Для считывания точек с дисплея программа использует видео
функцию BIOS . Эта функция считывает восемь рядов точек текущнго
столбца и собирает их в один байт: "1" означает, что точка имеет
основной цвет и должна появиться на бумаге. Цикл продолжается через
метку NEXT_COLUMN - до тех пор, пока все 320 столбцов (что
соответствует 320 байтам) не будут переданы на принтер . После
перехода принтера на новую строку при помощи служебных символов
"возврат каретки" и "перевод строки" (13 и 10 в коде ASCII),
программа пересылает следующую группу из восьми рядов. За 25
проходов печатающей головки выводятся все 200 рядов. Возврат в
интерпретатор Бейсика производится при помощи возврата типа FAR.
В рассматриваемой программе было бы удобно использовать
процедуру PRINT. Эта процедура выдает один байт на принтер при
помощи функции печати BIOS. Функция помещает необходиые для базовой
системы ввода-вывода установки регистров в определенное место. Если
не использовать указанную функцию, то программа сама должна была бы
устанавливать регистры AH и DX равными нулю перед каждым вызовом
процедуры PRINT.
A
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:06:57
Фиг. 10.5 Печать графической копии дисплея Page 1-1
PAGE ,132
TITLE Фиг. 10.5 Печать графической копии дисплея
= 001B ESC EQU 27 ; Символ Escape
0000 CODE SEGMENT
ASSUME CS:CODE
0000 PRINT_SCREEN PROC FAR
0000 B0 1B MOV AL, ESC ; Установка перевода строки на 1/8 дюйма
0002 E8 0060 R CALL PRINT
0005 B0 33 MOV AL, '3'
0007 E8 0060 R CALL PRINT
000A B0 18 MOV AL, 24 ; 1/8 = 24/216 дюйма
000C E8 0060 R CALL PRINT
000F BA 0000 MOV DX, 0 ; Номер строки
0012 NEXT_ROW:
0012 B0 1B MOV AL, ESC
0014 E8 0060 R CALL PRINT
0017 B0 4B MOV AL, 'K'
0019 E8 0060 R CALL PRINT
001C B0 40 MOV AL, 320-256
001E E8 0060 R CALL PRINT
0021 B0 01 MOV AL, 1
0023 E8 0060 R CALL PRINT
0026 B9 0000 MOV CX, 0 ; Номер столбца
0029 NEXT_COLUMN:
0029 52 PUSH DX ; Сохранение номера строки
002A BB 0008 MOV BX, 8 ; Число одновлеменно обрабатываемых точек
002D NEXT_DOT:
002D D0 E7 SHL BH, 1 ; Освобождение младшего разряда
002F B4 0D MOV AH, 13 ; Чтение цвета точки из памяти дисплея
0031 CD 10 INT 10h
0033 0A C0 OR AL, AL
0035 74 03 JZ BACKGROUND ; Проверка на цвет фона
0037 80 CF 01 OR BH, 1 ; Не фон, необходимо вывести точку на печать
003A BACKGROUND:
003A 42 INC DX ; Переключение на следующую строку
003B FE CB DEC BL ; Уменьшение счетчика строк в данном проходе
003D 75 EE JNZ NEXT_DOT
003F 8A C7 MOV AL, BH ; Печать 8-ми точек
0041 E8 0060 R CALL PRINT ; Вывод на печать
0044 5A POP DX ; Восстановление номера строки начала прохода
0045 41 INC CX ; Переключение на следуюий столбец
0046 81 F9 0140 CMP CX, 320 ; Все столбцы выведены?
004A 75 DD JNZ NEXT_COLUMN
004C B0 0D MOV AL, 13 ; Переход на следующую строку на принтере
004E E8 0060 R CALL PRINT
0051 B0 0A MOV AL, 10
0053 E8 0060 R CALL PRINT
0056 83 C2 08 ADD DX, 8 ; Переключение на слдующую группу из 8 строк
0059 81 FA 00C8 CMP DX, 200 ; Все строки выведены?
005D 72 B3 JB NEXT_ROW
005F CB RET ; Возврат в BASIC
Фиг. 10.5 Печать графического экрана (начало)
0060 PRINT_SCREEN ENDP
0060 PRINT PROC NEAR
0060 52 PUSH DX
0061 B4 00 MOV AH, 0 ; Печать символа, находящегося в регистре AL
0063 BA 0000 MOV DX, 0
0066 CD 17 INT 17h
0068 5A POP DX
0069 C3 RET
006A PRINT ENDP
006A CODE ENDS
ENDA
Фиг. 10.5 Печать графического экрана (продолжение)
Как же обратиться к этой процедуре из программы, написанной на
языке Бейсик? В языке Бейсик существуют два способа подключения
подпрограмм. Во время работы интерпретатор Бейсика использует
оставшуюся память системы (до 64 кбайт) в качестве рабочей области.
Если в системе более 96 кбайт памяти, часть памяти будет не
доступна для интерпретатора Бейсика. Лучше всего поместить нашу
процедуру в эту область. Если свободной области памяти нет, то
можно специально выделить некоторый объем памяти из рабочей области
интерпретатора Бейсика для хранения подпрограммы. В данном примере
подпрограмма будет храниться вне рабочей области интерпретатора
Бейсика. В следующем примере будет показано, как включить процедуру
в контролируемую интерпретатором Бейсика область памяти.
На Фиг. 10.6 показана последовательность действий для
подготовки подпрограммы к дальнейшему использованию.
Соответствующая информация приведена в приложении C справочника по
языку Бейсик. Программа (на Фиг. 10.6) предназначена для машины с
оперативной памятью 96 кбайт и более. Программа ассемблируется
обычным образом. При редактировании связей задается опция /H.
Редактор связей создает файл типа .EXE таким образом, что программа
загружается в верхние адреса оперативной памяти, а не с самого
низкого из доступных адресов.
Чтобы подключить процедуру к программе, написанной на языке
Бейсик, нам потребуется программа DEBUG. После загрузки программы
на языке Бейсик во время работы программы DEBUG и уточнения
значений регистров, загружаем процедуру на языке ассемблера.
Приведенный пример реализован на машине с памятью 128 кбайт.
Значение регистра CS, равное 1FF9H, указывает на то, что программа
помещена в 70H байт от конца оперативной памяти. Заметим, что
рассматриваемая программа имеет объем около 6AH байт, так что
редактор связей разместил программу с самого старшего адреса
памяти, допускающего выравнивание по границе параграфа. Следует
также заметить, что эта программа является сегментно -
перемещаемой. Это означает, что ее можно перемещать в памяти,
поскольку первая ее команда имеет смещение 0 относительно текущего
сегмента кода. При переносе этой программы на машину с большим или
меньшим объемом памяти эта особенность оказывается решающей.
A
B>A:MASM FIG10-5,,,;
The IBM Persona Computer MACRO Assembler
Version 1.00 (C)Copyright IBM Corp 1981
Warning Severe
Errors Errors
0 0
B>A:LINK FIG10-5,,,/H;
IBM Personal Computer Linker
Version 1.10 (C)Copyright IBM Corp 1982
Warning: No STACK segment
There was 1 error detected
B>A:DEBUG A:BASIC.COM
-R
AX=0000 BX=0000 CX=2B80 DX=0000 SP=FFF0 BP=0000 SI=0000 DI=0000
DS=04B5 ES=04B5 SS=04B5 CS=04B5 IP=0100 NV UP DI PL NZ NA PO NC
04B5:0100 E91329 JMP 2A16
-NFIG10-5.EXE
-L
-R
AX=0000 BX=0000 CX=006A DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=04B5 ES=04B5 SS=1FF9 CS=1FF9 IP=0000 NV UP DI PL NZ NA PO NC
1FF9:0000 B01B MOV AL,1B
-RSS
SS 1FF9
:4B5
-RCS
CS 1FF9
:4B5
-RIP
IP 0000
:100
-G
---- В интерпретаторе Бэйсика введите команды
DEF SEG = &H1FF9
BSAVE "FIG10-5",0,&H70
Фиг. 10.6 (а) Создание подпрограммы для Бэйсика
B>A:MASM FIG10-5,,,;
The IBM Persona Computer MACRO Assembler
Version 1.00 (C)Copyright IBM Corp 1981
Warning Severe
Errors Errors
0 0
B>A:LINK FIG10-5,,,/H;
IBM Personal Computer Linker
Version 1.10 (C)Copyright IBM Corp 1982
Warning: No STACK segment
There was 1 error detected
B>A:DEBUG A:BASIC.COM /M:&H8000
-R
AX=0000 BX=0000 CX=2B80 DX=0000 SP=FFF0 BP=0000 SI=0000 DI=0000
DS=04B5 ES=04B5 SS=04B5 CS=04B5 IP=0100 NV UP DI PL NZ NA PO NC
04B5:0100 E91329 JMP 2A16
-NFIG10-5.EXE
-L
-R
AX=0000 BX=0000 CX=006A DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=04B5 ES=04B5 SS=0FF9 CS=0FF9 IP=0000 NV UP DI PL NZ NA PO NC
0FF9:0000 B01B MOV AL,1B
-RSS
SS 0FF9
:4B5
-RCS
CS 1FF9
:4B5
-RIP
IP 0000
:100
-G
---- В интерпретаторе Бэйсика; введите команды
DEF SEG = &H0FF9
BSAVE "FIG10-5",0,&H70
(b)
A
Фиг. 10.6 (а) Создание подпрограммы для Бэйсика; (b) Создание
подпрограммы для Бэйсика на машине с 64K
Теперь мы имеем дело с интерпретатором Бейсика. Нам необходимо
восстановить содержимое регистров таким, каким оно было после
загрузки Бейсика. После того, как интерпретатор Бейсика запущен,
для локализации подпрограммы используется команда DEF SEG. Команда
BSAVE помещает обратно на дискету объектный код, готовый к новой
загрузке из Бейсика при помощи команды BLOAD.
В части (b) Фиг. 10.6, повторяются действия из части (а), но
для машины с объемом памяти 64 кбайт. Различие здесь состоит в том,
что интерпретатор Бейсика не может использовать всю память под
рабочую область. Опция /M в командной строке Бейсика ограничивает
рабочую область Бейсика и оставляет место для подпрограммы.
Аналогичная команда потребуется и при запуске программы.
Программой DEBUG можно воспользоваться для вашей ассемблерной
процедуры и при работе интерпретатора Бейсика . Включите в команду
G при запуске программы Бейсика установку контрольной точки в
подпрограмме. По достижении контрольной точки работа
интерпретатора Бейсика приостанавливается, и производится обычная
для программы DEBUG выдача содержимого регистров.
Теперь все готово для выполнения подпрограммы на языке
ассемблера как части программы на языке Бейсик. Снова предположим,
что система располагает 128 кбайтами памяти, и выполним следующую
последовательность действий:
введите "BASIC" на уровне команд DOS;
введите "SCREEN1" во время работы интерпретатора Бейсика.
Фиг. 10.7 Печатная копия экрана
Эти действия вводят нас в интерпретатор и переводят режим
изображения на размер 320*200 точек. На Фиг. 10.7. показаны
оставшиеся события этой истории. Команда BLOAD загружает процедуру
в ту же область памяти, из которой она была сохранена. При желании
в команде BLOAD можно задать параметры, обеспечивающие загрузку
программы в другую область памяти. Оператор LINE дает процедуре
графической распечатки экрана информацию для печати. Для вызова
этой процедуры мы используем команду DEF SEG, чтобы установить
значение регистра CS на процедуру. Значение регистра IP для
процедуры помещается в простую переменную. Оператор CALL
осуществляет дальний вызов по заданному адресу. Фиг. 10.7
представляет собой копию реальной распечатки, полученной при
выполнении приведенной программы.
Если в системе 64Кбайт памяти, то программа будет отлична в
двух аспектах. Для вызова интерпретатора Бейсика используется
команда BASIC/M:&H8000, резервирующая верхнюю часть памяти для
нашей ассемблерной процедуры, а команда DEF SEG задает адрес
подпрограммы, как это было сделано в части (b) Фиг. 10.6.
Процедуры на языке Ассемблера
Процедуры на языке Ассемблера
Помимо постоянно находящихся в памяти драйверов и автономных
программ, язык ассемблера используется и для подпрограмм в больших
программах, написанных, как правило, на языке высокого уровня.
Такие языки высокого уровня как Бейсик или Паскаль позволяют быстро
и ясно писать большие программы. Однако эти языки позволяют делать
не все, что может понадобиться. Сказанное особенно справедливо для
персональных ЭВМ, поскольку хорошая прикладная программа здесь
требует использования всех возможностей машины. Достигнуть этого на
языке высокого уровня удается не всегда. Либо язык высокого уровня
не позволяет реализовать требуемую функцию (например, вызов
процедуры BIOS), либо накладные расходы языка делают прикладную
программу слишком медленной (например, за счет операторов Бейсика
PEEK и POKE для считывания конкретных ячеек памяти).
К счастью, в языках высокого уровня имеется механизм,
позволяющий вызывать подпрограммы, написанные на языке ассембоера.
Требуемую функцию можно выполнить быстро и эффективно на машинном
языке, а затем вернуться к языку высокого уровня для выполнения
остальной работы. В этом разделе будут приведены примеры,
иллюстрирующие два способа включения процедуры ассемблера в
программу, написанную на языке высокого уровня.
Расширение системы BIOS
Расширение системы BIOS
Некоторех программы - драйверы устройств мы хотим загружать в
память так, чтобы они становились постоянным расширением системы.
Хорошим примером программы такого типа является BIOS. Поместив
драйверы устройств в ПЗУ, фирама IBM сделала их постоянной частью
системы. Эти процедуры доступны для всех программ, запускаемых на
IBM PC, поскольку BIOS всегда находится в памяти.
Однако большинство из нас не может позволить себе роскошь
помещать свои программы в ПЗУ. Если программа не предназначена для
широкого тиражирования, то нет никакого смысла тратить тысячи
долларов на производство единичного модуля ПЗУ. Однако, существует
более дешевая альтернатива. Разработаны ПЗУ специальных типов,
которые пользователь может программировать самостоятельно.
Некоторые компании поставляют программаторы ППЗУ (программируемые
ПЗУ - PROM в англоязычной аббревиатуре), которые позволяют вам
помещать свои программы в постоянную память. Специальное
техническое обеспечение для программирования ППЗУ стоит несколько
сотен долларов, а отдельные модули ППЗУ - от 10 до 50 долларов.
Для некоторых программ вам может понадобиться этот способ. В
IBM PC имеется свободное гнездо для подключения модуля ПЗУ. К этому
гнезду можно подключить стандартное ПЗУ на 8 кбайт или ППЗУ. Это
сделает программу постоянной частью ЭВМ. Мы не будем более подробно
обсуждать установку ПЗУ и ППЗУ. Для этого требуется специальное
техническое обеспечение, и эта процедура различна для разных
пользователей.
Вместо этого будут рассмотрены способы загрузки программы в
оперативную память таким образом, чтобы она стала постоянной частью
системы. Программа будет находиться в памяти вплоть до выключения
машины. Преимуществом в данном случае является то, что такая
функция встроена в ЭВМ не навсегда. Ее можно изменять не разбирая
машину. Если вы обнаружите в программе какие-либо недостатки, то ее
можно модифицировать без повторения всех манипуляций с ПЗУ.
Расширение системы BIOS
Расширение системы BIOS
Некоторех программы - драйверы устройств мы хотим загружать в
память так, чтобы они становились постоянным расширением системы.
Хорошим примером программы такого типа является BIOS. Поместив
драйверы устройств в ПЗУ, фирама IBM сделала их постоянной частью
системы. Эти процедуры доступны для всех программ, запускаемых на
IBM PC, поскольку BIOS всегда находится в памяти.
Однако большинство из нас не может позволить себе роскошь
помещать свои программы в ПЗУ. Если программа не предназначена для
широкого тиражирования, то нет никакого смысла тратить тысячи
долларов на производство единичного модуля ПЗУ. Однако, существует
более дешевая альтернатива. Разработаны ПЗУ специальных типов,
которые пользователь может программировать самостоятельно.
Некоторые компании поставляют программаторы ППЗУ (программируемые
ПЗУ - PROM в англоязычной аббревиатуре), которые позволяют вам
помещать свои программы в постоянную память. Специальное
техническое обеспечение для программирования ППЗУ стоит несколько
сотен долларов, а отдельные модули ППЗУ - от 10 до 50 долларов.
Для некоторых программ вам может понадобиться этот способ. В
IBM PC имеется свободное гнездо для подключения модуля ПЗУ. К этому
гнезду можно подключить стандартное ПЗУ на 8 кбайт или ППЗУ. Это
сделает программу постоянной частью ЭВМ. Мы не будем более подробно
обсуждать установку ПЗУ и ППЗУ. Для этого требуется специальное
техническое обеспечение, и эта процедура различна для разных
пользователей.
Вместо этого будут рассмотрены способы загрузки программы в
оперативную память таким образом, чтобы она стала постоянной частью
системы. Программа будет находиться в памяти вплоть до выключения
машины. Преимуществом в данном случае является то, что такая
функция встроена в ЭВМ не навсегда. Ее можно изменять не разбирая
машину. Если вы обнаружите в программе какие-либо недостатки, то ее
можно модифицировать без повторения всех манипуляций с ПЗУ.
Возврат программы в DOS с сохранением ее резидентности
Возврат программы в DOS с сохранением ее резидентности
Первый способ написания и загрузки постоянной функции в DOS состоит
в том, чтобы, возвращая управление DOS, программа оставалась в
памяти резидентной. Такую функцию существляет прерывание INT 27H.
Обычно для выхода в DOS используется прерывание INT 20H, либо
программа производит переход по адресу 0 программного префикса, как
мы делали в программах типа .EXE. В результате управление
возвращается DOS. Операционная система освобождает память,
предоставленную этой программе. Следующую программу, которая
загружается после прерывания INT 20H, DOS помещает в ту же область
памяти, которая использовалась для предыдущей.
Выход в DOS через прерывание INT 27H отличается от
рассмотренного. Управление возвращается в DOS точно так же, как и в
случае прерывания INT 20H, но часть памяти, занимаемая программой,
не возвращается для дальнейшего использования. В регистре DX
указывает на адрес первой свободной ячейки после той области
памяти, котрую вы хотите зарезервировать. DOS резервирует эту
область памяти, как часть системы. Это означает, что ваша программа
становится частью DOS. Такую программу можно удалить из памяти
только перезагрузив DOS и начав все сначала.
Если выход в PC DOS осуществляется при помощи прерывания INT
27H, то в регистре CS должен находиться адрес программного
префикса. Легче всего это сделать, если писать использующую INT 21H
программу как .COM программу. Написать программу типа .EXE,
оставляющую при выходе содержимое регистров CS и DX корректным,
довольно трудно. Поскольку создание программ типа .COM было
рассмотрено в гл.5, будем считать, что все наши остающиеся
резидентными программы имеют тип .COM.
Рассматриваемый для прерывания DOS INT 27H пример довольно
сложен. Он иллюстрирует не только использование INT 27H, но и
способы замены существующей BIOS другой версией. В этом примере мы
даже применим несколько трюков с таймером для увеличения скорости
обработки.
Пример представлен на Фиг. 10.1. Приведенная здесь программа
предназначена для обслуживания буфера печати. Обычно при выдаче на
печать символа программа обращается к прерыванию INT 17H - драйверу
печати BIOS. Эта функция выдает символ на принтер после
проверки ошибок и ожидания готовности принтера. Как правило, при
этом обеспечивается достаточная производительность. Но допустим,
что вы пишете несколько программ и хотите вывести их на принтер.
Если вы попытаетесь сделать это, то не сможете обратиться к системе
до тех пор, пока принтер не закончит работу. Чтобы
продолжить редактирование или ассемблирование другой части
программы, вам придется ждать завершения печати.
A
Microsoft (R) Macro Assembler Version 5.00 4/2/89 16:06:27
Фиг. 10.1 Буфер для печати Page 1-1
PAGE ,132
TITLE Фиг. 10.1 Буфер для печати
0000 ABS0 SEGMENT AT 0
0020 ORG 4*8H
0020 ???????? TIMER_INT DD ? ; Аппратное прерывание от таймера
005C ORG 4*17H
005C ???????? PRINTER_INT DD ? ; Прерывание к BIOS для печати
0408 ORG 408H
0408 ???? PRINTER_BASE DW ? ; Базовый адрес адаптера принтера
040A ABS0 ENDS
0000 CODE SEGMENT
0100 ORG 100H
ASSUME CS:CODE,DS:CODE,ES:CODE
0100 EB 09 90 JMP START
0103 ???????? PRINT_VECTOR DD ? ; Место для хранения исходного вектора 17h
0107 ???????? TIMER_VECTOR DD ? ; Место для хранения исходного вектора 9h
010B START:
010B 2B C0 SUB AX,AX ; Установка регистра ES на сегмент ABS0
010D 8E C0 MOV ES,AX
ASSUME ES:ABS0
010F 26: A1 005C R MOV AX,WORD PTR PRINTER_INT
0113 26: 8B 1E 005E R MOV BX,WORD PTR PRINTER_INT+2
0118 26: 8B 0E 0020 R MOV CX,WORD PTR TIMER_INT
011D 26: 8B 16 0022 R MOV DX,WORD PTR TIMER_INT+2
0122 A3 0103 R MOV WORD PTR PRINT_VECTOR,AX
0125 89 1E 0105 R MOV WORD PTR PRINT_VECTOR+2,BX
0129 89 0E 0107 R MOV WORD PTR TIMER_VECTOR,CX
012D 89 16 0109 R MOV WORD PTR TIMER_VECTOR+2,DX
;----- Во время занесения векторов прерываний прерывания запрещены
0131 FA CLI
Фиг. 10.1 Буфер печати (начало)
0132 26: C7 06 005C R 0162 MOV WORD PTR PRINTER_INT,offset PRINT_HANDLER
R
0139 26: 8C 0E 005E R MOV WORD PTR PRINTER_INT+2,CS
013E 26: C7 06 0020 R 0196 MOV WORD PTR TIMER_INT,offset TIMER_HANDLER
R
0145 26: 8C 0E 0022 R MOV WORD PTR TIMER_INT+2,CS
014A B0 36 MOV AL,00110110b
014C E6 43 OUT 43H,AL
014E B0 00 MOV AL,0 ; Увеличение скорости работы таймера в 256 раз
0150 E6 40 OUT 40H,AL
0152 B0 01 MOV AL,1
0154 E6 40 OUT 40H,AL
0156 FB STI
0157 8D 16 28FE R LEA DX,BUFFER_END ; Занесение адреса конца программы
015B CD 27 INT 27H ; Выход с сохранением программы в памяти
015D 00 TIMER_COUNT DB 0
015E 01EE R BUFFER_HEAD DW BUFFER_START
0160 01EE R BUFFER_TAIL DW BUFFER_START
;----- Эта подпрограмма управляет вызовом прерывания 17h
0162 PRINT_HANDLER PROC FAR
ASSUME CS:CODE,DS:nothing,ES:nothing
0162 0A E4 OR AH,AH
0164 74 05 JZ BUFFER_CHARACTER ; Проверка на функцию вывода символа
0166 2E: FF 2E 0103 R JMP PRINT_VECTOR ; Переход на стандартный обработчик
; прерывания 17h
016B BUFFER_CHARACTER:
016B FB STI
016C 53 PUSH BX
016D 51 PUSH CX
016E 56 PUSH SI
016F 2B C9 SUB CX,CX ; Счетчик отсчетов таймера
0171 PRINT_LOOP:
0171 2E: 8B 1E 0160 R MOV BX,BUFFER_TAIL ; Выборка адреса конца буфера
0176 8B F3 MOV SI,BX
0178 E8 01E2 R CALL ADVANCE_POINTER ; Перемещение указателя на следующий байт
017B 2E: 3B 1E 015E R CMP BX,BUFFER_HEAD ; Проверка на наличие места в буфере
0180 74 0E JE BUFFER_FULL ; Нет места,ожидается пока оно появится
0182 2E: 88 04 MOV CS:[SI],AL ; Вывод символа в буфер
0185 2E: 89 1E 0160 R MOV BUFFER_TAIL,BX ; Занесение нового адреса конца буфера
018A B4 00 MOV AH,0 ; Код возврата из прерывания 17h
018C PRINT_RETURN:
018C 5E POP SI
018D 59 POP CX
018E 5B POP BX
018F CF IRET
0190 BUFFER_FULL:
0190 E2 DF LOOP PRINT_LOOP ; Повторить цикл проверки занятости буфера
0192 B4 01 MOV AH,1 ; Буфер занят слишком долго,ошибка
0194 EB F6 JMP PRINT_RETURN
0196 PRINT_HANDLER ENDP
Фиг. 10.1 Буфер печати (продолжение)
;----- Эта программа вызывает 4660 раз в секунду
0196 TIMER_HANDLER PROC FAR
ASSUME CS:CODE,DS:nothing,ES:nothing
0196 50 PUSH AX
0197 53 PUSH BX
0198 2E: 8B 1E 015E R MOV BX,BUFFER_HEAD
019D 2E: 3B 1E 0160 R CMP BX,BUFFER_TAIL ; Есть ли что-нибудь в буфере?
01A2 75 14 JNZ TEST_READY ; Переход,если буфер не пуст
;----- Эта подпрограмма управляет таймером в скоростном режиме
01A4 TIMER_RETURN:
01A4 5B POP BX
01A5 2E: FE 06 015D R INC TIMER_COUNT ; Увеличение счетчика делителя таймера
01AA 75 06 JNZ SKIP_NORMAL
01AC 58 POP AX ; Это выполняется один раз на 256 прерываний
01AD 2E: FF 2E 0107 R JMP TIMER_VECTOR ; Переход на стандартную программу обработки
; прерывания от таймера
01B2 SKIP_NORMAL:
01B2 B0 20 MOV AL,20H
01B4 E6 20 OUT 20H,AL ; Конец прерывания
01B6 58 POP AX
01B7 CF IRET
;----- Символ в буфере,производится попытка напечатать его
01B8 TEST_READY:
01B8 52 PUSH DX
01B9 1E PUSH DS
01BA 2B D2 SUB DX,DX
01BC 8E DA MOV DS,DX ; Установка регистра DS на сегмент ABS0
ASSUME DS:ABS0
01BE 8B 16 0408 R MOV DX,PRINTER_BASE
01C2 42 INC DX ; Установка на порт состояния
01C3 EC IN AL,DX
01C4 A8 80 TEST AL,80H ; Проверка готовности принтера
01C6 74 16 JZ NO_PRINT
01C8 4A DEC DX ; Установка на порт данных
01C9 2E: 8A 07 MOV AL,CS:[BX] ; Выбрка выводимого символа
01CC E8 01E2 R CALL ADVANCE_POINTER
01CF 2E: 89 1E 015E R MOV BUFFER_HEAD,BX
01D4 EE OUT DX,AL ; Вывод символа в порт принтера
01D5 83 C2 02 ADD DX,2 ; Установка на порт управления
01D8 B0 0D MOV AL,0DH
01DA EE OUT DX,AL ; Передача символа из порта в принтер
01DB B0 0C MOV AL,0CH
01DD EE OUT DX,AL
01DE NO_PRINT:
01DE 1F POP DS
01DF 5A POP DX
01E0 EB C2 JMP TIMER_RETURN ; Возврат через подпрограмму управления
01E2 TIMER_HANDLER ENDP ; таймером
01E2 ADVANCE_POINTER PROC NEAR
01E2 43 INC BX ; Сдвиг указателя
Фиг. 10.1 Буфер печати (продолжение)
01E3 81 FB 28FE R CMP BX,offset BUFFER_END
01E7 75 04 JNE ADVANCE_RETURN ; Проверка на конец циклического буфера
01E9 8D 1E 01EE R LEA BX,BUFFER_START ; Установка указателя на начало буфера
01ED ADVANCE_RETURN:
01ED C3 RET
01EE ADVANCE_POINTER ENDP
01EE BUFFER_START LABEL BYTE
01EE 2710[ DB 10000 DUP (?)
??
]
28FE BUFFER_END LABEL BYTE
28FE CODE ENDS
END
Фиг. 10.1 Буфер печати (продолжение)
Приведенная в примере программа может облегчить решение задачи.
Конечно, это не обойдется вам даром. Программа отводит под буфер
печати некоторую область памяти, котрая будет постоянно за ним
закреплена. DOS изымает эту область из общего объема памяти,
предоставляемой пользователю. Например, если в системе 96K байт
памяти, а 10 кбайт отводится под буфер печати, то пользоваться
Макроассемблером уже не удастся. Для макроассемблера требуется 96
кбайт, а после создания буфера печати останется лишь 86 кбайт.
Поэтому, прежде чем организовать буферизацию печати, убедитесь, что
в системе останется еще достаточный объем памяти.
Буферизация печати осуществляется примерно так. Стандартная
команда PRINT (INT 17H) заменяется процедурой, которая помещает
символы в буфер вместо того, чтобы посылать их на принтер. Эта
часть программы и называется буферизацией печати. Отдельная часть
программы, называемая выводом на печать, извлекает символы из
буфера печати и пересылает их на принтер.
Основным моментом в данном примере является замена прерывания
INT 17H базовой системы ввода-вывода. Почти все прикладные
программы для вывода на печать используют именно это прерывание, а
это означает, что теперь все обычные операции печати будут
приводить к пересылке символов в подпрограмму буферизации печати, а
не на принтер. В частности, в нашем примере, мы можем
листинг ассемблирования вывести на принтер, нажав клавиши
Ctrl-PrtSc, служащие для пересылки символов с экрана на печать.
Когда мы выводим листинг ассемблирования с программой
буферизации печати в памяти, символы поступают в буфер в памяти, а
не на принтер. Буферизация очень незначительно
увеличивает время просмотра. Когда файл выведен на экран (и в буфер
печати), управление возвращается DOS. Вы можете прекратить
пересылку символов на принтер, снова нажав клавиши Ctrl-PrtSc.
Листинговый файл находится в буфере, и DOS готова продолжить
выполнение других заданий, например, редактирование или
ассемблирование.
Затем начинает выполняться вторая часть программы. Эта
процедура извлекает символы из буфера и пересылает их на принтер.
Она управляется прерыванием от таймера. При каждом прерывании от
таймера процедура вывода на печать также получает управление. Если
в буфере имеется символ, и если устройство печати находится в
состоянии "готово", то подпрограмма пересылает этот символ на
принтер. Таким образом, символы извлекаются из буфера и
пересылаются на принтер со скоростью работы этого устройства.
Поскольку программа вывода на печать работает в фоновом режиме,
одновременно могут выполняться другие задания, например,
редактирование или ассемблирование.
Обратимся к программе, представленной на Фиг. 10.1, и
рассмотрим, как взаимодействуют ее компоненты. Во-первых, в ней
описан сегмент ABS0, содержащий вектор прерываний, с которым
программа имеет дело. Приведенная в примере программа заменяет как
прерывание вывода на печать INT 17H, так и прерывание от таймера
INT 8. Заметим также, что в сегменте ABS0 определяется адрес
PRINTER_BASE. В этой ячейке находится базовый адрес для устройства
печати 0. В данном примере предполагается, что все операции печати
производятся на системном устройстве печати.
Сегмент CODE - это та секция программы, которая остается
резидентной. При помощи команды ORG 100H мы составили эту программу
как файл типа .COM. Это означает, что для создания из выходного
файла редактора связей файла типа .COM, необходимо выполнить
описанную в гл.5 последовательность действий. Для хранения исходных
значений вектора печати и вектора таймера в программе используются
области памяти PRINT_VECTOR и TIMER_VECTOR. Хотя программа заменяет
значения этих векторов, при выводе на печать в ней должны быть
известны их исходные значения.
Первая часть сегмента CODE, начиная с метки START, является
инициализирующей частью программы. В ней считываются исходные
значения векторов прерываний и сохраняются в области данных
сегмента CODE. В процедуре инициализации векторы прерываний в
нижних адресах памяти заменяются новыми, используемыми в процедурах
буферизации и вывода на печать. Обратите внимание на команду CLI,
которая блокирует прерывания перед выполнением этой операции.
Поскольку программа изменяет прерывание таймера, она не может
допустить обработку его шага в этот момент времени. Если бы
прерывание от таймера произошло в тот момент, когда программа
изменила только одно из двух слов вектора прерываний от таймера, то
микропроцессор продолжил бы выполнение с непредсказуемого адреса
памяти. Разумнее запретить прерывания, чем допустить возможность
перехода по неизвестному адресу.
Прежде чем разблокировать прерывания, программа изменяет
текущее значение счетчика таймера. Обычно прерывания от таймера
происходят примерно 18 раз в секунду. Устройство печати может
печатать по 80 символов в секунду. Если бы процедура вывода на
печать выдавала по одному символу при каждом прерывании от таймера,
то максимальная скорость печати составила бы 18 символов в секунду.
Если ускорить таймер, прерывания от таймера будут происходить чаще.
Это позволит программе выдавать на печать все 80 символов в
секунду. В приведенном примере в таймер загружается значение
счетчика 256, оно в 256 раз меньше стандартного значения.
Компенсируется это увеличение скорости при помощи процедуры
TIMER_HANDLER.
Процедура инициализации возвращает управление в DOS при помощи
прерывания INT 27H. Перед выходом из процедуры в регистр DX
загружается указатель на байт, сразу следующий за последнм байтом
всей программы. Заметим, что все процедуры и буфер печати мы
расположили в пределах этой области памяти. В соответствии с
правилами действия прерывания INT 27H DOS не затронет эту
область.
Приведенная программа зря расходует часть памяти.
Инициализирующая ее часть выполняется только один раз, поэтому нет
смысла оставлять ее в памяти. Можно оптимизировать программу
поместив часть кода от команды START до INT 27H после метки
BUFFER_END. В этом случае при прерывании INT 27H инициализирующая
часть программы оказалась бы за пределами защищаемой области
памяти, и следующая загружаемая DOS программа перекрыла бы
процедуру инициализации. Экономия около 90 байт из более чем 10000
байт в нашем примере не впечетляет, но она вполне доступна в случае
необходимости.
Далее следует процедура PRINT_HANDLER. Эта подпрограмма
вместо базовой системы ввода-вывода осуществляет управление
принтером при каждом обращении программ к прерыванию INT 17H для
вывода данных на печать. Первые три команды управляют перехватом
управления у BIOS. Наша процедура работает только тогда, когда
должен быть напечатан символ (AH = 0). При любом другом коде
функции работу выполняет BIOS, поэтому программа производит
проверку, не равен ли регистр AH нулю. Если нет, то производится
косвенный переход с использованием сохраненного значения исходного
вектора печати. В результате управление передается процедуре
входящей в BIOS, которая выполняет требуемую функцию. Сказанное
означает, что в нашей процедуре обработки прерывания достаточно
написать только поддержку сделанных изменений.
Относительно рассмотренного способа управления печатью следует
сделать два замечания. Во-первых, передача дальше всех функций
печати кроме случая AH = 0 - не блестящая идея. Если какая-либо
программа инициализирует принтер (AH = 2) во время работы механизма
буферизации, то BIOS берет управление на себя и выдает на принтер
команду RESET. Эта команда обрывает ту строку, которая в это время
выводится на печать, что в большинстве случаев приводит к потере
одного или нескольких символов. Если вы хотите сделать эту
программу более защищенной от ошибок, то вам придется рассмотреть
вопрос об управлении всеми функциями печати.
Второе, на что следует обратить внимание - это использование
сохраненного вектора прерываний печати. Можно было бы обратиться к
листингу BIOS, приведенному в техническом справочнике, и найти
начальный адрес процедуры печати. Затем включить этот адрес
непосредственно в код программы так же, как это делается для других
абсолютных адресов. Однако в результате программа оказалась бы
жестко к этому адресу в системе BIOS. Если фирма IBM изменит
процедуры BIOS и, таким образом, - адрес процедуры печати, то
рассмотренная программа не сможет больше работать. Конечно, если
пишите эту программу для своей собственной машины, а покупать новую
или продавать свою программу не собираетесь, то указанных проблем
не возникнет. Однако в общем случае надо избегать использования
абсолютных адресов, если есть выбор. В приведенном примере
процедура инициализации легко может использовать вектор
прерываний печати для определения адреса процедуры печати
BIOS в ПЗУ.
В оставшейся части процедуры PRINT_HANDLER символ помещается в
буфер печати. Перед тем, как поместить символ программа проверяет,
есть ли в буфере место. Если буфер полон, программа ждет, пока
освободится место. Это ожидание не вызовет проблем, поскольку и
стандартная процедура BIOS ждет, чтобы принтер был готов принять
символ. Из соображений безопасности в регистре CX накапливается
число проходов по ветви "занято". Если это число становится равным
64K, а буфер по-прежнему полон, то это может означать какой-то
сбой. В этом случае процедура PRINT_HANDLER так же, как и BIOS,
выдает сообщение о превышении допустимого времени ожидания.
В приведенном примере процедура печати использует также
внутреннюю процедуру ADVANCE_POINTER. Эта несложная процедура
делает буфер печати циклическим. Если указатель сдвигается за
пределы буфера, подпрограмма переносит его на начало буфера. Она
аналогична процедуре BIOS для буфера клавиатуры. Только в данном
случае в буфер помещается 10000 символов, а не 16.
Интересно рассмотреть работу процедуры TIMER_HANDLER из
приведенного примера. Инициализирующая процедура связывает эту
подпрограмму с аппаратным прерыванием от таймера, поэтому на каждом
цикле таймера она получает управление. Помимо пересылки кодов на
принтер, эта процедура должна обеспечивать, через компенсацию
ускоения таймера, выполнение его обычных функций, таких как
ведение времени дня.
Сначала процедура работы с таймером проверяет, имеются ли
предназначенные для вывода на печать символы. Нет смысла пытаться
переслать символы на принтер, если пересылать нечего. Если в буфере
нет символов, процедура проходит на метку TIMER_RETURN. Этот
фрагмент процедуры обслуживает ускорение таймера.
Метка TIMER_RETURN указывает часть программы, обеспечивающую
нормальное функционирование таймера. При каждом прерывании от
таймера значение байта TIMER_COUNT увеличивается на единицу. Если
этот байт не нулевой, то процедура выходит из прерывания после
выдачи сигнала о завершении прерывания на контроллер прерываний.
Если этот байт равен нулю, то выход из программы осуществляется
посредством косвенного перехода по сохраненному вектору прерывания
от таймера TIMER_VECTOR. При этом управление передается процедуре
BIOS для определения текущего времени и выключения дисковода.
Дублировать эти операции в нашей программе не требуется. Переход в
BIOS происходит только один из 256 раз выполнения подпрограммы
работы с таймером. Но поскольку скорость таймера была увеличена в
256 раз, процедура реакции на прерывание от таймера базовой системы
ввода-вывода по-прежнему будет получать управление 18,2 раза в
секунду. Это означает, что текущее время будет поддерживаться
правильно, и мотор дисковода будет выключен вовремя. Именно поэтому
и было выбрано ускорение таймера в 256 раз, хотя и ускорения в 5
раз было бы достаточно, чтобы обеспечить работу устройства печати с
максимальной скоростью.
Ускорение таймера в 256 раз было выбрано потому, что это было
просто сделать. Однако если брать в расчет производительность, то
лучше было бы ускорить работу таймера в 5 раз, поскольку на
обработку каждого прерывания от таймера тратится по меньшей мере 10
микросекунд, и даже больше, если в буфере печати есть символы.
Время, затраченное на обработку прерываний, идет в ущерб выполнению
системой других заданий, например ассемблирования. При такой
частоте прерываний от таймера, становится заметным замедление
работы. Для оптимизации производительности следует ускорять таймер
менее, чем в 256 раз.
Что же происходит в процедуре работы с таймером, когда в буфере
есть символы, предназначенные для печати? Программа считывает порт
состояния, чтобы определить, готов ли принтер к приему символа.
Поскольку в процедуре используется базовый адрес из области данных
BIOS, то наша подпрограмма будет работать и с автономным адаптером
устройства печати, и с портом адаптера монохромного дисплея. Если
устройство печати не готово, процедура возвращает управление на
метку TIMER_RETURN, где в случае необходимости поддерживаются
стандартные функции таймера. Процедура вывода на печать не ждет,
когда устройство печати освободится, если оно занято. Мы знаем, что
прерывание от таймера очень скоро повторится, тогда мы и повторим
попытку вывода. Ожидание готовности устройства печати здесь
связывало бы бы всю систему. Результат был бы таким же, как и в
случае отсутствия буферизации печати.
Если принтер готов, программа извлекает символ из буфера и
передает его на принтер. И в данном случае программа вновь не
делает всего, что следовало бы. Подпрограмма, входящая в BIOS,
делает проверку на ситуацию ошибки при передаче каждого символа. То
же самое следовало бы делать и в нашей процедуре. Но что же
произойдет в случае сбоя? Если процедура вывода обнаружила ошибку,
то как она сможет сообщить программе, что это произошло во время
печати? В некоторых случаях к этому моменту программа передававшая
даные для печати уже завершила свою работу. Наилучший выход может
состоять в проверке ошибок при каждой пересылке символа на принтер
процедурой работы с таймером. При обнаружении ошибки процедура
PRINT_HANDLER должна выдать сообщение об ошибке, что далее все
программы будут производить вывод на печать через прерывание INT
17H. Возможно, это не идеальный вариант, но, вероятно, лучший.
Прежде чем закончить рассмотрение примера, следует обратить
внимание еще на одну проблему. Существуют и другие процедуры,
изменяющие частоту прерываний от таймера. BASICA - расширенная
версия интерпретатора Бейсика, для ускорения таймера используется
прием, во многом аналогичный приведенному. При вызове программы
BASICA после установки буферизованной печати, процедура
TIMER_HANDLER получает прерывания уже не с той частотой, которая
предполагается. Поскольку процедура TIMER_HANDLER ограничивает
передачу управления прерыванием от таймера процедуре BIOS, текущее
время замедлится в 256 раз. BASICA осуществляет также инициализацию
устройства печати, что, как мы уже видели, мешает выводу на печать.
Это означает, что программа буферизации печати будет работать не
для всех приложений. Однако она иллюстрирует использование
прерывания INT 27H для создания постоянной системной функции.
Приведенный пример иллюстрирует также метод переопределения
векторов BIOS для подцепления новой функции к уже имеющимся
программам.
Вставка короткой программы
Вставка короткой программы
В предыдущем примере рассматривалась довольно большая программа
на языке ассемблера, хранящаяся в собственном объектном файле и
загружаемая в память интерпретатором Бейсика. А как в случае очень
маленькой программы. Представляется, что для такой программы
тратилось бы слишком много усилий на одну только загрузку ее из
собственного файйла. В приложении C справочника по языку Бейсик
приведен способ "упаковки" программы на машинном языке в область
памяти за пределами рабочей области интерпретатора. Приведем пример
применения другого способа.
На Фиг. 10.8 показана программа, написанная на языке
ассемблера, которой мы воспользуемся. Эта программа обращается к
BIOS для сдвига изображения на экране. Рассмотрев параметры,
хранящиеся в регистрах CX и DX, можно увидеть, что сдвигаемое окно
отображает лишь часть экрана. Мы будем исползовать приведенную
программу для разбиения экрана на несколько окон, в каждом из
которых сдвиг может производиться независимо. Поскольку средства
реализации этого в языке Бейсик отсутствуют, понадобится процедура
на языке ассемблера.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:07:03
Фиг. 10.8 Программа прокрутки окон на дисплее Page 1-1
PAGE ,132
TITLE Фиг. 10.8 Программа прокрутки окон на дисплее
0000 CODE SEGMENT
ASSUME CS:CODE
0000 SCROLL PROC FAR
0000 55 PUSH BP
0001 8B EC MOV BP, SP
0003 8B 76 06 MOV SI, [BP+6] ; Загрузка адреса параметра
0006 8B 0C MOV CX, [SI] ; Загрузка параметра
0008 0A C0 OR AL, AL
000A B7 07 MOV BH, 7
000C B8 0601 MOV AX, 601h
000F 75 0C JNZ WINDOW1 ; Определение требуемого окна
0011 B9 0200 MOV CX, 200h ; Окно 1
0014 BA 1010 MOV DX, 1010h
0017 DO_SCROLL:
0017 CD 10 INT 10h
0019 5D POP BP
001A CA 0002 RET 2
001D WINDOW1:
001D B9 0514 MOV CX, 514h ; Окно 2
0020 BA 1224 MOV DX, 1224h
0023 EB F2 JMP DO_SCROLL
0025 SCROLL ENDP
0025 CODE ENDS
END
Фиг. 10.8 Процедура сдвига изображения для Бэйсика
Как можно увидеть на листинге ассемблирования на Фиг. 10.8, для
определения, в каком из двух окон должен производиться сдвиг,
используется входной параметр. Программа, написанная на языке
Бейсик, передает этот параметр в ассемблерную подпрограмму
оператором CALL. На Фиг.10.9(а) показано содержимое стека в момент
вызова в Бэйсике процедуры SCROLL. Оператор CALL помещает в стек
адрес параметра перед выполнением дальнего вызова (FAR CALL)
подпрограммы на машинном языке. Адрес в стеке является смещением
параметра относительно регистра DS. Первые команды процедуры SCROLL
извлекают этот адрес из регистра SI для того, чтобы загрузить
истинное значение в регистр CX. На Фиг.10.9(b) показано содержимое
стека после того, как процедура SCROLL поместила содержимое
регистра BP в стек, а затем переслала содержимое регистра SP в
регистр BP. Обратите внимание, что параметр находится в шести
байтах от вершины стека. Если бы программа на языке Бейсик
передавала более одного параметра, перед вызовом они были бы
аналогичным образом помещены в стек. Забегая вперед, заметим, что
перед возвратом процедура, используя команду RET 2, извлекает
параметры из стека. Интерпретатор Бейсика предполагает, что перед
возвратом подпрограмма удаляет параметры из стека.
ГДДДДДДДДДДДДґ ГДДДДДДДДДДДДґ
SPДДДДД>і Смещение і SPДДДД>і Старое зна-і
і возврата і і чение BP і<ДДДДBP
ГДДДДДДДДДДДДґ ГДДДДДДДДДДДДґ
і Сегмент і і Смещение і [BP+2]
і возврата і і возврата і
ГДДДДДДДДДДДДґ ГДДДДДДДДДДДДґ
і Смещение і і Сегмент і [BP+4]
і аргумента і і возврата і
ГДДДДДДДДДДДДґ ГДДДДДДДДДДДДґ
і Смещение і [BP+6]
і аргумента і
ГДДДДДДДДДДДДґ
(a) (b)
Фиг. 10.9 Стек при вызове процедуры
Подпрограмма SCROLL в зависимости от значения параметра
обрабатывает одно из двух окон экрана. Если параметр равен нулю,
то изображение в окне, заданном координатами (2, 0) и (16, 16)
сдвигается вверх на одну строку. Если параметр не равен нулю, то
на одну строку вверх сдвигается изображение в окне (5, 20), (18,
36). Перемещается текст только в заданном окне, остальной текст
или данные на экране остаются неподвижными. Реализация такого
оконного режима входит в функцию сдвига BIOS. Для ее использования
требуется лишь вызвать BIOS с правильно заданными параметрами.
На Фиг. 10.10 представлена программа на языке Бейсик,
обращающаяся к процедуре SCROLL. В этом простом примере в каждое
окно записывается строка символов, а затем вызывается процедура
сдвига текста вверх. Эта Бэйсик-программа не более чем
иллюстрирует использование сдвига окон.
Первое, на что следует обратить внимание, это - способ загрузки
программы на машинном языке в систему. Программа содержится в
символьной строке P$. Каждый символ в строке соответствует одному
байту объектного кода из Фиг. 10.8. В программу на Бэйсике эта
программа вводится с клавиатуры по листингу ассемблирования. Это -
одна из причин, по которой применение рассмотренного способа
ограничено лишь короткими программами. При вводе программы таким
способом очень легко сделать ошибки.
A
1 CLS
5 DEFINT A-Z
10 P$=CHR$(&H55)+CHR$(&H8B)+CHR$(&HEC)+CHR$(&H8B)+CHR$(&H76)+CHR$(&H6)
20 P$=P$+CHR$(&H8B)+CHR$(&HC)+CHR$(&HA)+CHR$(&HC9)+CHR$(&HB7)+CHR$(&H7)
30 P$=P$+CHR$(&HB8)+CHR$(&H1)+CHR$(&H6)+CHR$(&H75)+CHR$(&HC)+CHR$(&HB9)
40 P$=P$+CHR$(&H0)+CHR$(&H2)+CHR$(&HBA)+CHR$(&H10)+CHR$(&H10)+CHR$(&HCD)
50 P$=P$+CHR$(&H10)+CHR$(&H5D)+CHR$(&HCA)+CHR$(&H2)
60 P$=P$+CHR$(&H0)+CHR$(&HB9)+CHR$(&H14)
70 P$=P$+CHR$(&H5)+CHR$(&HBA)+CHR$(&H24)+CHR$(&H12)+CHR$(&HEB)+CHR$(&HF2)
100 ENTRY!=(PEEK(VARPTR(P$)+1))+(PEEK(VARPTR(P$)+2))*256
110 IF ENTRY!>32768! THEN ENTRY%=ENTRY!-65536! ELSE ENTRY%=ENTRY!
120 A$="АБВГДЕЖЗИК"
130 L=0:R=1
140 LOCATE 1,1:PRINT "Пример сдвига окна . . .э
200 LOCATE 15,1:PRINT A$;
210 CALL ENTRY%(L)
220 LOCATE 18,21:PRINT A$;
230 CALL ENTRY%(R)
240 A$=RIGHT$(A$,9)+LEFT$(A$,1)
250 GOTO 200
Фиг. 10.10 Бэйсик-программа для сдвига окон
Поскольку программа на машинном языке задана в строке P$, то
для определения адреса этой строки программа на языке Бейсик
использует функцию VARPTR. Для оператора CALL необходим адрес
подпрограммы, поэтому для его нахождения и используется функция
VARPTR. Воспользовавшись информацией из приложения C справочника
по Бейсику, можно найти адрес строки во втором и третьем байтах
дескриптора строки. Возвращаемое функцей VARPTR значение является
адресом дескриптора строки для P$. Программа извлекает адрес
строки из дескриптора и присваивает его значение переменной ENTRY!.
Поскольку это значение может находиться в диапазоне от 0 до 65536,
подпрограмма должна преобразовать его в целое значение длиной в
одно слово, со значением от от -32768 до 32767. Это слово
помещается в переменную ENTRY%. В остальных строках программы в
сдвигаемые окна записывается символьная строка, а затем для
перемещения текста вызывается подпрограмма SCROLL.
При запуске этой программы вы увидите, что данные в двух окнах
перемещаются независимо. Такой прием позволяет задать два
различных окна на экране и перемещать в них текст независимо друг
от друга. Если написать немного более длинную программу, можно
было бы ограничить каждое окно рамкой, чтобы деиствительно отделить
их друг от друга. Применение подобных методов построения окон
позволяет писать довольно симпатичные программы с одновременным
выводом на экран наскольких фрагментов текста.
Прежде чем покончить с этой программой, давайте просмотрим
через отладчик часть программы, написанную на машинном языке. Для
этого надо иметь готовую к выполнению программу ДОС DEBUG. Это
достигается следующим образом: сначала загружается программа
DEBUG, а затем загружается BASIC.COM (или BASICA.COM, если
используется расширенный Бейсик). После загрузки программы Бейсик
замените первый символ в P$ (и соответственно, первый байт
программы на машинном языке), на CHR$($HCC). Это - код для
прерывания INT 3 прерывания по точке прерывания. Теперь, когда во
время выполнения программы на языке Бейсик она вызывает
подпрограмму на машинном языке, управление получает программа
DEBUG. Теперь можно вновь заменить код 0CCH на исходное значение
(в данном случае 055H). Программу DEBUG можно использовать для
трассировки программы на машинном языке. Конечно, если программа
на языке ассемблера хорошо написана и коротка, то такая отладка не
так необходима. На самом же деле вы, вероятно, заметите, что в
большинстве случаев из-за ошибок при вводе с клавиатуры программы
на машинном языке в строку интерпретатора Бейсик возникает
множество проблем.
Загрузка в верхнюю часть памяти
Загрузка в верхнюю часть памяти
Применение прерывания DOS INT 27H является предпочтительным
способом включения в систему постоянных функций типа драйверов
устройств. Это - удобный способ сделать программу постоянной частью
системы. Пользователь может включить программу в файл AUTOEXEC.BAT,
тогда она будет загружаться автоматически. Такую автоматическую
загрузку можно использовать, когда в вашей системе имеется
специальное устройство ввода-вывода. DOS будет загружать драйвер
этого устройства при каждой загрузке системы. Вы можете даже
предпочесть собственную версию процедуры буферизации печати,
поскольку вы хотите, чтобы она постоянно загружалась в систему.
Однако выход в DOS с фиксацией программы в ОЗУ работает не
всегда. Фирма IBM предлагает три операционные системы для
персональных ЭВМ: DOS, которая и рассматривается в данной книге,
CP/M-86 фирмы Digital Research и UCSD p-System фирмы SofTech
Microsystems. Кроме указанных систем, предлагаемых фирмой IBM,
несколько независимых разработчиков распространяют свои системы.
Чтобы создать драйвер устройства, который работал бы со всеми этими
системами, нужно использовать нечто отличного от метода,
применяемого для DOS.
Допустим, у вас имеется специализированное устройство печати,
которое вы хотите продавать как приспособление к IBM PC. Поскольку
ваш принтер - отноительно дешевое устройство, для него потребуется
больше управления со стороны BIOS, чем для принтера фирмы IBM. Вы
конструируете принтер и устройство подсоединения и пишете BIOS
программу для поддержки его работы. Если вы пользуетесь
прерыванием INT 27H, то ваше устройство можно передавать только
пользователям, имеющим на своей персональной ЭВМ DOS. Необходим
такой способ загрузки драйвера устройства, который бы работал во
всех операционных системах.
Способ загрузки, годный не только для DOS, называется загрузкой
в верхние адреса оперативной памяти. При этом управление системой
перехватывается непосредственно после процедуры самоконтроля при
включении питания. Это может быть реализовано при помощи
специальной дискеты загрузки. Программа будет записана на дискету,
которая вставляется в дисковод перед включением питания.
Подпрограмма загрузки, входящая в BIOS, загружает драйвер
устройства с дискеты в верхнюю часть оперативной памяти. Затем
можно изменить размер области данных сообщаемый BIOS в соответствии
с имеющимся объемом оперативной памяти. При загрузке программы в
верхние адреса размер доступной оперативной памяти уменьшается.
Если после этого загрузить стандартную операционную систему, будет
восстановлено нормальное функционирование ЭВМ. Все операционные
системы фирмы IBM учитывают объем памяти BIOS при определении
границ оперативной памяти. Указанные системы не затрагивают
программ, загруженных в верхние адреса. Если система удовлетворяет
указанным требованиям, то можно пользоваться загрузкой в верхние
адреса оперативной памяти.
Приведем пример для иллюстрации описанного приема. На Фиг. 10.2
представлен листинг ассемблирования двух подпрограмм. Первая
подпрограмма осуществляет инициализацию и загрузку драйвера
устройства. Вторая подпрограмма является собственно драйвером
устройства. Позже станет ясным, почему удобнее было разделить эту
программу на две части.
A
Microsoft (R) Macro Assembler Version 5.00 1/1/80 01:21:50
Фиг. 10.2(а) Загрузчик для создания псевдо-диска Page 1-1
PAGE ,132
TITLE Фиг. 10.2(а) Загрузчик для создания псевдо-диска
0000 NEW_DISK SEGMENT
0000 DISK_BIOS LABEL FAR
0003 ORG 3
0003 OLD_VECTOR LABEL WORD
0003 NEW_DISK ENDS
0000 ABS0 SEGMENT AT 0
004C ORG 13H*4
004C DISK_VECTOR LABEL WORD
0410 ORG 410H
0410 EQUIPMENT LABEL WORD
0413 ORG 413H
0413 MEMORY_SIZE LABEL WORD
= 00A0 DISK_SIZE EQU 160
7C00 ORG 7C00H ; Место,в которое заносится загрузчик ДОС
7C00 BOOT_RECORD LABEL FAR
7C00 ABS0 ENDS
0000 CODE SEGMENT
ASSUME CS:CODE,DS:ABS0
7C00 ORG 7C00H
7C00 8C C8 MOV AX,CS
7C02 8E D8 MOV DS,AX
7C04 8E C0 MOV ES,AX
7C06 8D 36 7C00 R LEA SI,BOOT_RECORD
7C0A 8D 3E 7A00 R LEA DI,BOOT_RECORD-200H ; Место,на которое переносится
7C0E B9 0200 MOV CX,512 ; загрузчик ДОС
7C11 F3/ A4 REP MOVSB ; Перенесение загрузчика
7C13 E9 7A16 R JMP NEXT_LOCATION-200H
7C16 NEXT_LOCATION:
Фиг. 10.2 программа создания псевдо-диска (начало)
7C16 83 06 0410 R 40 ADD EQUIPMENT,40H ; Увеличение числа дисководов
7C1B A1 0413 R MOV AX,MEMORY_SIZE
7C1E 2D 00A0 SUB AX,DISK_SIZE
7C21 A3 0413 R MOV MEMORY_SIZE,AX ; Уменьшение доступной ДОС памяти,необхо-
7C24 B1 06 MOV CL,6 ; димое для размещения псевдо-диска
7C26 D3 E0 SHL AX,CL ; Умножение на 1024/16
7C28 8E C0 MOV ES,AX ; Сегментная часть адреса нового диска
7C2A B8 0201 MOV AX,201H ; Чтение сектора в эту область
7C2D BB 0000 MOV BX,0
7C30 B9 0002 MOV CX,2
7C33 BA 0000 MOV DX,0
7C36 CD 13 INT 13H
7C38 72 1A JC BOOT_ERROR
ASSUME ES:NEW_DISK
7C3A A1 004C R MOV AX,DISK_VECTOR
7C3D 26: A3 0003 R MOV OLD_VECTOR,AX
7C41 A1 004E R MOV AX,DISK_VECTOR+2 ; Сохранение старого вектора пре-
7C44 26: A3 0005 R MOV OLD_VECTOR+2,AX ; рывания 13h
7C48 C7 06 004C R 0000 MOV DISK_VECTOR,0 ; Установка вектора прерывания 17h
7C4E 8C 06 004E R MOV DISK_VECTOR+2,ES ; на новое место
7C52 EB 07 JMP SHORT REBOOT ; Чтение загрузчика с другой дискеты
7C54 BOOT_ERROR:
7C54 8D 36 7A93 R LEA SI,ERROR_MSG-200H ; Печать сообщения об ошибке
7C58 E8 7C81 R CALL PRINT_MSG
7C5B REBOOT:
7C5B 8D 36 7AA5 R LEA SI,BOOT_MSG-200H ; Печать сообщения о загрузке ДОС
7C5F E8 7C81 R CALL PRINT_MSG
7C62 WAIT_BOOT:
7C62 B4 00 MOV AH,0
7C64 CD 16 INT 16H ; Ожидание ввода с клавиатуры
7C66 3C 20 CMP AL,' ' ; Ожидается ввод пробела
7C68 75 F8 JNE WAIT_BOOT
7C6A B8 0201 MOV AX,201H
7C6D BB 7C00 MOV BX,7C00H
7C70 B9 0001 MOV CX,1
7C73 BA 0000 MOV DX,0
7C76 8E C2 MOV ES,DX ; Ввод на стандартное место загрузчика
7C78 CD 13 INT 13H
7C7A 72 D8 JC BOOT_ERROR
7C7C EA 7C00 ---- R JMP BOOT_RECORD
7C81 PRINT_MSG PROC NEAR
7C81 2E: 8A 04 MOV AL,CS:[SI] ; Взять символ для печати
7C84 46 INC SI
7C85 3C 24 CMP AL,'$' ; Проверка на символ конца вывода
7C87 75 01 JNE OUTPUT
7C89 C3 RET
7C8A OUTPUT:
7C8A B4 0E MOV AH,14
7C8C BB 0000 MOV BX,0
7C8F CD 10 INT 10H ; Вывод на дисплей через BIOS
7C91 EB EE JMP PRINT_MSG
7C93 8E E8 A8 A1 AA A0 20 ERROR_MSG DB 'Ошибка загрузки',13,10,'$'
A7 A0 A3 E0 E3 A7 AA
A8 0D 0A 24
Фиг. 10.2 программа создания псевдо-диска (продолжение)
7CA5 82 E1 E2 A0 A2 EC E2 BOOT_MSG DB 'Вставьте новую дискету с ДОС',13,10
A5 20 AD AE A2 E3 EE
20 A4 A8 E1 AA A5 E2
E3 20 E1 20 84 8E 91
0D 0A
7CC3 A8 20 AD A0 A6 AC A8 DB 'и нажмите на пробел',10,13,'$'
E2 A5 20 AD A0 20 AF
E0 AE A1 A5 AB 0A 0D
24
7CD9 PRINT_MSG ENDP
7CD9 CODE ENDS
END
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:06:49
Фиг. 10.2(б) Программа обслуживания псевдо-диска Page 1-1
PAGE ,132
TITLE Фиг. 10.2(б) Программа обслуживания псевдо-диска
0000 CODE SEGMENT
ASSUME CS:CODE
;--------------------------------------------
; Эта программа находится в секторе 1 трека 0
; псевдо-диска. Чтение и запись на устройство 2
; переадресуется на эту программу
;--------------------------------------------
0000 DISK PROC FAR
= 0140 DISK_SIZE EQU 320 ; Размер псевдо-диска в сектрах
0000 EB 05 90 JMP START_BIOS
0003 ???????? ORIGINAL_VECTOR DD ?
0007 START_BIOS:
0007 80 FA 02 CMP DL, 2 ; Программа обрабатывает только обращения
000A 74 05 JE L1 ; к устройству (дисководу) 2
000C OLD_BIOS:
000C 2E: FF 2E 0003 R JMP ORIGINAL_VECTOR ; Переход на стандартную программу
0011 L1:
0011 3C 01 CMP AL, 1
0013 76 F7 JBE OLD_BIOS
0015 80 FC 04 CMP AH, 4
0018 72 06 JB READ_WRITE ; Обрабатываются только команду чтения и
; записи
001A OK_RETURN:
001A B4 00 MOV AH, 0 ; Код возврата - 0
001C F8 CLC ; Сброс C-флага - нет ошибки
001D CA 0002 RET 2
0020 READ_WRITE:
0020 53 PUSH BX ; Сохранение регистров
0021 51 PUSH CX
0022 52 PUSH DX
0023 56 PUSH SI
0024 57 PUSH DI
0025 1E PUSH DS
0026 06 PUSH ES
Фиг. 10.2 программа создания псевдо-диска (продолжение)
;----- Вычисление адреса расположения требуемой записи в псевдо-диске
0027 50 PUSH AX ; Сохранение кода требуемой операции
0028 B0 08 MOV AL, 8 ; Число секторов на треке
002A F6 E5 MUL CH
002C B5 00 MOV CH, 0
002E 03 C1 ADD AX, CX ; Прибавление номера сектора
0030 80 FE 00 CMP DH, 0 ; Проверка на номера стороны
0033 74 03 JE HEAD_0
0035 05 0140 ADD AX, 320 ; Переключение на второю сторону
0038 HEAD_0:
0038 48 DEC AX
0039 3D 0140 CMP AX, DISK_SIZE ; Вычисленное значение правильно?
003C 76 0E JBE DISK_OK
003E RECORD_NOT_FOUND:
003E 58 POP AX ; Восстановление регистров
003F 07 POP ES
0040 1F POP DS
0041 5F POP DI
0042 5E POP SI
0043 5A POP DX
0044 59 POP CX
0045 5B POP BX
0046 B4 04 MOV AH, 4 ; Ошибка: сектор не найден
0048 F9 STC
0049 CA 0002 RET 2 ; Возврат с указанием об ошибке
004C DISK_OK:
004C B1 05 MOV CL, 5
004E D3 E0 SHL AX, CL ; Определение расположения данных на
0050 8C C9 MOV CX, CS ; псевдо-диске
0052 03 C8 ADD CX, AX ; В регистре CX сегментная часть адреса
; данных на диске
0054 51 PUSH CX
0055 8B D3 MOV DX, BX ; В регистре DX адрес передачи
0057 B1 04 MOV CL, 4
0059 D3 EA SHR DX, CL
005B 8C C1 MOV CX, ES
005D 03 D1 ADD DX, CX ; В регистре DX сегментная часть адреса
; передаваемых данных
005F 59 POP CX
0060 83 E3 0F AND BX, 0Fh ; Выделение младших 4 разрядов
0063 58 POP AX ; Восстановление код требуемой операции
0064 80 FC 02 CMP AH, 2
0067 74 11 JE READ_OPN
0069 WRITE_OPN:
0069 8C CE MOV SI, CS
006B 3B CE CMP CX, SI ; Проверка на запись поверх этой программы
006D 74 1B JE ALL_DONE
006F 8E C1 MOV ES, CX
0071 BF 0000 MOV DI, 0
0074 8E DA MOV DS, DX
0076 8B F3 MOV SI, BX ; Установка параметров передачи
0078 EB 09 JMP SHORT DO_MOVE
007A READ_OPN:
007A 8E D9 MOV DS, CX
Фиг. 10.2 программа создания псевдо-диска (продолжение)
007C BE 0000 MOV SI, 0
007F 8E C2 MOV ES, DX
0081 8B FB MOV DI, BX
0083 DO_MOVE:
0083 8A E8 MOV CH, AL ; Число слов в секторе
0085 B1 00 MOV CL, 0
0087 FC CLD
0088 F3/ A5 REP MOVSW ; Пересылка данных
008A ALL_DONE:
008A 07 POP ES ; Восстановление регистров
008B 1F POP DS
008C 5F POP DI
008D 5E POP SI
008E 5A POP DX
008F 59 POP CX
0090 5B POP BX
0091 B4 00 MOV AH, 0 ; Нормальное окончание
0093 F8 CLC
0094 CA 0002 RET 2
0097 DISK ENDP
0097 CODE ENDS
END
Фиг. 10.2 (а) Процедура загрузки для виртуального диска;
(b) Программа драйвера виртуального диска.
Драйвер устройства, приведенный в рассматриваемом примере,
реализует модель диска в оперативной памяти. Мы возьмем 160К
памяти системы и будем исполльзовать ее не как оперативную
память, а как дискету. Мы выбрали именно 160К потому, что это
минимальный объем дискеты фирмы IBM. Очевидно, при большем объеме
оперативной памяти можно моделировать дискету большего объема.
Подпрограмму псевдо-диска можно использовать для повышения
производительности программ, производящих интенсивный обмен с
диском. Например, если поместить на псевдо-диск ассемблер и
исходный код программы, ассемблирование будет произведено не за
минуты, а за секунды. Производительность некоторых программ может
быть повышена более чем на порядок. Платой за такое повышение
производительности являются 160K байт оперативной памяти, отводимые
под псевдо-диск. Если в системе, которая в основном используется
для редактирования и ассемблирования, имеется 256 кбайт памяти, то
в действительности для ассемблера достаточно всего лишь 96 кбайт.
Оставшиеся 160 кбайт можно использовать для моделирования диска в
оперативной памяти. Следует помнить, что содержимое такого диска
теряется при отключении питания, поэтому, прежде чем окончить
работу, убедитесь, что информация скопирована на настоящую
дискету.
Первая подпрограмма на Фиг. 10.2 - процедура загрузки. Ее
код находится в секторе 1 дорожки 0 загрузочной дискеты. Как
поместить программу туда, будет объяснено позже. Подпрограмма POST
при завершении считывает содержимое сетора 1 дорожки 0 в память,
по адресу 0:7C00H. Затем POST передает управление по первому
адресу этой записи. Таким образом система фирмы IBM загружает в
память DOS или любую другую операционную систему. А мы как раз и
собираемся, загружать свою собственную простую операционную
систему.
Сегмент NEW_DISK определяет адрес подпрограммы-драйвера
устройства, также представленной на втором листинге (см. Фиг.
10.2). Поскольку наши подпрограммы ассемблируются отдельно, этот
сегмент для связи процедуры загрузки и драйвера устройства во время
выполнения. Сегмент ABS0 локализует векторы прерываний, заменяемые
в процедуре загрузки. В сегменте CODE, содержатся команды,
загружаемые с дискеты. Сегмент CODE - единственная часть
приведенной программы, находящаяся на загрузочной дискете.
Первое, что делает программа инициализации - пересылает себя по
адресу 0:7A00H. Затем, в процессе инициализации, процедура
перезагружает систему, чтобы загрузить настоящую операционную
систему. Эта загрузка производится по адресу 0:7C00H. Если бы
процедура инициализации не переносила себя на другое место, она бы
считывала следующую запись загрузки в ту область памяти, где
находится сама.
С адреса NEXT_LOCATION процедура инициализации инсталирует
драйвер устройства. Она изменяют флаги оборудования для указания на
наличие дополнительного дисковода по сравнению с установкой внешних
переключателей. Это "убеждает" операционную систему, что диск в
оперативной памяти является частью технического обеспечения. При
инициализации значение MEMORY_SIZE уменьшается на 160 кбайт,
которые резервируются для моделирования диска. Это предотвращает
использование предназначенной для него памяти. Кроме того,
программа подсчитывает значение сегмента для этой области в 160
кбайт, чтобы знать, куда загружать драйвер устройства. Когда это
выполнено, подпрограмма инициализации загружает в зарезервиро-
ванную память содержимое сектора 2 дорожки 0 загрузочной дискеты.
Как поместить драйвер устройства в сектор 2 будет описано при
размещении программы загрузки в секторе 1.
После чтения процедуры драйвера устройства, подпрограмма
инициализации изменяет вектор прерывания BIOS дискеты BIOS (INT
13H), чтобы он указывал на новый драйвер устройства. Как и в
предыдущем примере, эта процедура сохраняет старый вектор. Новому
драйверу этот вектор нужен чтобы при необходимости считывать данные
с настоящей дискеты, а не с ее модели. Наконец, наша программа
загружает систему. Она предлагает пользователю вставить системную
дискету, ждет утвердительного ответа и считывает запись загрузки.
(Если бы процедура предварительно не произвела пересылку программы,
то сейчас она была бы испорчена). Если все идет нормально, то
процедура осуществляет переход по первому адресу записи загрузки, в
результате чего управление получает стандартная операционная
система.
Прежде чем двинуться дальше, рассмотрим, как поместить
процедуру загрузки на новую загрузочную дискету. Во-первых,
необходима пустая отформатированная дискета. Она и станет
загрузочной. Листинг на Фиг. 10.3 показывает, что ассемблирование и
редактирование связей процедуры загрузки происходят, как обычно.
Вызовите программу DOS DEBUG и загрузите процедуру инициации.
Она загружается со смещением 7C00H, установленным программой DEBUG.
Регистры устанавливаются таким образом, чтобы использовать BIOS для
записи одного сектора дискеты. Это выполняет трехбайтовая
программа, находящаяся по адресу 200H. Если после записи нет
состояния ошибки, то запись инициализации уже на дискете.
Для записи драйвера устройства в сектор 2 выполните следующие
шаги, показанные на Фиг. 10.3. С помощью программы DEBUG мы
загружаем в память драйвер псевдодиска. Команда записи программы
DEBUG помещает код драйвера в сектор с относительным номером 1
(сектор 2 дорожки 0) дискеты, находящейся на дисководе A:.
Аналогичный способ можно было бы применить и для занесения на
дискету записи инициализации.
Такой способ формирования вызова BIOS в программе DEBUG
для записи на дискету может использоваться почти для всех функций
BIOS. Проследить, что именно происходит при вызове BIOS, можно с
помощью программы DEBUG. Можно установить регистры для вызова и
написать несложную трехбайтовую программу, осуществляющую
программное прерывание и производящую возврат в DEBUG. Этот прием
удобен также для тестирования собственного драйвера устройства.
Вернемся к процедуре драйвера псевдо-диска во второй части Фиг.
10.2. Заметим, что процедура загрузки сохранила исходный вектор
дискеты (INT 13H) в этом сегменте со смещением 3. Подпрограммы-
драйвера используют этот вектор для реализации всех функций
дискеты, которые не реализуются псевдо-диском. В приведенной
подпрограмме предполагается, что псевдо-диск находится на дисководе
2. На запрос любого другого дисковода процедура передает управление
BIOS, используя приэтом сохраненный в ORIGINAL_VECTOR исходный
вектор. Аналогично и запрос на смену дискеты передается BIOS. Если
функция, запрашиваемая для псевдо-дисковода, не считывание и не
запись, то драйвер псевдо-диска не производит никаких действий, и
происходит возврат с нормальным кодом завершения. Псевдо-диск
не требует форматирования, а поскольку у нас нет контроля ошибок,
то не остается ничего проверять.
Если запрашиваемой операцией является считывание или запись,
драйвер вычисляет адрес соответствующего псевдо-сектора в памяти.
При обращении за границу диска поцедура возвращает запись об ошибке
отсуствия адреса. Код драйвера устанавливает регистры источника и
назначения в соответствии с направлением операции. Наконец,
команда REP MOVSW передает данные между псевдо-диском и буфером
пользователя. Рассматриваемая программа всегда устанавливает
нормальный код завершения и производит возврат в вызывающую
программу.
Данный пример показывает, как реализовать моделирование диска,
однако он не готов для продуктивного использования. Для того, чтобы
стать утилитой общего назначения, эта программа должна быть
преобразована для обеспечения работы с любым прсевдоустройством, а
не только со вторым. Программу можно было бы изменить для работы с
сектором любой длины, хотя обычно этого не требуется. Фактически,
если моделирование диска применяется только при работе с DOS,
процедура инициализации должна форматировать дискету, записав
A
A>MASM BOOT,,,;
The IBM Personal Computer MACRO Assembler
Version 1.00 (C)Copyroght IBM Corp 1981
Warning Severe
Errors Errors
0 0
A>B:LINK BOOT,,,;
IBM Personal Computer Linker
Version 1.00 (C)Copyroght IBM Corp 1981
Warning: No STACK segment
Therhe was 1 error detected
A>MASM DISK,,,;
The IBM Personal Computer MACRO Assembler
Version 1.00 (C)Copyroght IBM Corp 1981
Warning Severe
Errors Errors
0 0
A>B:LINK DISK,,,;
IBM Personal Computer Linker
Version 1.00 (C)Copyroght IBM Corp 1981
Warning: No STACK segment
Therhe was 1 error detected
A>DEBUG BOOT.EXE
-R
AX=0000 BX=0000 CX=7CD3 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=06D7 ES=06D7 SS=06E7 CS=06E7 IP=0000 NV UP DI PL NZ NA PO NC
06E7:0000 0000 ADD [BX+SI],AL DS:0000=CD
-U7C00 7C05
06E7:7C00 8CC8 MOV AX,CS
06E7:7C00 8CD8 MOV DS,AX
06E7:7C00 8CC0 MOV ES,AX
-RAX
AX 0000
:301
-RBX
BX 0000
:7C00
-RCX
CX 7CD3
:1
-RDX
DX 0000
:
-RES
ES 06D7
:6E7
-E200
O6D7:0200 OO.CD 00.13 00.CC ;*** Здесь вставьте загрузочную дискету
-g=100
AX=0000 BX=7C00 CX=0001 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=06D7 ES=06D7 SS=06E7 CS=06E7 IP=0102 NV UP EI PL NZ NA PE NC
06E7:0102 CC INT 3
-NDISK.EXE ;*** Здесь вставьте программную дискету
-L
-UD 10
06E7:0000 EB05 JMPS 0007
06E7:0002 90 NOP
06E7:0003 0000 ADD [BX+SI],AL
06E7:0005 0000 ADD [BX+SI],AL
06E7:0007 80FA02 CMP DL,02
06E7:000A 7405 CMP 0011
06E7:000C 2E SEG CS
06E7:000D FF2E0300 JMP L,[0003] ;*** Здесь вставьте загрузочную дискету
-W0 0 1 1
-Q
A>
A
Фиг. 10.3 Шаги подготовки загрузки в верхние
адреса памяти
справочник и таблицу размещения файлов FAT. При нынешнем виде этой
процедуры после загрузки DOS вы должны "форматировать" диск C:. Для
псевдо-диска не требуеися физического форматирования, но утилита
FORMAT записывает таблицу FAT и каталог, необходимые для
функционирования DOS.
Эта процедура обеспечивает также сохранение процедуры-драйвера
устройства в псевдо-секторе 1 на дорожке 0. Система DOS не
использует указанный сектор дисковода C:, однако другие системы
могут это делать. Вы вооозможно, заметили, что программа псевдо-
диска предотвращает запись в смоделированный сектор дорожки 0, так
что программа по крайней мере не уничтожит саму себя.
Вообще говоря, метод загрузки в верхние адреса оперативной
памяти довольно сложен. Необходима загрузка с двух дискет, что
требует от оператора дополнительных манипуляций. Если не
предполагается использование программы в каких-либо других
системах, кроме DOS, то гораздо удобнее использовать прерывание INT
27H. В противном случае загрузка в верхние адреса оперативной
памяти может оказаться единственно возможным способом.
в детали, далекие от основного
Заключение
Язык ассемблера - мощное средство программирования. Он позволяет
программисту осуществлять всестороннее управление аппаратными
средствами ЭВМ. Однако такое управление заставляет программиста
вникать в детали, далекие от основного содержания программы. Все
преимущества языка ассемблера оборачиваются подчас пустой тратой
времени на многочисленные детали.
В настоящей главе было рассмотрено несколько способов
использования возможностей программирования на языке ассемблера в
сочетании удобcтвами языков высокого уровня. Разумно распределяя
функции по выполнению работы, умелый программист предоставит
отработку бесчисленных деталей программирования языку высокого
уровня, а сам сосредоточится на реализации основной функции
программы. Затем, когда потребуется повышение производительности
программы или более точное управление аппаратными средствами,
программист может переключиться на язык ассемблера. Язык
ассемблера позволяет программисту выполнять действия, которые либо
вообще нельзя реализовать на языке высокого уровня, либо выполнение
которых займет слишком много машинного времени в случае привлечения
дорогих средств языка высокого уровня.
Существует два способа распределения работ между программами на
языке ассемблера и языке высокого уровня. В первом случае можно
ввести новый драйвер устройства, который позволит программисту
использовать стандартные методы доступа к некоторому нестандартному
драйверу устройства. Здесь были приведены примеры, в которых
осуществлялась буферизация печати, а в оперативной памяти
создавалась модель диска. При втором подходе подпрограмма на языке
ассемблера становится частью программы, написанной на языке
высокого уровня, с явным обращением к этой подпрограмме по мере
необходимости. В любом из этих вариантов данная глава соединяла
все принципы языка ассемблера, изложенные в этой книге.