Для осуществления ввода с клавиатуры и вывода на экран символьной информации используются функции DOS. Однако DOS не поддерживает ни позиционирование курсора, ни смену цвета выводимых символов. В текстовом режиме расширить возможности DOS можно с помощью драйвера ANSI.SYS. С графическими изображениями дело обстоит хуже, так как в DOS нет никаких графических функций. Нет их также и в драйвере ANSI.SYS, за исключением возможности перевода видеоадаптера в графический режим (с помощью Esc-последовательности Еsc[=режимh). Для того, чтобы вывести на экран графическое изображение необходимо воспользоваться нижним уровнем операционной системы - базовой системы ввода-вывода (Basic In-Out System, BIOS). Программы BIOS находятся в постоянном запоминающем устройстве (ПЗУ) BIOS. В отличие от DOS, ко всем функциям которой можно обратиться с помощью прерывания 21h, в BIOS за каждым устройством компьютера закреплено свое прерывание. Так, программирование диска осуществляется с помощью прерывания int13h, клавиатуры - int16h, экрана – int10h. Прерывание int10h обеспечивает все функции видеоадаптера: смену видеорежима, вывод символьной и текстовой информации, смену шрифтов, настройку цветовой палитры, работу с графическим изображением и т.д. Воспользуемся прерыванием int10h для перехода в графический режим и вывода простейшего графического изображения.
Пример 3.1. Вывод на экран горизонтальной прямой.
;Установим графический режим EGA mov АН,00h ;(1)Функция задания режима mov AL,10h ;(2)Графический режим EGA int 10h ;(3)Вызов BIOS ;Нарисуем прямую линию в цикле по Х mov SI,150 ;(4)Начальная Х-координата mov CX,300 ;(5)Число точек по горизонтали line: push CX ;(6)Сохраним его в стеке mov AX,0Ch ;(7)Функция вывода пиксела mov AL,4 ;(8)Цвет красный mov ВН,0 ;(9)Видеостраница mov CX,SI ;(10)Х-координата (переменная) mov DX,175 ;(11)Y-координата (константа) int 10h ;(12)Вызов BIOS inc SI ;(13)Инкремент Х-координаты pop CX ;(14)Восстановим счетчик шагов loop line ;(15)Цикл из CX шагов ;Остановим программу для наблюдения результата ее работы mov АН,08h ;(16)Функция ввода с клавиатуры без эха int 21h ;(17)Вызов DOS ;Переключим видеоадаптер назад в текстовый режим mov АН,00h ;(18)Функция задания режима mov AL,03h ;(19)Текстовый режим int 10h ;(20)Вызов BIOS
В предложениях 1-3 с помощью функции 00h прерывания BIOS 10h осуществляется переключение видеоадаптера в графический режим. Поскольку номер режима заносится в байтовый регистр AL, всего может существовать 256 различных текстовых и графических режимов, из которых на сегодняшний день используются (аппаратурой различных фирм) около ста. Режим 10h обеспечивает вывод графического изображения 16 цветами с разрешением 640х350 точек и широко используется с видеоадаптерами EGA и VGA.
Изображение рисуется по точкам (в BIOS не предусмотрено программных средств вывода каких-либо геометрических фигур или хотя бы линий, как нет и средств закрашивания областей экрана). Для вывода на экран цветной точки (пиксела) используется функция 0Ch прерывания 10h. Эта функция требует занесения в регистр AL кода цвета, в ВН - номера видеостраницы, в CX - Х-координаты выводимой точки в диапазоне 0-349, а в DX - Y-координаты точки в диапазоне 0-639. Поскольку регистр CX используется, как счетчик шагов в цикле, для хранения Х - координаты зарезервирован регистр SI.
Прямая горизонтальная линия в примере 3.1 рисуется путем вызова функции 0Сh в цикле, в каждом шаге которого значение Y-координаты остается неизменным (175 в примере), а значение Х-координаты увеличивается на 1 (предложение 13). После завершения цикла формирования изображения в программе предусмотрена остановка (предложения 16-17) для того, чтобы пользователь мог, оставаясь в графическом режиме, проанализировать результаты работы программы. Для остановки программы используется функция DOS 08h ввода одного символа с клавиатуры, функция 08h, как уже отмечалось, не отображает введенный символ на экране и, тем самым, не искажает графическое изображение. Нажатие любой клавиши (кроме управляющих - Ctrl, Alt, Shift и др.) возобновляет выполнение программы.
В конце рассматриваемого фрагмента предусмотрено переключение видеоадаптера в стандартный текстовый режим с номером 03h (предложения 18...20). Если такое переключение не выполнить, видеоадаптер останется в графическом режиме, что может помешать правильному выполнению прикладных программ.
Рассмотрим кратко параметры вызова функции 0Ch прерывания 10h. В регистр ВН заносится номер видеостраницы, на которую выводится данная точка. Графический адаптер EGA обеспечивает хранение и отображение двух графических страниц. По умолчанию видимой (активной) делается страница 0, однако рисовать изображение можно как на видимой, так и на невидимой странице. Для переключения страниц предусмотрена функция 05h прерывания 10h.
В регистр AL заносится код цвета точки. Адаптер поддерживает 64 цвета, хотя в каждый момент времени изображение на экране может содержать только 16 цветов. Этот набор из 16 цветов, выводимых на экран (цветовая палитра), задается программно и может легко изменяться. При загрузке машины устанавливается стандартная палитра, коды цветов которой приведены в табл. 3.1.
Таблица 3.1. Коды цветов стандартной цветовой палитры EGA
Код цвета | Цвет | Вид | ||
0 | Черный | |||
1 | Синий | |||
2 | Зеленый | |||
3 | Бирюзовый | |||
4 | Красный | |||
5 | Фиолетовый | |||
6 | Коричневый | |||
7 | Белый | |||
8 | Серый | |||
9 | Голубой | |||
10 | Салатовый | |||
11 | Светло-бирюзовый | |||
12 | Розовый | |||
13 | Светло-фиолетовый | |||
14 | Желтый | |||
15 | Ярко-белый |
Все современные программы разрабатываются по модульному принципу – программа обычно состоит из одной или нескольких небольших частей, называемых подпрограммами или процедурами, и одной главной программы, которая вызывает эти процедуры на выполнение, передавая им управление процессором. После завершения работы процедуры возвращают управление главной программе и выполнение продолжается с команды, следующей за командой вызова подпрограммы.
Достоинством такого метода является возможность разработки программ значительно большего объема небольшими функционально законченными частями. Кроме того, эти подпрограммы можно использовать в других программах, не прибегая к переписыванию частей программного кода. В довершение ко всему, так как размер сегмента не может превышать 64К, то при разработке программ с объемом кода более 64К, просто не обойтись без модульного принципа.
Язык программирования Ассемблера поддерживает применение процедур двух типов – ближнего (near) и дальнего (far).
Процедуры ближнего типа должны находится в том же сегменте, что и вызывающая программа. Дальний тип процедуры означает, что к ней можно обращаться из любого другого кодового сегмента.
При вызове процедуры в стеке сохраняется адрес возврата в вызывающую программу:
- при вызове ближней процедуры – слово, содержащее смещение точки вызова относительно текущего кодового сегмента;
- при вызове дальней процедуры – слово, содержащее адрес сегмента, в котором расположена точка возврата, и слово, содержащее смещение точки возврата в этом сегменте.
В общем случае группу команд, образующих подпрограмму, можно никак не выделять в тексте программы. Для удобства восприятия в языке Ассемблера процедуры принято оформлять специальным образом. Описание процедуры имеет следующий синтаксис:
<имя_процедуры> PROC <параметр>
<тело_процедуры>
<имя_процедуры> ENDP
Следует обратить внимание, что в директиве PROC после имени не ставится двоеточие, хотя имя и считается меткой.
Параметр, указываемый после ключевого слова PROC, определяет тип процедуры: ближний (NEAR) или дальний (FAR). Если параметр отсутствует, то по умолчанию процедура считается ближней.
В общем случае, размещать подпрограмму в теле программы можно где угодно, но при этом следует помнить, что сама по себе подпрограмма выполняться не должна, а должна выполняться лишь при обращении к ней. Поэтому подпрограммы принято размещать либо в конце сегмента кода, после команд завершения программы, либо в самом начале сегмента кода, перед точкой входа в программу. В больших программах подпрограммы нередко размещают в отдельном кодовом сегменте.
Передавать фактические параметры процедуре можно несколькими способами. Простейший способ – передача параметров через регистры: основная программа записывает параметры в какие-либо регистры, а процедура по мере необходимости извлекает их из этих регистров и использует в своей работе. Такой способ имеет один основной недостаток: передавать параметры через регистры можно если их немного (если много, то просто не хватит регистров). Решить это проблему можно, передавая параметры через стек. В этом случае основная программа записывает параметры в стек и вызывает подпрограмму, подпрограмма работает с параметрами и, возвращая управление, очищает стек.
Для работы с подпрограммами в систему команд процессора включены специальные команды, это вызов подпрограммы CALL и возврат управления RET.
Все команды вызова CALL безусловны. Внутрисегментный вызов NEAR CALL используется для передачи управления процедуре, находящейся в том же сегменте. Он указывает новое значение регистра IP и сохраняет старое значение счетчика команд (IP) в стеке в качестве адреса возврата. Межсегментный вызов FAR CALL используется для передачи управления процедуре, находящейся в другом сегменте или даже программном модуле. Он задает новые значения сегмента CS и смещения IP для дальнейшего выполнения программы и сохраняет в стеке как регистр IP, так и регистр CS.
Все возвраты RET являются косвенными переходами, поскольку извлекают адрес перехода из вершины стека. Внутрисегментный возврат извлекает из стека одно слово и помещает его в регистр IP, а межсегментный возврат извлекает из стека два слова, помещая слова из меньшего адреса в регистр IP, а слово из большего адреса – в регистр CS. Команда RET может иметь операнд, который представляет собой значение, прибавляемое микропроцессором к содержимому указателя стека SP после извлечения адреса возврата (очистка стека).
Модифицируем программу из примера 3.1, разбив ее на процедуры и организовав в цикле обращение к подпрограмме с передачей ей параметров. Поскольку введение процедур несколько изменяет структуру программы, пример 3.2 приведен не фрагментарно, а полностью, включая описание сегментов.
Пример 3.2. Вывод на экран горизонтальной прямой с помощью подпрограммы.
Text segment ’code’ ;(1)Начало сегмента команд assume CS:text,DS:dat ;(2) ;Подпрограмма вывода одной точки. Параметры при вызове находятся в ;ячейках памяти: color - цвет точки, vpage - видеостраница, ;x - X-координата, y - Y-координата draw proc ;(3)Объявление процедуры - подпрограммы mov AH,Och ;(4)Функция вывода пиксела mov AL,color ;(5)Цвет mov BH,vpage ;(6)Видеостраница mov CX,x ;(7)X - координата mov DX,y ;(8)Y - координата int 10h ;(9)Вызов BIOS ret ;(10)Команда выхода из подпрограммы draw endp ;(11)Конец процедуры ;Главная процедура, с которой начинается выполнение программы main proc ;(12)Объявление главной процедуры mov AX,data ;(13)Инициализация сегментного mov DX,AX ;(14)регистра DS ;Установим графический режим EGA mov AH,00h ;(15)Функция задания режима mov AL,10h ;(16)Графический режим EGA int 10h ;(17)Вызов BIOS ;Нарисуем горизонтальную линию в цикле по X mov CX,300 ;(18)Число точек по горизонтали line: push CX ;(19)Сохраним его в стеке call draw ;(20)Вызов подпрограммы inc x ;(21)Инкремент X - координаты pop CX ;(22)Восстановим счетчик шагов loop line ;(23)Цикл из CX шагов ;Остановим программу для наблюдения результата ее работы mov AH,08h ;(24)Функция ввода с клавиатуры int 21h ;(25)Вызов DOS ;Переключим видеоадаптер назад в текстовый режим mov AH,00h ;(26)Функция задания режима mov AL,03h ;(27)Текстовый режим int 10h ;(28)Вызов BIOS mov AX,4C00h ;(29)Завершение программы int 21h ;(30) main endp ;(31)Конец главной процедуры text ends ;(32)Конец сегмента команд data segment ;(33)Начало сегмента данных x dw 150 ;(34)Текущая X - координата y dw 175 ;(35)Текущая Y - координата color db 14 ;(36)Цвет точек vpage db 0 ;(37)Видеостраница data ends ;(38)Конец сегмента данных stack segment stack ;(39)Начало сегмента стека dw 128 dup (0) ;(40)Стек stack ends ;(41)Конец сегмента стека end main ;(42) Конец текста программы
Программа состоит теперь из двух процедур - главной с именем main и процедуры - подпрограммы с именем draw. Каждая процедура начинается оператором proc, перед которым указывается имя процедуры, а заканчивается оператором endp (end procedure, конец процедуры)(пары предложений 3, 11 и 12, 31). Порядок процедур в программе в большинстве случаев не имеет значения, однако имя главной процедуры, с которой начинается выполнение программы, должно быть указано в качестве операнда директивы end, завершающей текст программы (предложение 42).
Подпрограммы вызываются оператором call (вызов); каждая подпрограмма должна заканчиваться командой ret (return, возврат), которая передает управление в точку возврата, т. е. на команду вызывающей программы, следующую за командой call.
Подпрограмма draw выводит на экран одну точку. В качестве входных параметров она должна получить две координаты точки, ее цвет, а также номер видеостраницы, на которую выводится изображение. В языке ассемблера нет установленных правил передачи параметров подпрограмме. Их можно передать через регистры общего назначения, стек или ячейки памяти. В примере 3.2 используется последний способ, не самый быстрый, но наиболее наглядный. Для хранения и модификации параметров в сегменте данных предусмотрены ячейки х, у, color и vpage. В данном примере вывода горизонтальной линии в трех ячейках хранятся константы, и лишь ячейка х модифицируется.
При использовании подпрограммы основной цикл упрощается. Фактически в нем лишь две содержательные строки: вызов подпрограммы draw и инкремент Х-координаты в ячейке х. Однако сохранение в стеке и восстановление регистра СХ является обязательным, потому что он используется в подпрограмме для задания Х-координаты.
В примере 3.3 показано, как можно, в дополнение к горизонтальной, вывести на экран и вертикальную линию. В данном примере реализовано построение прямоугольника путем рисования горизонтальных и вертикальных линий в соответствующих подпрограммах. Здесь не используется сегмент данных, поэтому он не инициализируется в начале программы.
Пример 3.3. Вывод на экран прямоугольника.
text segment 'code' ;(1) начало сегмента команд assume CS:text ;(2) vertical proc ;(3) объявление процедуры построения вертикальной линии v: ;(4) push CX ;(5) сохраним в стек счетчик цикла mov AH,0Ch ;(6) функция вывода пикселя mov AL,6 ;(7) установка цвета mov BH,0 ;(8) видеостраница mov CX,SI ;(9) установка X-координаты int 10h ;(10) вызов BIOS inc DX ;(11) счетчик Y-координаты pop CX ;(12) выгрузим из стека счетчик цикла loop v ;(13) меньшим его на единицу ret ;(14) выход из подпрограммы vertical endp ;(15) конец текста подпрограммы horizontal proc ;(16) объявление процедуры построения горизонтальной линии h: ;(17) push CX ;(18) сохраним в стек счетчик цикла mov AH,0Ch ;(19) функция вывода пикселя mov AL,6 ;(20) установка цвета mov BH,0 ;(21) видеостраница mov CX,SI ;(22) установка X-координаты int 10h ;(23) вызов BIOS inc SI ;(24) счетчик Х-координаты pop CX ;(25) выгрузим из стека счетчик цикла loop h ;(26) уменьшим его на единицу ret ;(27) выход из подпрограммы horizontal endp ;(28) конец текста подпрограммы begin: ;(29) начало основной программы mov AX,00h ;(30) функция задания режима mov AL,10h ;(31) графический режим EGA int 10h ;(32) вызов BIOS mov SI,300 ;(33) Х-координата mov DX,110 ;(34) Y-координата mov CX,50 ;(35) длина стороны call vertical ;(36) вызов подпрограммы mov SI,300 ;(37) Х-координата mov DX,110 ;(38) Y-координата mov CX,100 ;(39) длина стороны call horizontal ;(40) вызов подпрограммы mov AX,4C00h ;(41) завершение программы int 21h ;(42) text ends ;(43) конец сегмента команд end begin ;(44) конец текста программы
В данном примере рисование прямоугольника осуществляется построением четырех линий: двух горизонтальных и двух вертикальных. Для понимания работы программы достаточно проследить за алгоритмом построения по одной линии каждого типа, поскольку построения двух оставшихся отличаются только координатами начала вывода. Но помните, что для того, чтобы обнаружить на экране прямоугольник, необходимо добавить в программу соответствующие строки. Это не составит труда, если вы поймете, как работает данный алгоритм.
Как уже отмечалось, здесь не используется сегмент данных, поэтому он просто отсутствует в программе. Передача данных из основной программы в процедуру (подпрограмму) осуществляется посредством регистров. Обращаем ваше внимание, что в данном случае подпрограммы расположены перед текстом основной программы. Мы опускаем моменты, связанные с графическими особенностями, поскольку рассчитываем на вашу компетентность в данном вопросе, приобретенную при изучении более ранних примеров этого пособия. Важными здесь являются процедуры vertical и horizontal , реализующие соответственно построение вертикальных и горизонтальных линий. Для успешного выполнения в процедуру необходимо отправить значение начальных X и Y координат. Это выполняется в предложениях 33 (37) и 34 (38). В следующем предложении передается значение длины стороны прямоугольника, которое фактически является значением счетчика цикла подпрограмм в начальном состоянии. После этого можно вызывать процедуру, что и отражено в предложении 36 (40). Переходим к анализу работы подпрограмм. Он не потребует большого умственного напряжения. В самом начале необходимо сохранить в стеке текущее значение счетчика цикла (предложение 5 (18)), это будет осуществляться на каждом шаге цикла. В 9 (22) предложении поместим в регистр СХ значение координаты Х. Далее выводим пиксель на экран. Как вы уже, наверняка, заметили, эти две процедуры отличаются лишь в одном: в первой мы инкрементируем счетчик Y-координаты (предложение 11), а во второй – Х-координаты (предложение 24). После этого не забываем выгрузить из стека значение счетчика цикла. Напомним, что при использовании подпрограмм это важно вдвойне, т.к. к моменту выхода из подпрограммы в стеке не должно остаться ничего, что было туда помещено после вызова подпрограммы. Иначе мы не сможем вернуться в текст основной программы.
Рассмотрим механизм выполнения конкретных команд call draw и ret из примера 3.2. На рис. 3.1 приведены фрагменты загрузочного модуля программы 3.2 с указанием расположения некоторых команд, их кодов, смещений, мнемонических обозначений и описания их действия. Показана также часть сегмента данных.
Сегмент команд начинается с процедуры draw. Первая команда этой процедуры mov AH,OCh имеет поэтому смещение (относительный адрес в сегменте команд) ООООП. Процедура draw занимает 14h==20 байт с относительными адресами от OOOOh до 0013h. Последней командой процедуры draw является однобайтовая команда ret с кодом C3h.
Рис. 3.1. Фрагменты загрузочного модуля программы 3.2 с поясняющей информацией.
За процедурой draw располагается главная процедура main. Ее первая команда mov AX,data имеет смещение 0014h. Код команды включает код операции mov (B8h) и значение имени data, равное сегментному адресу сегмента данных. При загрузке программы под управлением отладчика сегментный адрес data оказался равным 4476h.
Команда call draw расположена по адресу 0023h. В ее полный код входит код операции call (E8h) и адрес процедуры draw, на которую надо осуществить переход. Этот адрес записан в виде смещения к началу процедуры draw относительно текущего содержимого IP, т.е. относительно адреса следующей команды (в нашем случае команды inc x). Смещение это знаковое и в данном случае отрицательное, так как процедура draw располагается до процедуры main. Поскольку адрес draw равен 0, а адрес следующей команды равен 26h, в коде команды записано число -26h, которое по правилам записи отрицательных чисел выражается кодом FFDAh (знаковые числа будут рассмотрены позднее).
Главная процедура занимает 18h=24 байт, а первый свободный байт после конца этой процедуры имеет смещение 003Сh. На этом заканчивается сегмент команд. С ближайшего адреса, кратного 16 (44760h в нашем случае), начинается сегмент данных. Относительные адреса в нем опять начинаются с 0, поэтому смещение первой переменной х равно 0, смещение следующей переменной у - 2 и т.д. Весь сегмент данных занимает всего 6 байт.
Вернемся к рассмотрению команд call и ret. При выполнении команды call процессор помещает адрес возврата (содержимое IP, т.е. адрес следующей команды) в стек, а в IP заносит относительный адрес процедуры draw, который находится суммированием текущего содержимого IP и смещения, записанного в коде команды call. В результате указатель стека SP смещается вверх на одно слово, а процессор переходит на выполнение подпрограммы.
Команда ret выполняет обратную операцию - извлекает из верхнего слова стека (с восстановлением исходного состояния указателя стека SP) адрес возврата и загружает его в IP, в результате чего процессор возвращается к выполнению вызывающей процедуры.
Из сказанного ясно, что если в подпрограмме используется стек, с ним надо работать очень аккуратно: все, что заносится в стек в процессе выполнения подпрограммы, должно быть обязательно снято с него до выполнения команды ret, иначе эта команда извлечет из стека и загрузит в IP не адрес возврата, а какое-то данное, что заведомо приведет к нарушению выполнения программы.
Рассмотренный нами вызов подпрограммы носит название прямого ближнего (или внутрисегментного) вызова. Прямым такой вызов называется потому, что адрес перехода хранится непосредственно в коде команды (а это, в свою очередь, получилось потому, что мы указали в качестве операнда команды call имя подпрограммы). Если бы адрес подпрограммы хранился в каком-то другом месте (именно, в регистре или в ячейке памяти), то вызов был бы косвенным. Вторая характеристика вызова говорит о том, что вызываемая подпрограмма находится в том же сегменте, что и вызывающая процедура. В этом случае для перехода на подпрограмму надо знать лишь "половину" полного адреса подпрограммы, именно, относительный адрес точки перехода. Сегментный адрес остается тем же; он не фигурирует в строке вызова подпрограммы и отсутствует в коде команды. В дальнейшем мы рассмотрим и другой вид подпрограмм - дальние подпрограммы, для обращения к которым следует применять межсегментные вызовы.