Цель: написать программу для перевода 16-ричного числа в символьном представлении в двоичное. Вывести результат на экран;
Инструменты: TASM 5, Turbo Debugger (из комплекта TASM), Notepad++ v.5.9
Используемые источники дополнительной информации:
1. Юров В.И. Справочная система по языку ассемблера IBM PC к книге Assembler: учебный курс издательство «Питер» 1998 год (
ссылка для скачивания руководства в chm формате);
2.
Таблица асхи кодов;
3. Зубков С.В. - Assembler для DOS, Window и Unix- 2000;
4.
Обсуждение на тему: вывод числа в двоичном виде на форуме cyberforum.ru;
ОС: Windows XP SP3
Для начала небольшое Intro. В данной статье, как пользоваться той или иной командой я описывать не буду. Этим я займусь в других статьях, по тематике более подробно. И по мере написания новых статей, в этой я буду размещать ссылки, на изучение того, или иного материала. Для отладки программы я использую дебаггер поставляемый вместе с компилятором. Находится он в каталоге \tasm\bin, приложение td.exe. Это дебаггер для 16 разрядных программ. С его помощью можно просматривать построчное выполнение программы. Клавишей F8 выполняется переход по инструкциям. Клавишей F7 можно пермещаться по инструкциям внути процедур и циклов.
А теперь перейдем к делу.
Ввод: с клавиатуры вводится шестнадцатиричное число из двух цифр, используюя функцию DOS - 07h, int 21h. Вывод: результат преобразования выводится на экран, используюя функцию DOS - int 29h (выводит символ из al)
Любой символ, вводимый с клавиатуры записывается в регистр не как сам символ, а в виде ASCII кода в 16-ричном виде. Т.е. если мы введем с клавиатуры к примеру число 5, то в регистр будет занесено значение 035. Таблица ASCII-кодов представлена в заголовке статьи.
Функция DOS 07h — Считать символ из STDIN без эха, с ожиданием и без проверки на Ctrl-Break Ввод: АН = 07h, Вывод: AL = код символа.
Как я уже говорил раньше, все вводимые с экрана символы записываются в регистры в виде ASCII кода.
Функция DOS 02h — Записать символ в STDOUT с проверкой на Ctrl-Break
Ввод: АН = 02h, DL = ASCII-код символа. Вывод: Никакого, согласно документации, но на самом деле: AL = код последнего записанного символа (равен DL, кроме случая, когда DL = 09h (табуляция), тогда в AL возвращается 20h).
Начнем с самого простого. Напишем каркас приложения.
.model small
.stack 100h
.data
.code
start:
end start
Эта программа ничего не делает. Но именно сюда мы будем дописывать нужные нам фрагменты.
.model small директива объявления модели памяти. Код размещается в одном сегменте, а данные и стек в другом.
.stack 100h объявляет сегмент стека размером в 256 байт
.data директива сегмента данных. После нее мы будем объявлять переменные.
.code директива сегмента кода. Следом за этой директивой размещается код программы.
Код программы помещается между строками "start:" и "end start"
В многих программах часто необходимо выполнять перевод каретки на новую строку. По этому я сразу решил написать для этого макрос. Макрос будет выполнять вывод переменной, в которой содержатся коды первода каретки. Переменная определяется следующим образом (помним, что объявление переменных выполняется после директивы .data):
nline db 10, 13, '$'
Один из вариантнов размещения макросов и процедур внутри кода, это вначале программы. Я использую этот способ. Код макроса:
newline macro
lea dx, nline
mov ah, 9
int 21h
endm
Хочу сделать одно замечание по использованию команы lea. Ее эквивалентом является команда mov <операнд>, offset <операнд>. При использовании lea не возникает проблем с выводм строк на экран.
Теперь, вызывая "newline" в программе, курсор будет переводиться на начало новой строки. Сейчас этот макрос может быть бесполезен. Потому как его я использую только несколько раз. Но в будующем, в других программах он может пригодиться.
Такой подход написания макроса для перевода курсона на начало новой строки довольно не плох. Но зависит от переменной, которая находится вне тела макроса. По этому целесообразней использовать такой набор команд, что макрос будет независимый. И решение есть. Следующий макрос немного длинней, но его использование уже гибче.
newline macro
mov ah, 02h
mov dl, 10
int 21h
mov dl, 13
int 21h
endm
Именно его мы и будеи использовать в нашей программе.
Теперь рассмотрим процедуру, выполняющую первод ASCII-кода символа введенного с экрана в цифру, и помещает его в регистр al. В случае, если символ не входит в дипозон тех, которыми можно представить 16-ричное число, процедура выполняет выход из программы.
check proc
cmp al,'f' ; вместо f будет подставлен ASCII код: 066h
ja errcatch ; если код больше 066h то перейти к концу процедуры
cmp al,'a' ; вместо a будет подставлен ASCII код: 061h
jae isschar ; если код больше либо равно 061h то символ
; выполняем переход на метку перевода кода в число
cmp al, 'F'
ja errcatch
cmp al, 'A'
jae isuchar
cmp al, '9'
ja errcatch
cmp al, '0'
jae isnum
isnum: ; вычитание числа из кода символа,
sub al, 30h ; для представления числа в программе, как число
jmp ok
isschar:
sub al, 57h
jmp ok
isuchar:
sub al, 37h
jmp ok
errcatch: ; аварийное прекращение программы
newline ; новая строка
lea dx, crash ; сообщение об ошибке
mov ah, 9
int 21h
jmp exit ; перейти к метке окончания программы
ok: ; процедура выполнена успешно
ret
check endp
Команда cmp предназанчена для сравнения двух оперндов. Следующая за ней команда выполняет переход в зависимости от результата сравнения. В нашем случае ja - Jump if above. Т.е. перейти к метке, если значение больше. Переход выполняется на метку выводящую на экран сообщение об ошибке, и выполняется выход из программы.
Первую часть процедуры можно трактовать так: перейти по метке isschar (т.е. введенное число - маленькие буквы), если символ в диапазоне от a до f. Два последующих сравнения аналогичны первому. Я думаю разъяснянию не подляжат.
Для преобразования кода символа непосредственно в число, нужно от него отнять опрееделенную цифру. Какие именно числа нужно отнимать предствалено ниже:
Для цифр 0-9 отнимать 30h
Для цифр A-F отнимать 37h
Для цифр a-f отнимать 57h
Этими вычитаниями как раз и занимается следующая часть кода процедуры, в зависимости от результата сравнения. Так что после ее успешного выполнения в регистр al помещается 16-риченое число. И теперь с ним можно работать.
Теперь осталось совмес немного - получить от пользователя символы, и перевести их в другу систему счисления.
Для того, чтобы пользователь знал, что мы от него хотим, выведем для него на экран текст, с просьбой ввести символы. В разделе .data объявляем переменную со строкой приглашения: intro
db 'Enter number: $'. Также следует заметить, что символ доллара (он же символ окончания строки) в конце строки важен. Если его не указать, то тогда вместе с нашим сообщением на экран выведется мусор, который находится за пределами переменной. А нам этого не нужно!
Для вывода сообщения необходимо в регистр сегмента данных сегмент данных. Выполняется это следующими двумя командами:
mov ax, @data ; сегментный адрес
mov ds, ax ; помещается в DS
Сразу возникает вопрос, - "А почему бы сразу не выполнить команду mov ds, @data". А вот нет. В сегментный регситр смещение можно поместить только из другого регистра.
Вывод приветствия выполняется следующим образом:
mov dx, offset intro
mov ah, 9
int 21h
Первая строка выполняет помещение смещения перемнной intro в регистр dx. Втроая помещает функцию вывода строки из dx на экрна. 21h выполняет прерывание, в результате которого будет выведена строка на экран. Следует заменить, что функция 07h, к примеру, не выполнится, пока не выполнить прерывание (в нашем случае 21h).
В этой программе введенные символы мы будем хранить в массиве. В разделе данных объявляем слудующим образом:
num
db 2 dup(?)
Так... Приветствие вывели, теперь нужно прочесть введенный символ. А делать мы это будем следующим образом:
mov ah, 7 ; считать символ в al
int 21h ; выполнение прерывания
call check ; проверка правильности ввода
mov num, al ; помещаем введенный символ в num
Т.е. в регистр ah помещаем функцию считывания символа с экрана. Выполнив прерывание введнный символ помещается в регистр al. Затем выполняем процедуру check, которая переводит код симола в число. Ну и на последок, сохраняем переменную в массив.
После ввода двух чисел, и помещения их в массив нужно произвести их конвертирование в двоичное число. Помещая первый введнный символ в bh, и втрой в bl получается неприятная вещ. Оба регистра выглядят как два нуля (00). После помещение числа в первый регистр, получаем, к примеру, 0e, после второго 04 (если это число 4). И общий вид регистра dx получается 0e04. А это не введнное число e4, а число e04. А для того, чтобы перевести число в двоичное, нам понадобится выполнять побитовый сдвиг регистра, и на выходе мы получим совсем, что нам нужно. Возникает вопрос, - "Как поместить число в регистр в необходимом виде?".
И этому есть решение! И даже два.
Первое. Побитовый сдвиг регистра dl влево на 4, затем побитовый сдвиг регистра dx вправо на 4. Псле чего все число помещается в dl в виде e4. Выполняется это слудющим образом:
shl bl, 4 ; смещение содержимого bl на 4 бита влево
; это дает возможность разместить число в регистре рядом
; с первым введнным числом
shr bx, 4 ; смещение всего содержимого регистр bx
Второе. Умножить регистр dl на 16(или на 010 в шеснадцатиричном виде). И прибавить к нему значение регистра dh. И результат будет аналогичен. Делается это так:
mov al, 010h
mul bl
mov bl, al
add bl, bh
Какой выбрать, ришть вам. В этой статье я оставлю первый способ.
Как я говорил выше, что-бы число сделать двичным, его нужно побитово сдвинуть. Так вот, его нужно не просто сдвигать, а сдвигать через флаг переноса. И каждый раз, после сдвига выполняя вывод символа 30h (а это символ 0) + значение флага переноса на выходе получится двоичное число. Посмотрим код:
mov cx, 8 ; количество итераций сдвига
cnv: ; метка выполнение первода числа
shl bl, 1 ; сдвиг регистра bl влево через флаг переноса
mov al,'0' ; помещение ASCII-кода цифры 0 в регистр dl
adc al, 0 ; прибавление к ASCII-коду нуля с учетом флага переноса
int 29h ; выполнение прерывания: вывод символа из al
loop cnv ; продолжение цикла - переход по метке
Регистр cx отвечает за счетчик команд. По этому в него мы помещаем число итераций для следующего цикла - 8. Почему 8? А очень просто. Как выглядит число "e" в двоичной системе? 1110, а 4 - 0100, а 15? 1111. Т.е. для каждого введенного ичсла нам нужно по 4 символа в двоичном, вот и получаем 8 итераций (или 8 сдвигов в цикле).
shl bl, 1 выполняет сдвиг содержимого регистра bl в лево на 1 бит. Левый бит помещается во флаг переноса (cf). В al помещает ASCII-код символа 0, Команда adc прибавляет к коду в al 0 или 1 в зависимости от состояния фалага cf. Ну а int 29h выполняет непосредственно вывод символа из регистра al.
Фух! Сколько мы проделали много работы. Пора бы уже увидеть результат наших трудов. А результат представлен ниже. Это конечный код программы.
.model small ; модель памяти, используемая для ЕХЕ
.stack 100h ; сегмент стека размером в 256 байт
.data
intro db 'Enter number: $' ; сообщение приветствия
num db 2 dup(?) ; массив с двумя числами
crash db 10, 13, 'Input error $' ; сообщение об ошибке
.code
; макрос перехода на новую строку
newline macro
mov ah, 02h ; функция вывода символа из регистра dl на экран
mov dl, 10 ; перевод каретки на новую строку
int 21h
mov dl, 13 ; перенос курсора на начало строки
int 21h
endm
; процедура проверяет, является ли символ в регистре al принадлжеащим
; диапазону букв и цифр, пренадлежащих 16-ричному представлению чисел
; и переводит ASCII код в цифру
check proc
cmp al,'f' ; вместо f будет подставлен ASCII код: 066h
ja errcatch ; если код больше 066h то перейти к концу процедуры
cmp al,'a' ; вместо a будет подставлен ASCII код: 061h
jae isschar ; если код больше либо равно 061h то символ
; выполняем переход к метке перевода кода в число
cmp al, 'F'
ja errcatch
cmp al, 'A'
jae isuchar
cmp al, '9'
ja errcatch
cmp al, '0'
jae isnum
isnum: ; вычитание числа из кода символа,
sub al, 30h ; для представления числа в программе, как число
jmp ok
isschar:
sub al, 57h
jmp ok
isuchar:
sub al, 37h
jmp ok
errcatch: ; аварийное прекращение программы
newline ; новая строка
lea dx, crash ; сообщение об ошибке
mov ah, 9
int 21h
jmp exit ; перейти к метке окончания программы
ok: ; процедура выполнена успешно
ret
check endp
start:
mov ax, @data ; сегментный адрес
mov ds, ax ; помещается в DS
mov dx, offset intro ; попещение приветствия в область данных
mov ah, 9
int 21h ; функция DOS "вывод строки"
mov ah, 7 ; считать символ в al
int 21h ; выполнение прерывания
call check ; проверка правильности ввода
mov num, al ; помещаем введенный символ в num
mov ah, 7 ; считать символ в al
int 21h ; выполнение прерывания
call check ; проверка правильности ввода
mov num+2, al ; помещаем введенный символ в
; num+2 (второй элемент массива)
newline ; перходим на новую строку
mov bh, num ; Помещение первого числа в bh
mov bl, num+2 ; помещение второго числа в bl
shl bl, 4 ; смещение содержимого bl на 4 бита влево
; это дает возможность разместить
; число в регистре рядом
; с первым введнным числом
shr bx, 4 ; смещение всего содержимого регистр bx
; после чего, число будет полность в регистре bl
mov cx, 8 ; количество итераций сдвига
cnv: ; метка выполнение первода числа
shl bl, 1 ; сдвиг содержимого регистра bl влево через флаг переноса
mov al,'0' ; помещение ASCII-кода цифры 0 в регистр dl
adc al, 0 ; прибавление к ASCII-коду нуля с учетом флага переноса
int 29h ; выполнение прерывания: вывод символа из al
loop cnv ; продолжение цикла - переход по метке
newline ; перводим курсор на новую строку
mov ah, 1 ; Ожидание нажатия клавиши пользователем
int 21h
exit:
mov ax, 4C00h ; аналог: mov ah, 4ch
int 21h ; функция DOS "завершить программу"
end start
Outro. Буду благодарен за ваши коментарии и найденные ошибки! Надеюсь эта статья поможет понять вам необходимые основы программирования на языке ассемблера. Успехов в обучении вам!!!