Жанр: Учеба

Справочник программиста на IBM PC

сторінка №38

не имеет ничего общего с засылкой
значения в порт с адресом 2000. Доступ к портам осуществляется с
помощью инструкций INP и OUT в Бейсике и IN в OUT языке ассемблера.
Доступ к адресам памяти осуществляется в Бейсике инструкциями
PEEK и POKE, а в языке ассемблера инструкцией MOV. Имеется 65K
доступных адресов портов и 1024K доступных адресов памяти.
Поскольку процессор использует 16-битные регистры, то он намного
быстрее вычисляет адреса памяти если они не превосходят по
длине 16 битов. Однако максимальное число, которое может содержаться
в 16 битах равно 65535. Мы будем представлять его как
четырехзначное шестнадцатиричное число FFFFH. Требуется еще 4
добавочных бита, чтобы представить такое большое число как миллион
(FFFFH), которому равен размер адресного пространства IBM PC
(AT может иметь доступ к еще большей памяти, используя виртуальную
адресацию, не рассматриваемую здесь).
Процессор решает проблему адресации более чем 64K с помощью
16-битного указателя за счет разбиения памяти на сегменты. Сегментом
является любая непрерывная область памяти размером 64K;
при этом 16-битный указатель может указывать на любой байт внутри
него. Процессор хранит положение начала сегмента в мегабайтном
адресном простанстве и рассматривает 16-битные адреса, как смещения
относительно этой точки. Но как определить эту точку? Ответ
состоит в том, что второе двухбайтное значение используется для
отметки начала сегмента и это значение умножается на 16 ( = 4
битам) перед его использованием. Таким образом, если это сегментное
значение равно 2, то, умножив его на 16, получим 32, и адреса
будут затем вычисляться как смещение относительно 32-го байта в
памяти. Если адрес в сегменте равен 7, то суммируя 32 и 7 получаем,
что нам нужно обратиться к 39-му байту памяти, а не к 7-му.
Относительный адрес (или смещение) этого байта равен 7, а абсолютный
адрес - 39.
В Бейсике Вы можете установить сегментный адрес с помощью
оператора DEF SEG. Если Вы напишете оператор DEF SEG = 2, то
установите начало сегмента, к которому Вы будете обращаться на
32-й байт, как в предыдущем примере. Затем Вы можете использовать
операторы PEEK и POKE для чтения и записи отдельных байтов памяти.
Например, PEEK(7) прочитает седьмой байт с начала сегмента,
т.е. 39-й байт памяти.
Во многих местах этой книги мы ссылаемся на абсолютные адреса
памяти. Это необходимо, поскольку операционная система хранит
важную информацию в определенных местах. Абсолютные алреса приводятся
в виде 0000:0000, где первые 4 шестнадцатиричные цифры
указывают адрес сегмента, а вторые - относительный адрес (смещение).
Вспоминая предыдущий пример, мы можем адресовать 39-й байт
памяти записав 0002:0007. Отметим, что тот же самый адрес может
быть записан в другом виде, если изменить значение сегментного
регистра, например, 0001:0017. Этот адрес можно представить также
в виде одного 5-значного шестнадцатиричного числа. Например,
видеобуфер начинается с адреса B000:0000, который можно записать
как B0000H. Отметим, что суффикс H опускается в специальной адресной
нотации.
И последнее замечание относительно использования памяти. Когда
число занимает два или более байтов, то младший байт этого числа
хранится в ячейке с меньшим адресом. Если целое число A48BH хранится,
начиная с ячейки 1000:0007, то ячейка 0007 содержит 8B, а
0008 - A4. Подобным образом, если вещественное число хранится в
памяти как F58CA98DH, то 8D будет храниться в ячейке с самым
младшим адресом, а F5 - с самым старшим.
Приложение Б. Битовые операции в Бейсике.

