ASCII и Scan коды
Считывание одной шестнадцатеричной цифры
Считывание двузначного шестнадцатеричного числа
Процедуры
Стек
Использование инструкций PUSH и POP
Организация пауз в программах
Для ввода символов с клавиатуры используется функция 01h прерывания INT 21h: по адресу 100h введите INT 21h; в регистр AH занесите номер функции 01h; выполните прерывание командой "p". В результате появится мерцающий курсор - это приглашение операционной системы (OS) ввести с клавиатуры любой символ. Например, введите букву "h":
h
AX=0168 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0ABD ES=0ABD SS=0ABD CS=0ABD IP=0102 NV UP EI PL NZ NA PO NC
0ABD:0102 96 XCHG SI,AX
-
После выполнения прерывания, в регистре AL появилось число 68h (ASCII код буквы "h").
Задачи
- Используя данный алгоритм, определите ASCII коды символов: "q", "Q", "й", "Й", "*", "#".
- Попробуйте определить ASCII код клавиши [F1].
При определении ASCII кода клавиши [F1], в регистр AL заносится число 00h, а рядом с приглашением Debug появляется символ ";". Почему получается такой результат? Следующий раздел поможет разобраться в этом.
ASCII и Scan коды
Каждая клавиша клавиатуры при нажатии вырабатывает код сканирования (Scan code): [F1] формирует Scan код 3Bh, [Q] формирует Scan код 10h и т.д. Коды сканирования позволяют вводить информацию в компьютер и управлять его работой.
В отличие от Scan кода, ASCII код описывает не клавишу, а символ, который изображен на ней. Например, при нажатии клавиши с буквой "Z" вырабатывается Scan код 2Сh. Далее этот код преобразуется в один из ASCII кодов: "Z"=5Ah, "z"=7Ah, "Я"=9Fh, "я"=EFh. Преобразование кода зависит от состояния клавиш [Shift], [Caps Lock] и от текущей кодовой страницы.
Управляющие клавиши: [F1]...[F12], [Ctrl], [Alt] и др., не предназначены для ввода символов, и вырабатывают только Scan код. Для каждой из этих клавиш посылается два кода, один за другим. Первый код всегда 0h, он означает, что следом идет Scan код специальной клавиши.
Чтобы Scan код клавиши [F1] попал в регистр AL, надо выполнить INT 21h дважды:
0ABD:0100 INT 21
0ABD:0102 INT 21
Проверьте функцию в регистре AH и запустите программу командой "g 104". На приглашение OS надо ответить нажатием клавиши [F1], после чего вы увидите:
-g 104
;
AX=013B BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0ABD ES=0ABD SS=0ABD CS=0ABD IP=0104 NV UP EI PL NZ NA PO NC
0ABD:0104 CC INT 3
Регистр AL содержит Scan код клавиши [F1].
Задачи
- Определите Scan коды функциональных клавиш: [F2] ... [F10].
- Определите Scan коды клавиш управления курсором: [←], [↑], [→].
Позже мы используем Scan коды в программе редактирования секторов диска, для операций: управление псевдокурсором, листинг секторов диска и запуск отдельных фрагментов кода.
Считывание одной шестнадцатеричной цифры
Любой символ, запрашиваемый функцией 01h прерывания INT 21h, отображается в регистре AL в виде ASCII кода. Например, клавиатурный ввод символа "9" завершается записью в AL числа 39h. Для преобразования кода в цифру достаточно выполнить вычитание: 39h-30h=9. Этот алгоритм можно использовать для ввода с клавиатуры лобой десятичной цифры. Ввод шестнадцатеричных цифр A ... F выполняется аналогично, с той лишь разницей, что код символа уменьшается на 37h.
Следующая программа выполняет запрос символа с клавиатуры, и преобразует этот символ в соответствующее шестнадцатеричное число:
0ABD:0100 B401 MOV AH,01
0ABD:0102 CD21 INT 21
0ABD:0104 2C30 SUB AL,30
0ABD:0106 3C09 CMP AL,09 если AL ≤ 9,
0ABD:0108 7E02 JLE 010C то переход на адрес 010Ch
0ABD:010A 2C07 SUB AL,07
0ABD:010C CD20 INT 20
Инструкция JLE (Jump if Less or Equal to) - перейти, если меньше или равно.
Программу следует выполнять в два этапа: "g 10C" и "g". Остановка по адресу 10Сh нужна для просмотра числа, введенного в регистр AL, до того, как прерывание INT 20h вернет все регистры в исходное состояние.
Проведите следующие эксперименты с программой: - проверьте ввод нечисловых символов ("k", "z", "w" ... ); - проверьте ввод строчных числовых символов ("a" ... "f").
Данная программа работает корректно только с символами "0"..."9" и "A"..."F". Позже мы исправим этот недостаток, а пока ввод символов надо контролировать самостоятельно.
Считывание двузначного шестнадцатеричного числа
Алгоритм ввода многозначного числа сводится к последовательному запросу всех его цифр: от старшего разряда к младшему. Далее цифры легко объединяются в исходное число.
Например, надо ввести число B9h. После ввода первой цифры, в регистре AL окажется число Bh. Скопируем это число в регистр DL и сдвинем влево на четыре бита:
Сдвиг выполняется инструкцией SHL (Shift Left - сдвиг влево, работает аналогично SHR). После сдвига, в DL получается число B0h. Далее вводится вторая цифра (в AL попадает 9h). Остается выполнить сложение DL + AL: B0h + 9h = B9h.
Программа ввода двузначного шестнадцатеричного числа:
0ABD:0100 B401 MOV AH,01 устанавливаем функцию запроса символа
0ABD:0102 CD21 INT 21 запрашиваем символ старшей цифры
0ABD:0104 88C2 MOV DL,AL копируем код символа в DL
0ABD:0106 80EA30 SUB DL,30 |
0ABD:0109 80FA09 CMP DL,09 |
0ABD:010C 7E03 JLE 0111 | подготовка
0ABD:010E 80EA07 SUB DL,07 | старшей цифры
0ABD:0111 B104 MOV CL,04 |
0ABD:0113 D2E2 SHL DL,CL |
0ABD:0115 CD21 INT 21 запрашиваем символ младшей цифры
0ABD:0117 2C30 SUB AL,30 |
0ABD:0119 3C09 CMP AL,09 | подготовка
0ABD:011B 7E02 JLE 011F | младшей цифры
0ABD:011D 2C07 SUB AL,07 |
0ABD:011F 00C2 ADD DL,AL объединяем цифры в число
0ABD:0121 CD20 INT 20
Запустите программу командой "g 121". Введите двузначное шестнадцатеричное число. Результат ввода должен отобразиться в регистре DL. Завершите программу командой "g".
Испытайте программу с различными двузначными числами. Для ввода шестнадцатеричных чисел используйте только заглавные буквы: "A"..."F".
Процедуры
Процедура - это список инструкций, который можно многократно вызывать из различных точек программы. Процедуры используют для оформления фрагментов кода, встречающихся в тексте программы несколько раз. Это позволяет значительно сократить длину программы.
Для работы с процедурами используется две инструкции:
CALL XXXX - вызов процедуры, расположенной по адресу XXXX
RET - завершение процедуры и переход на инструкцию, следующую за CALL
В программе вывода на экран двузначного шестнадцатеричного числа, дважды повторялась последовательность из пяти инструкций. Ниже показано, как этот повторяющийся фрагмент оформить в виде процедуры:
0ABD:0100 B402 MOV AH,02 грузим функцию печати символа
0ABD:0102 88DA MOV DL,BL копируем исходное число из BL в DL
0ABD:0104 B104 MOV CL,04 устанавливаем шаг сдвига
0ABD:0106 D2EA SHR DL,CL сдвигм старшей цифры на место младшей
0ABD:0108 E80A00 CALL 0115 переход на процедуру печати HEX-цифры
0ABD:010B 88DA MOV DL,BL восстанавливаем копию числа в DL
0ABD:010D 80E207 AND DL,0F обнуляем старшую цифру
0ABD:0110 E80200 CALL 0115 переход на процедуру печати HEX-цифры
0ABD:0113 CD20 INT 20
0ABD:0115 80C230 ADD DL,30 процедура вывода на экран
0ABD:0118 80FA3A CMP DL,3A шестнадцатеричной цифры
0ABD:011B 7C03 JL 0120 из регистра DL
0ABD:011D 80C207 ADD DL,07
0ABD:0120 CD21 INT 21
0ABD:0122 C3 RET возврат в программу
В данном примере процедура имеет достаточно низкую эффективность: исходная программа уменьшилась всего на три инструкции, а вызов процедуры выполняется только два раза. Следующий пример демонстрирует более эффективное использование процедур.
Программа вывода на экран строки символов от "A" до "J":
0ABD:0100 B241 MOV DL,41 грузим код символа "A" в регистр DL
0ABD:0102 B10A MOV CL,0A устанавливаем счетчик для цикла LOOP
0ABD:0104 E80400 CALL 010B переход на процедуру печати символа
0ABD:0107 E2FB LOOP 0104 если CX > 0, то переход на адрес 104
0ABD:0109 CD20 INT 20
0ABD:010B B402 MOV AH,02 грузим функцию печати символа
0ABD:010D CD21 INT 21 выводим символ с кодом в DL на экран
0ABD:010F FEC2 INC DL увеличиваем число в DL на единицу
0ABD:0111 C3 RET возврат в программу
Каждый вызов инструкции LOOP сопровождается уменьшением регистра CX на единицу, и проверкой: если CX > 0, то выполняется переход на адрес, указанный в инструкции LOOP. Использование регистра CL (вместо CX) сокращает длину кода на 1 байт, но подвергает программу опасности: если до запуска программы в CH будет записано некоторое число, то LOOP выполнит не 10, а значительно большее количество циклов.
Инструкция INC (Increment - приращение) увеличивает содержимое регистра на единицу. Для уменьшения содержимого регистра на единицу используется инструкция DEC (Decrement).
Запустите программу командой "g". Далее выполните трассировку программы. Не забывайте использовать точку останова (или команду "p") для запуска прерываний.
Задачи:
- Используя инструкцию DEC, модифицируйте последнюю программу так,
чтобы она выводила на экран строку символов: "9876543210".
- Используя процедуру печати шестнадцатеричной цифры, напишите программу,
которая будет выводить на экран содержимое регистровой пары SS:SP.
Стек
Инструкция RET передает управление на адрес, следующий за вызовом CALL. Предположим, в программе используется несколько вызовов CALL, передающих управление единственной процедуре. То есть, при каждом вызове, инструкция RET должна возвращать управление на различные адреса. Как RET определяет адрес возврата?
До передачи управления процедуре, CALL копирует адрес возврата в специальную область ОЗУ, называемую - СТЕК. Инструкция RET извлекает адрес возврата из стека, и передает управление инструкции, расположенной по этому адресу.
Стек работает подобно стопке подносов на пружине: когда на верх стопки ставят очередной поднос, то вся стопка немного опускается, а верхний поднос является первым кандидатом на выход. Другими словами, работа стека организована по принципу LIFO ("Last In, First Out" - последним пришел, первым ушел).
При реализации некоторых алгоритмов возникает необходимость написать одну процедуру внутри другой. Такую организацию называют вложенной структурой (вложенным вызовом):
0ABD:0100 CALL 0200
0ABD:0103 INT 20h
0ABD:0200 CALL 0300
0ABD:0203 RET переходы между инструкциями
во вложенной структуре
0ABD:0300 CALL 0400
0ABD:0303 RET
0ABD:0400 RET
вершина стека (SS:SP) ->
В данном примере выполняется три вложенных вызова: CALL 200, CALL 300, CALL 400. В стек заносятся адреса 103, 203, 303:
вершина стека (SS:SP) ->
По адресу 400 находится инструкция RET, которая извлекает адрес c вершины стека, и передает управление инструкции по этому адресу. Картина в стеке меняется:
Очередной RET вновь извлекает значение с вершины стека, и передает управление на адрес 203. Последний RET забирает из стека адрес инструкции INT 20, и передает ей управление.
Вершина стека всегда хранится в регистровой паре SS:SP: SS (Stack Segment - сегмент стека); SP (Stack Point - смещение стека, или указатель стека).
Чтобы лучше разобраться с работой стека, выполните трассировку приведенного примера. Каждый трассировочный шаг сопровождайте анализом регистров SP и IP.
Работа стека может показаться не очень интересной, однако существует много способов его использования. Полезные свойства стека мы начнем обсуждать уже в следующем разделе.
Использование инструкций PUSH и POP
Инструкция CALL помещает адрес возврата в стек, а RET извлекает и передает его в регистр IP (так управление возвращается из процедуры в программу). Аналогичные действия можно выполнить, используя инструкции PUSH и POP:
PUSH - помещает число в стек POP - извлекает число из стека
Инструкции PUSH и POP позволяют использовать стек в качестве временного хранилища данных. Например, в стеке очень удобно сохранять значения регистров.
Предположим, в программе описано несколько процедур. Все регистры общего назначения активно используются в основной части программы - они содержат важные данные. Для работы каждой процедуры также требуются регистры общего назначения. Возникает вопрос: как выполнить процедуры, сохранив данные в основной программе?
На помощь приходят инструкции PUSH и POP. В начале каждой процедуры нужно сохранить текущие значения регистров, которые будут использованы в процедуре. В конце процедуры (перед возвращением в программу) восстановить значения регистров из стека, например так:
0ABD:0200 50 PUSH AX сохранение и восстановление регистров
0ABD:0201 52 PUSH CX происходит в обратном порядке -
0ABD:0202 51 PUSH DX - это соответствует принципу LIFO
0ABD:0203 B402 MOV AH,02
0ABD:0205 B241 MOV DL,41
0ABD:0207 B90900 MOV CX,0009
0ABD:020A CD21 INT 21
0ABD:020C E2FC LOOP 020A
0ABD:020E 59 POP DX
0ABD:020F 5A POP CX
0ABD:0210 58 POP AX
0ABD:0211 C3 RET
Сохранение и восстановление регистров AX, CX и DX позволяет использовать их в качестве локальных переменных внутри процедуры.
Работать со стеком надо очень аккуратно. Кроме значений регистров, в стеке хранится адрес возврата из процедуры. Т.е., если хоть один регистр не будет восстановлен, то адрес возврата вовремя не окажется на вершине стека, инструкция RET передаст в IP неправильный адрес, и нормальный ход программы будет нарушен.
Организация пауз в программах
Один из вариантов использования вложенных структур - это организация фиксированных по времени пауз. Наиболее простая процедура задержки выглядит так:
0ABD:0200 51 PUSH CX
0ABD:0201 B9FFFF MOV CX,FFFF
0ABD:0204 E2FE LOOP 0204
0ABD:0206 59 POP CX
0ABD:0207 C3 RET
Цикл LOOP адресован сам на себя, и выполняет FFFFh - "пустых оборотов". При частоте процессора 1 GHz, такая процедура будет выполнена за несколько десятков микросекунд.
Для более длительной паузы надо поместить данную процедуру внутрь аналогичного цикла:
0ABD:0200 51 PUSH CX
0ABD:0201 B9FF00 MOV CX,0FFF
0ABD:0204 51 PUSH CX
0ABD:0205 B9FFFF MOV CX,FFFF
0ABD:0208 E2FE LOOP 0208 внутренний цикл
0ABD:020A 59 POP CX
0ABD:020B E2F7 LOOP 0204 внешний цикл
0ABD:020D 59 POP CX
0ABD:020E C3 RET
0FFFh - определяет задержку в секундах (внешний цикл); FFFFh - определяет задержку в долях секунды (внутренний цикл).
Выполним приблизительную оценку длительности паузы. Общее количество операций CPU: 0FFF x (FFFF + 3) + 4 = FFF2002. Количество тактов CPU на каждую команду может быть 3, 4 и более. Упростим задачу, взяв 4 такта на операцию: FFF2002 x 4 = 3FFC8008 (тактов CPU). Для CPU с частотой 1 GHz получаем: 3FFC8008h / 1000000000d = 1,073512456 (секунд).
Задача:
Используя процедуру задержки, напишите программу, которая выводит на экран сообщение: "Выполняется форматирование диска C: ... ", а через несколько секунд, в другой строке: "Форматирование завершено!". Для перевода строки используйте символы с кодами Ah, Dh.
|