В Бейсике нельзя использовать числа в двоичной форме. Он рассматривает
цепочку битов 11000000 как 11 миллионов, а не как 192.
Однако манипуляции с цепочками битов часто необходимы при программировании,
поскольку требуется читать и изменять содержимое
статусных байтов и статусных регистров.
В большинстве случаев к цепочкам битов применяются две логические
операции. Это операции ИЛИ (OR) и И (AND) и обе они доступны
в Бейсике. Используемые по отдельности или в комбинации
они позволяют программе читать и устанавливать индивидуальные
биты байта. Обе эти операции бинарные, т.е. они применяются к
паре значений, давая в качестве результата третье, в точности как
обычные арифметические операции: Z = X OR Y. При использовании со
значениями байтной длины эти операции выполняются 8 раз, по разу
для каждого бита. ИЛИ проверяет бит 0 двух байтов и если этот бит
установлен хотя бы в одном байте, то бит 0 будет установлен и в
результирующем байте. Этот процесс выполняется и для остальных
семи пар битов. Операция И устанавливает бит результата только в
том случае, если оба бита были установлены, в противном случае
этот бит будет равен 0. Изучите эти две операции, используя приведенную
диаграмму:

операнд1 операнд2 результат операнд1 операнд2 результат

бит 7 1 0 1 1 0 0
6 1 0 1 1 0 0
5 1 1 1 1 1 1
4 1 OR 1 = 1 1 AND 1 = 1
3 0 0 0 0 0 0
2 0 0 0 0 0 0
1 0 1 1 0 1 0
0 0 1 1 0 1 0

При программировании ИЛИ используется для установки одного или
более битов в ячейке памяти или статусном регистре. Например,
может возникнуть необходимость установить атрибут мерцания определенному
символу на экране терминала. Эта операция требует установки
седьмого бита. Программа может просто записать весь байт
атрибутов по нужному адресу, но состояние остальных семи битов
может быть неизвестным. Поэтому надо прочитать байт из нужного
места видеобуфера и поместить его в целую переменную, например,
X. Затем готовится байт, у которого установлен только седьмой
бит. Как Вы знаете (или можете узнать из приложения А) такой байт
равен 128. Теперь просто запишите Y = X OR 128 и в Y будет то же
значение, что и в X, но с установленным седьмым битом. Нижеприведенная
диаграмма иллюстрирует эту операцию:
бит атрибут 128 результат

7 0 1 1
6 1 0 1
5 0 0 0
4 1 OR 0 = 1
3 0 0 0
2 0 0 0
1 0 0 0
0 1 0 1

В числе 128 только седьмой бит установлен. Независимо от того,
был ли он установлен или нет у байта атрибутов, он будет установлен
в результирующем байте. Что касается остальных семи битов, то
они будут установлены в результирующем байте только если они уже
были установлены в байте атрибутов. Операция ИЛИ может быть использована
для установки более чем одного бита за один прием (но
смотрите предостерегающие замечания ниже). Чтобы установить биты
2 и 3 надо использовать сумму значений этих двух битов: 4 + 8 =
12.

бит атрибут 12 результат

7 0 0 0
6 1 0 1
5 0 0 0
4 1 OR 0 = 1
3 0 1 1
2 0 1 1
1 0 0 0
0 1 0 1

Для сброса одного или более битов используется операция И. Для
этого надо вычислить значение байта, у которого установлены все
биты, за исключением того, который Вы хотите сбросить. Помните,
что все соответствующие биты должны быть установлены, чтобы результирующий
бит тоже был установлен. Чтобы сбросить бит 7 используйте
значение 255 - 128 = 127:

бит атрибут 127 результат

7 1 0 0
6 1 1 1
5 0 1 0
4 1 AND 1 = 1
3 0 1 0
2 0 1 0
1 0 1 0
0 1 1 1
Отметим, что каждый бит, установленный в байте атрибутов (кроме
бита 7), комбинируется с 1 в байтовом значении 127 и поэтому
равен 1 и в результирующем байте. Биты, которые были равны 0 в
байте атрибутов, остаются равными 0.

Иногда программе нужно установить группу битов (поле). Например,
Вы хотите изменить три младших бита байта видеоатрибутов,
изменяя тем самым цвет символа. Пусть новая цепочка битов будет
101. Ей соответствует значение 5, но выполнение операции ИЛИ с 5
может не привести к желаемому результату, поскольку ИЛИ устанавливает
бит в результирующем байте если хотя бы один из соответствующих
битов был равен 1. Если средний бит был установлен в байте
атрибутов, то он останется установленным и в результирующем байте:


бит атрибут 5 результат

2 0 1 1
1 1 0 1
0 0 1 1

В таком случае программа должна сначала сбросить все три бита с
помощью И, а затем установить нужные биты с помощью ИЛИ. В данном
случае 255 - 4 - 2 - 1 = 248, поэтому сначала надо вычислить Y =
X AND 248, а затем Z = Y OR 5.
Не слишком сложно для программы и определить установлен или
сброшен определенный бит. Для этого надо произвести операцию И
с байтом, у которого сброшены все биты, кроме тестируемого (скажем,
бита 5, который равен 32). Если результат отличен от нуля,
то тестируемый бит был установлен:

бит атрибут 32 результат

7 1 0 0
6 0 0 0
5 1 1 1
4 1 AND 0 = 0
3 0 0 0
2 0 0 0
1 0 0 0
0 1 0 0

Ну а что делать, когда программе требуется знать установку
двух или более битов? Например, биты 6 и 7 могут хранить номер от
0 до 3, но если изолировать эти биты, то они дадут в результате
одно из четырех (десятичных) значений: 0, 64, 128 и 192. Поскольку
Бейсик вынуждает Вас работать не с двоичными числами, то требуется
хитрая обработка, чтобы определить какому значению соответствует
данная цепочка битов. Чтобы позволить Вам избежать этих
махинаций приводятся две процедуры. Первая из них преобразует
десятичное число, хранящееся в байте, в строку из восьми 1 или 0.
Отметим, что это символьная строка, а не двоичное число. Вторая
процедура берет такую строку (любой длины) и преобразует ее в
десятичное число. Используя эти процедуры Вы можете легко анализировать
статусные байты в памяти. С помощью функции MID$ Вы
можете выделить битовое поле и преобразовать содержащееся в нем
значение в десятичное число. Вот процедура преобразования десятичного
числа в двоичную строку:
100 STATUSBYTE = PEEK(13): GOSUB 1000
110 PRINT BITPATTERN$ 'печатаем получившуюся строку
1000 '''преобразуем 10-ное число в строку из 8 единиц и нулей
1010 BITPATTERN$ = "" 'чистим переменную
1020 FOR N = 7 TO 0 STEP -1 'идем назад, начиная с бита 7
1030 IF STATUSBYTE - 2^N " 0 THEN 1060 'переход если бит равен 0
1040 BITPATTERN$ = "1" + BITPATTERN$ 'добавляем к строке 1
1050 STATUSBYTE = STATUSBYTE - 2^N: GOTO 1070
1060 BITPATTERN$ = "0" + BITPATTERN$ 'добавляем к строке 0
1070 NEXT
1080 RETURN

Важно отметить, что порядок битов в двоичной строке обратный.
Вместо того, чтобы двигаться слева направо от бита 7 к биту 0,
бит 0 расположен самым левым в строке. Причиной этого является
тот факт, что функция MID$ может легко выделить требуемые Вам
биты. Поскольку MID$ начинает отсчет с 1, то Вы должны считать,
что биты пронумерованы от 1 до 8. Чтобы выделить четвертый и
пятый биты, напишите BITFIELD$ = MID$(BITSTRING,4,2). Затем определите
десятичное значение (от 0 до 3) хранящееся в поле, используя
процедуру обратного преобразования:

100 BITFIELD$ = MID$(BITPATTERN,4,2): GOSUB 2000
110 PRINT DECVALUE
2000 '''преобразуем строку 1 и 0 в десятичное число
2010 DECVALUE = 0 'чистим переменную
2020 FOR N = 1 TO LEN(BITFIELD$) 'повторяем до конца поля
2030 DECVALUE = DECVALUE + VAL(MID$(BITFIELD$,N,1)*2^(N-1)
2040 NEXT
2050 RETURN
Приложение В. Основные сведения об языке ассемблера.

Читатель этой книги, не знакомый с языком ассемблера, скоро
поймет, что многие программистские трюки не могут быть достигнуты
другими средствами. Хотя изучение языка ассемблера требует отдельной
книги, в этом приложении приводятся основные понятия,
которые помогут новичкам разобраться в примерах на этом языке.
Внимательный просмотр разделов, посвященных среднему и низкому
уровням, даст Вам возможность получить представление о том, как
работает ассемблер, после чего намного легче изучить разные частные
вопросы. Здесь обсуждаются не все ассемблерные инструкции,
встречающиеся в программах, но Вы обнаружите, что около 95 %
инструкций, встреченных Вами в программах, описаны здесь, а значение
остальных может быть понято благодаря комментариям к программам.

Микропроцессор 8088 имеет 13 16-разрядных регистров, каждый из
которых имеет свои функции. В то время как в языках высокого
уровня Вы можете поместить два числа в переменные, а затем сложить
эти переменные, то в языке ассемблера эти числа помещаются в
регистры микропроцессора, а затем складываются значения, содержащиеся
в регистрах. Все операции в языке ассемблера состоят в
обмене данных с регистрами, а затем выполнении операций на регистрах,
таких как изменение отдельных битов, выполнение арифметических
операций и т.д. Одной из причин высокой эффективности
языка ассемблера является хранение данных в регистрах микропроцессора;
компиляторы имеют тенденцию возвращать все значения в
память после выполнения операции, а доступ к памяти требует большого
времени. На рис. В-1 показаны 13 регистров микропроцессоров
8088 и 80286 (последний имеет дополнительные средства для многозадачной
работы, которые мы не будем рассматривать здесь).
Регистры AX, BX, CX и DX являются регистрами общего назначения.
Их особенность состоит в том, что операции могут производиться
не только над содержимым всего регистра, но также и над
половиной. Каждый из четырех регистров делится на старшую и младшую
части, например, AH обозначает старшую половину регистра AX,
а AL - младшую. Точно так же ассемблерная программа может иметь
доступ к BH, BL, CH, CL, DH и DL. Это свойство очень полезно,
поскольку часто программе приходится работать с байтными величинами.
Регистры BP, SI и DI также достаточно удобны, хотя они
могут принимать только 16-битные значения. Каждый бит регистра
флагов сообщает о соответствующем статусе процессора, например, о
том, что при выполнении арифметической операции был перенос за
разрядную сетку.
В общем случае значения помещаются в регистры с помощью инструкции
MOV. MOV AX,BX пересылает содержимое регистра BX в AX,
затирая ранее содержащееся в AX значение. MOV AH,BL приводит к
пересылке байта из регистра в регистр, но MOV AX,BL - недопустимая
инструкция, так как значения должны иметь одинаковый размер.
Инструкция MOV можеть также передавать значения из памяти, например,
MOV AX,ACCT_NUMBER. Здесь ACCT_NUMBER - имя переменной,
которую создал программист, совсем как в языке высокого уровня.
Переменная создается оператором вида ACCT_NUMBER DW 0. Этот оператор
оставляет место для слова (двух байтов), присваивая им
значение 0. Другие допустимые символы в этом операторе это DD -
для двойного слова и DB - для байта или строк. Ассемблер следит
за адресами переменных, поэтому при ассемблировании оператора MOV
AX,ACCT_NUMBER имя переменной заменяется на ее адрес.
Работа с именами переменных - самый простой способ идентификации
данных в программах на языке ассемблера. Но имеются различные
способы хитрой адресации, которые позволяют программе хранить
массивы или использовать указатели. Например, MOV AX,[BX][SI]
посылает в AX значение, которое содержится по смещению, равному
сумме значений регистров BX и SI. Но от чего отсчитывать смещение?
Ответ заключается в том, что все данные собраны в одну часть
программы, а весь исполняемый код - в другую. Часть, отведенная
под данные, называется сегментом данных, а под программу - кодовым
сегментом. Все переменные, отведенные для хранения данных,
адресуются через смещение относительно начала сегмента данных.

Позиция в памяти, с которой начинается сегмент данных, хранится
в регистре DS, одном из четырех сегментных регистров. Как и
все остальные регистры микропроцессора он 16-разрядный, поэтому
он не может содержать числа, большие чем 65535. Каким же образом
сегмент даных может указывать на ячейки памяти, расположенные в
верхней части мегабайтного адресного пространства? Ответ состоит
в том, что сегментные регистры автоматически умножаются на 16, а
результат указывает на место в памяти, с которого начинается
сегмент. Таким образом, сегменты всегда выравнены на 16-байтную
границу. После того как сегмент установлен, все остальные регистры
могут содержать смещения, указывающие на любой из следующих
65535 байтов. Регистр дополнительного сегмента (ES) также используется
для указания на данные, хранящиеся в памяти.
Среди ассемблерных инструкций, которые Вы часто будете встречать
в этой книге, есть инструкции загрузки сегментных и относительных
адресов переменных. MOV AX,SEG ACCT_NUMBER помещает значение
сегментного регистра, в котором расположен ACCT_NUMBER в
AX, а впоследствии это значение будет переслано в DS. MOV BX,OFFSET
ACCT_NUMBER помещает в BX смещение переменной ACCT_NUMBER в
сегменте данных. После выполнения этих операций DS:BX будут указывать
на ACCT_NUMBER. Если ACCT_NUMBER является одномерным массивом,
то для указания на определенный элемент массива может
использоваться добавочное смещение. Вы часто будете встречать
также инструкцию LEA, предоставляющую другой способ загрузки
смещения.
Кодовый сегмент содержит последовательность машинных инструкций,
составляющих программу. Например, инструкция MOV существует
в виде нескольких байтов машинного кода, значение байтов которого
определяет в какой регистр идет пересылка и откуда. Регистр IP
(счетчик команд) содержит величину смещения, которая указывает на
ту инструкцию в кодовом сегменте, которая сейчас должна выполняться.
После выполнения инструкции IP увеличивается таким образом,
чтобы он указывал на следующую инструкцию. В простейшей
программе счетчик команд будет передвигаться от первого байта
кодового сегмента к последнему, где программа и завершится. Но,
как и другие программы, программа на языке ассемблера может быть
разбита на процедуры (подпрограммы), поэтому счетчик команд может
прыгать из одного места кодового сегмента в другое.
Когда счетчик команд прыгает в другое место кодового сегмента,
то его старое значение должно быть запомнено, с тем чтобы можно
было вернуться в нужное место, так как это делает оператор RETURN
в Бейсике, возвращая управление в то место, откуда была вызвана
процедура. В языке ассемблера процедуре присваивается имя, например,
COMBINE_DATA, и оператор CALL COMBINE_DATA передает управление
в процедуру. Процедура завершается инструкцией RET (возврат).
При вызове процедуры процессор запоминает текущее значение
счетчика команд, заталкивая его на стек.
Стек это область, используемая для временного хранения данных.
После завершения процедуры старое значение счетчика команд берется
из стека и выполнение программы продолжается. Стек также содержится
в отдельном сегменте, который, совершенно естественно,
называется сегментом стека. Ему соответствует сегментный регистр
SS. В регистре SP хранится указатель стека, который всегда указывает
на вершину стека и изменяется при засылке на стек и выборке
из стека.
На первый взгляд стек кажется достаточно неуклюжим способом
хранения информации, но у него есть два преимущества. Во-первых,
доступ к его содержимому намного быстрее, чем к переменным, хранящимся
в памяти, а, во-вторых, стек может использоваться для
многих целей. Он может хранить адреса возврата из процедуры,
вложенной в другую процедуру. Впоследствии, то же самое пространство
может использоваться программистом для хранения данных,
которые должны сейчас обрабатываться, но для которых не хватает
места в регистрах микропроцессора. Программа выталкивает содержимое
регистра на стек командой PUSH, а позднее забирает его оттуда
командой POP. В ассемблерных программах, приведенных в этой книге,
Вы не раз встретитесь с инструкциями типа PUSH BX и POP DX.
Неправильный порядок обмена данными со стеком - лучший способ
привести ассемблерную программу к краху.
После того как программист на ассемблере установил три сегментных
регистра (CS, DS и SS) и загрузил данные в регистры микропроцессора
он имеет широкий набор встроенных средств, которыми
процессор может помочь программисту на ассемблере. Вот наиболее
распространенные из них:

ADD AX,BX Прибавляет BX к AX. Существует также инструкция вычи-
тания (SUB), а также варианты обеих этих инструкций.

MUL BL Умножает BL на AX. Имеется также инструкция деления
(DIV), а также варианты обеих этих инструкций.

INC BL Увеличивает BL на 1. Имеется также инструкция умень-
шения (DEC).

LOOP XXX Возвращает программу назад к строке помеченной XXX,
повторяя процесс столько раз, какое число содержится
в CX (аналогично инструкции FOR .. TO .. NEXT в Бей-
сике).

OR AL,BL Выполняет операцию логического ИЛИ над содержимым
регистров AL и BL, причем результат помещается в AL.
Имеются также инструкции AND, XOR и NOT.

SHL AX,1 Сдвигает все биты, содержащиеся в AX, на одну позицию
влево. Это эквивалентно умножению содержимого AX на
2. Другие инструкции сдвигают биты вправо или осу-
ществляют циклический сдвиг. Все эти инструкции очень
полезны для битовых операций, таких как установка
точек экрана.
IN AL,DX Помещает в AX байт, обнаруженный в порте, адрес кото-
рого указан в DX. Имеется также инструкция OUT.

JMP Передает управление в другое место программы, как
инструкция GOTO в Бейсике. JMP YYY передает управле-
ние на строку программы, имеющую метку YYY.

CMP AL,BL Сравнивает содержимое AL и BL. За инструкцией CMP
обычно следует инструкция условного перехода. Напри-
мер, если за инструкцией CMP следует инструкция JGE,
то переход произойдет только если BL больше или равно
AL. Инструкция CMP достигает того же результата, что
и инструкция IF .. THEN в Бейсике (на самом деле
инструкция IF .. THEN переводится интерпретатором
Бейсика в инструкцию CMP).

TEST AL,BL Проверяет есть ли среди битов, установленных в BL,
такие, которые установлены также и в AL. За этой
инструкцией обычно следует команда условного перехо-
да, так же как за CMP. TEST очень полезен при провер-
ке статусных битов (битовые операции очень просто
реализуются в языке ассемблера).

MOVS Пересылает строку, длина которой содержится в CX, с
места, на которое указывает SI, на место, на которое
указывает DI. Имеется еще несколько других инструк-
ций, связанных с пересылкой и поиском строк.
Язык ассемблера обеспечивает несколько вариантов этих инструкций,
а также ряд других специальных инструкций. Имеется также целый
класс инструкций, называемых псевдооператорами, которые помещаются
в текст программы с целью указания ассемблеру как обрабатывать
данную программу. Например, один из типов псевдооператоров автоматически
вставляет часто используемый кусок кода по всей программе.
Такая порция кода называется макросом и именно это свойство
ассемблера дало ему название "макроассемблер".
И, наконец, ассемблер имеет возможность, которой завидуют
(или, по крайней мере, должны завидовать) все кто программирует
только на языках высокого уровня. Имеется ввиду возможность оптимальным
образом использовать прерывания операционной системы.
Ведь это ничто иное, как готовые процедуры. Однако вместо того,
чтобы вызывать их по CALL, они вызываются инструкцией INT. INT21H
вызывает прерывание с шестнадцатиричным номером 21. Имеется ряд
таких прерываний, как в базовой системе ввода/вывода ПЗУ, так и в
операционной системе, причем некоторые из этих процедур необычайно
мощны. На самом деле некоторые из них настолько тесно связаны
с системой, что Вы практически не можете сами написать эквивалентную
процедуру. Языки высокого уровня позволяют использовать
многие из этих прерываний. Они используют их для вывода на экран,
приема ввода с клавиатуры и доступа к дискам. Но многие действительно
полезные прерывания игнорируются языками высокого уровня,
например такие, которые позволяют запустить из одной программы
другую. Некоторые трансляторы (такие как Lattice C или Turbo
Pascal) позволяют доступ к этим прерываниям, если Вы знаете как
их готовить и Вы можете использовать разделы среднего уровня этой
книги для этой цели.

Перед вызовом прерывания некоторая информация должна быть
помещена в регистры процессора. Например, прерывание, верикально
сдвигающее экран, должно знать размеры сдвигаемого окна, число
строк на которое его надо сдвинуть и т.д. Эти значения часто
назы

Список сторінок

Поділитися у соцмережах

☏ Замовлення реклами: +3805044-68-872

© Асоціація електронних бібліотек України

☝ Усі матеріали сайту (зокрема статті, зображення, рекламні оголошення тощо) призначені лише для попереднього ознайомлення. Усі права на публікації, представлені на сайті, належать їхнім законним власникам. Просимо Вас не зберігати копії інформації.