Display as Block
Рис. 2.6. Дополнительное меню окна дампа памяти.
Чаще всего приходится пользоваться первым пунктом этого меню Goto, с помощью которого можно задать любой адрес (входящий или не входя-Щий в сегменты программы), и получить дамп этого участка.. На рис. 2.7.
Глава 2
Основы программирования
изображено содержимое окна дампа после ввода начального адреса в виде DS:0 (тот же результат даст начальный адрес DSrmsg, а так же и просто msg, так как по умолчанию сегментный адрес берется из DS). Как и следовало ожидать, по этому адресу расположено наше единственное данное — строка текста, выводимая программой на экран. Кстати, в окне дампа видно начало промежутка между сегментами (данных и стека), заполненного нулями.
ds:0000 8F ЕО АЕ ИЗ £0 АО АС АС Программ ds:0008 АО 20 £0 АО Al AE EZ АО а работа ds:0010 А5 EZ 21 24 00 00 00 00 ет?$ ds:0018 00 00 00 00 00 00 00 00
Рис. 2.7. Дамп сегмента данных.
2.2. Представление данных
В языке ассемблера имеются средства записи целых и вещественных чисел, а также символьных строк и отдельных символов. Целые числа могут быть со знаком и без знака, а также записанными в двоично-десятичном формате. Для целых чисел и символов в составе команд микропроцессора и, соответственно, в языке ассемблера, есть средства обработки — анализа, сравнения, поиска и проч. Для вещественных чисел таких средств в самом микропроцессоре нет, они содержатся в арифметическом сопроцессоре. Поскольку программирование сопроцессора в настоящей книге не рассматривается, то и вещественными числами мы заниматься не будем.
Рассмотрим сначала целые числа без знака и со знаком. Числа без знака получили свое название потому, что среди этих чисел нет отрицательных. Это самый простой вид чисел: они представляют собой весь диапазон двоичных чисел, которые можно записать в байте, слове или двойном слове. Для байта числа без знака могут принимать значения от OOh (0) до FFli (255); для слова — от OOOOh (0) до FFFFli (65535); для двойного слова - от OOOOOOOOh (0) до FFFFFFFFh (4294967295).
В огромном количестве приложений вычислительной техники для чисел нет понятия знака. Это справедливо, например, для адресов ячеек памяти, кодов ASCII символов, результатов измерений многих физических величин, кодов управления устройствами, подключаемыми к компьютеру. Для таких чисел естественно использовать весь диапазон чисел, записываемых в ячейку того или иного размера. Если, однако, мы хотим работать как с положительными, так и с отрицательными числами, нам придется половину чисел из их полного диапазона считать положительными, а другую половину — отрицательными. В результате диапазон изменения числа уменьшается в два раза. Кроме того, необходимо предусмотреть систему кодирования, чтобы положительные и отрицательные числа не перекрывались.
В вычислительной технике принято записывать отрицательные числа в так называемом дополнительном коде, который образуется из прямого путем замены всех двоичных нулей единицами и наоборот (обратный код) и прибавления к полученному числу единицы. Это справедливо как для байтовых (8-битовых) чисел, так и для чисел размером в слово или в двойное слово (рис. 2.8)
Для байта 0000 0101 1111 1010 +1 Дополнительный код числа 5: 1111 1011 |
Прямой код числа 5: Обратный код числа 5: |
Для слова
0000 0000 0000 0101 1111 1111 1111 1010
_______________ +1
1111 1111 1111 1011
Для двойного слова
Прямой код числа 5: Обратный код числа 5: |
0000 0000 0000 0000 0000 0000 0000 0101 1111 1111 1111 1111 1111 1111 1111 1010
+1
Дополнительный код числа 5: ЦЦ Щ1 ЦЦ цц Ш1 пи цц 1011 Рис. 2.8. Образование отрицательных чисел различного размера.
Такой способ образования отрицательных чисел удобен тем, что позволяет выполнять над ними арифметические операции по общим правилам с получением правильного результата Так, сложение чисел +5 и -5 дает 0; в результате вычитания 3 из 5 получается 2; вычитание -3 из -5 дает -2 и т.д.
Анализируя алгоритм образования отрицательного числа, можно заметить, что для всех отрицательных чисел характерно наличие* двоичной единицы в старшем бите. Положительные числа, наоборот, имеют в старшем бите 0. Это справедливо для чисел любого размера. Кроме того, из рис. 2.8 видно, что для преобразования отрицательного 8-битового числа в слово достаточно дополнить его слева восемью двоичными единицами. Легко сообразить, что для преобразования положительного 8-битового числа в слово его надо дополнить восемью двоичными нулями. То же справедливо и для преобразования слова со знаком в двойное слово со знаком, только добавить придется уже не 8, а 16 единиц или нулей. В системе команд МП 86 и, соответственно, в языке ассемблера, для этих операций предусмотрены специальные команды cow и cwd.
Следует подчеркнуть, что знак числа условен. Одно и то же число, например, изображенное на рис. 2.8 8-битовое число FBh можно в одном контексте рассматривать, как отрицательное (-5), а в другом — как положительное, или, правильнее, число без знака (FBh=251). Знак числа является характеристикой не самого числа, а нашего представления о его смысле.
На рис. 2.9 представлена выборочная таблица 16-битовых чисел с указанием их машинного представления, а также значений без знака и со знаком. Из таблицы видно, что для чисел со знаком размером в слово Диапазон положительных значений простирается от 0 до 32767, а диапазон отрицательных значений — от -1 до -32768.
Глава 2
16-ричное Десятичное представление: представление Без знака Со знаком | |||
OOOOh | 00000 Ноль | ||
OOOlh | +00001 | Минимальное положительное число | |
0002h | +00002 | ||
0003h | +00003 | ||
0004h | +00004 | ||
7FFCh | +32764 | ?• Диапазон положительных чисел | |
7FFDh | +32765 | ||
7FFEh 7FFFh 8000h | 32766 32767 32768 | +32766 +32767 -32768 | Максимальное положительное число Максимальное отрицательное число |
SOOlh | -32767 | ||
8002h | -32766 | ||
8003h | -32-765 | ||
FFFBh | -00005 | * Диапазон отрицательных чисел | |
FFFCh - | -00004 | ||
FFFDh | -00003 | ||
FFFEh FFFFh | 65534 65535 | -00002 -00001 - | Минимальное отрицательное число |
Рис. 2.9. Представление 16-битовых чисел без знака и со знаком.
Ноль Минимальное положительное число |
Диапазон положительных чисел |
Максимальное положительное число?! Максимальное отрицательное число -| |
Диапазон отрицательных чисел |
Минимальное отрицательное число |
На рис. 2.10 представлена аналогичная таблица для 8-битовых чисел. Из таблицы видно, что для чисел со' знаком размером в байт диапазон положительных значений простирается от 0 до 127, а диапазон отрицательных значений — от -1 до -128.
16-ричное Десятичное представление: представление Без знака Со знаком | ||
OOh | ||
Olh | + 001 | |
02h | + 002 | |
03h | + 003 | |
04h | + 004 | |
7Ch | + 124 | |
7Dh | - +125 | |
7 Eh | + 126 | |
7Fh | + 127 | |
80h | -128 | |
81h | -127 | |
82h | -126 | |
83h | -125 | |
FBh | -005 | |
FCh | -004 | |
FDh | -003 | |
FEh | -002 | |
FFh | -001 |
Рис. 2.10. Представление 8-битовых чисел без знака и со знаком.
Основы программирования
Среди команд процессора, выполняющих ту или иную обработку чисел, можно выделить команды, безразличные к знаку числа (например, inc, add, test), команды, предназначенные для обработки чисел без знака (mul, div, ja, jb и др.), а также команды, специально рассчитанные на обработку чисел со знаком (imul, idiv, jg, jl и т.д.). Особенности использования этих команд будут описаны в следующей главе.
Рассмотрим теперь другой вид представления чисел — двоично-десятичный формат (binary-coded decimal, BCD), используемый в ряде прикладных областей. В таком формате выдают данные некоторые измерительные приборы; он же используется КМОП-часами реального времени компьютеров IBM PC для хранения информации о текущем времени. В МП 86 предусмотрен ряд команд для обработки таких чисел.
Двоично-десятичный формат существует в двух разновидностях:, упакованный и распакованный. В первом случае в байте записывается двухразрядное десятичное число от 00 до 99. Каждая цифра числа занимает половину байта и хранится в двоичной форме. Из рис. 2.11 можно заметить, что для записи в байт десятичного числа в двоично-десятичном формате достаточно сопроводить записываемое десятичное число символом h.
10000110 Двоичное содержимое банта
8 6 Десятичное обозначение числа
8 6h Шестнадцатеричное обозначение числа
Рис. 2.11. Упакованный двоично-десятичный формат.
В машинном слове или в 16-разрядном регистре можно хранить в двоично-десятичном формате четырехразрядные десятичные числа от 0000 до 9999 (рис.2.12).
1001 0110 0000 0100 Двоичное содержимое слова
9604 Десятичное обозначение числа
9 6 0 4h Шестнадцатеричное обозначение числа
Рис. 2.12. Запись десятичного числа 9604 в слове.
Распакованный формат отличается от упакованного тем, что в каждом байте записывается лишь одна десятичная цифра (по-прежнему в двоичной форме). В этом случае в слове можно записать десятичные числа от 00 до 99 (см. рис. 2.13)
00001001 00001000 Двоичное содержимое слова
9 8 Десятичное обозначение числа
09 0 8h Шестнадцатеричное обозначение числа Рис. 2.13. Запись десятичного числа 98 в распакованном виде.
Глава 2
При хранении десятичных чисел в аппаратуре обычно используется более экономный упакованный формат; умножение и деление выполняются только с распакованными числами, операции же сложения и вычитания применимы и к тем, и к другим. Примеры операций с двоично-десятичными числами будут рассмотрены в следующей главе.
2.3. Описание данных
Практически любая программа содержит в себе перечень данных, с которыми она работает. Это могут быть символьные строки, предназначенные для вывода на экран; числа, определяющие ход выполнения программы или участвующие в вычислениях; адреса подпрограмм, обработчиков прерываний или просто тех или иных полей программы; специальные коды, например, коды цвета выводимых на экран символов и т.д. Кроме данных, определяемых в тексте программы, в программу часто входят зарезервированные поля, предназначенные для заполнения по ходу выполнения программы, например, результатами вычислений или путем чтения из файла. Все эти данные и зарезервированные поля должны быть определены в составе сегмента данных программы (в принципе они могут быть определены, и часто определяются, не в сегменте данных, а в сегменте команд, но здесь мы не будем касаться этого вопроса).
Для определения данных используются, главным образом, три директивы ассемблера: db (define byte, определить байт) для записи байтов, dw (define word, определить слово) для записи слов и dd (define double, определить двойное слово) для записи двойных слов:
db dw dd |
?••£-'I'^f-j |
Кроме перечисленных, имеются и другие директивы, например df (define farword, определить поле из 6 байт), dq (define quadword, определить четверное слово) или dt (define teraword, определить 10-байтовую переменную), но они используются значительно реже.
Для того чтобы к данным можно было обращаться, они должны иметь имена. Имена данных могут включать латинские буквы, цифры (не в качестве первого знака имени) и некоторые специальные знаки, например, знаки подчеркивания (_), доллара (S) и коммерческого at (@). Длину имени некоторые ассемблеры ограничивают (например, ассемблер MASM — 31 символом), другие — нет, но в любом случае слишком длинные имена затрудняют чтение программы. С другой стороны, имена данных следует выбирать таким образом, чтобы они отражали назначение конкретного данного, например counter для счетчика или filename для имени файла;
counter dw filename db |
10000 "a:\myfile.001"
Значения числовых данных можно записывать в различных системах счисления; чаще других используются десятичная и 16-ричная запись:
•новы программирования |
size dw 256 setb? db 80h |
;B ячейку size записывается;десятичное число 256;B ячейку setb? записывается;16-ричное число 80h
Необходимо отметить неточность приведенных выше комментариев. В памяти компьютера могут храниться только двоичные коды. Если мы говорим, что в какой-то ячейке записано десятичное число 128, мы имеем в виду не физическое содержимое ячейки, а лишь форму представления этого числа в исходном тексте программы. В слове с именем size фактически будет записан двоичный код 0000000100000000, являющийся двоичным эквивалентом десятичного числа 128. Во втором случае в байте с именем setbit? будет записан двоичный эквивалент шестнадцатеричного числа 80U, который составляет 10000000 (т.е. байт с установленным битом 7, откуда и получила имя эта ячейка).
Для резервирования места под массивы используется оператор dup (duplicate, дублировать), который позволяет «размножить» байт, слово или двойное слово заданное число раз:
rawdata dw 300 dup (1); Резервируются 300 слов,
;заполненных числом 1 string db 80 dup (IAI); Резервируются 80 байтов,
;заполненных знаком >А1
Присвоение данным символических имен позволяет обращаться к ним в программных предложениях, не заботясь о фактических адресах этих данных. Например, команда
mov AX.size
занесет в регистр АХ содержимое ячейки size (число 256), независимо от того, в каком месте сегмента данных эта ячейка определена, и в какое место физической памяти она попала. Однако программист, использующий язык ассемблера, должен иметь отчетливое представление о том, каким образом назначаются адреса ячейкам программы, и уметь работать не только с символическими обозначениями, но и со значениями адресов. Для обсуждения этого вопроса рассмотрим пример сегмента данных, в котором определяются данные различных типов. В левой колонке укажем смещения данных (в шестнадцатеричной форме), вычисляемые относительно начала сегмента.
data segment
OOOOh | counter |
0002U | pages |
OOOCh | numbers |
00 llh | page_addr |
data ends |
dw 10000 db "Страница Г db 0,1,2,3,4 dw pages
Глава 2
Основы программирования
57-
Сегмент данных начинается с данного по имени counter, которое описано, как слово.(2 байт) и содержит число 10000. Очевидно, что его смещение равно 0. Поскольку это данное занимает 2 байт, следующее за ним данное pages получило смещение "2. Данное pages описывает строку текста длиной 10 символов и занимает в памяти столько же байтов, поэтому следующее данное numbers получило относительный адрес 2 + 10 = 12 = Ch. В поле numbers записаны 5 байтовых чисел, поэтому последнее данное сегмента с именем page_addr размещается по адресу СИ + 5 = 1 111.
Ассемблер, начиная трансляцию сегмента (в данном случае сегмента
данных) начинает отсчет его относительных адресов. Этот отсчет ведется
в специальной переменной транслятора (не программы!), которая назы
вается счетчиком текущего адреса и имеет символическое обозначение
знака доллара (S). По мере обработки полей данных, их символические
имена сохраняются в создаваемой ассемблером таблице имен вместе с
соответствующими им значениями счетчика текущего адреса. Другими
словами, введенные нами символические имена получают значения, рав
ные их смещениям. Таким образом, с точки зрения транслятора counter
равно 0, pages — 2, numbers — Ch и т.д. Поэтому предложение ft-
(•
page_addr dw pages
м
трактуется ассемблером, как, t
page_addr dw 2
и приводит к записи в слово с относительным адресом ПИ числа 2 (сме*
щения строки pages). _ *
Приведенные рассуждения приходится использовать при обращении к «внутренностям» объявленных данных. Пусть, например, мы хотим выводить на экран строки «Страница 2», «Страница 3»,»Страница 4" и т.д. Можно, конечно, все эти строки описать в сегменте данных по отдельности, но это приведет к напрасному расходу памяти. Экономнее поступить по-другому: выводить на экран одну и ту же строку pages, но модифицировать в ней номер страницы. Модификацию номера можно выполнить с помощью, например, такой команды:
mov pages+9, '2'
Здесь мы «вручную» определили смещение интересующего нас символа в строке, зная, что все данные размещаются ассемблером друг за другом в порядке их объявления в программе. При этом, какое бы значение не получило имя pages, выражение pages + 9 всегда будет соответствовать байту с номером страницы.
Таким же приемом можно воспользоваться при обращении к данному numbers, которое в сущности предсташыет собой небольшой массив из 5 чисел. Адрес первого числа в этом массиве равен просто numbers, адрес второго числа — numbers + 1, адрес третьего — numbers + 2 и т.д. Следующая команда прочитает последний элемент этого массива в регистр DL:
mov DL,numbers+4
Какой смысл имело объединение ряда чисел в массив numbers' Да никакого, если к этим числам мы все равно обращаемся по отдельности. Удобнее было объявить этот массив таким образом:
nmbO db
nmbl db
nmb2 db
шпЬЗ db
nmb4 db
В этом случае для обращения к последнему элементу не надо вычислять его адрес, а можно воспользоваться именем шпЬ4. Если, с другой стороны, мы хотим работать с числами, как с массивом, используя индексы отдельных элементов (о чем речь будет идти позже), то присвоение массиву общего имени представляется естественным. Получение последнего элемента массива по его индексу выполняется с помощью такой последовательности команд:
mov mov |
SI,4;Индекс элемента в массиве
ОЬ,питЬег8[51];Обращение по адресу
;numbers + содержимое SI
Иногда желательно обращаться к элементам массива (обычно небольшого размера) то с помощью индексов, то по их именам. Для этого надо к описанию массива, как последовательности отдельных данных, добавить дополнительное символическое описание адреса начала массива с помощью директивы ассемблера label (метка):
db db db |
numbers label byte nmbO db 0 nmbl db nmb 2 nmb3
nmb4
Метка numbers должна быть объяшгена в данном случае с описателем byte, так как данные, следующие за этой меткой, описаны, как байты и мы планируем работать с ними именно как с байтами. Если нам нужно иметь массив слов, то отдельные элементы массива следует объявить с помощью директивы dw, а метке numbers придать описатель word:
numbers label word iimbO dw 0 nmbl dw 1
Глава 2
Основы программирования
nmb 2 dw птЪЗ dw nmb4 dw
В чем состоит различие двух последних описаний данных' Различие есть, и весьма существенное. Хотя в обоих случаях в память записывается натуральный ряд чисел от 0 до 4, однако в первом варианте под каждое число в памяти отводится один байт, а во втором — слово. Если мы в дальнейшем будем изменять значения элементов нашего массива, то в первом варианте каждому числу можно будет задавать значения от 0 до 255, а во втором — от 0 до 65535.
Выбирая для данных способ их описания, необходимо иметь в виду, что ассемблер выполняет проверку размеров используемых данных и не пропускает команды, в которых делается попытка обратиться к байтам, как к словам, или к словам — как к байтам. Рассмотрим последний вариант описания массива numbers. Хотя под каждый элемент выделено целое слово, однако реальные числа невелики и вполне поместятся в байт. Может возникнуть искушение поработать с ними, как с байтами, перенеся предварительно в байтовые регистры:
mov AL,nmbO mov DL,nmbl mov CL,nmb2 |
;Переносим nmbO в AL;Переносим nmbl в AL;Переносим nmb2 в AL
Так делать нельзя. Транслятор сообщит о грубой ошибке — несоответствии типов, и не будет создавать объектный файл. Однако довольно часто возникает реальная потребность в операциях такого рода. Для таких случаев предусмотрен специальный атрибутивный оператор byte ptr (byte pointer, байтовый указатель), с помощью которого можно на время выполнения одной команды изменить размер операнда:
П
mov AL,byte ptr nmbO \
mov DL,byte ptr nmbl,ч
mov CL,byte ptr nmb2
Эти команды транслятор рассматривает, как правильные.
Часто возникает необходимость выполнить обратную операцию — к паре байтов обратиться, как к слову. Для этого надо использовать оператор word ptr:
okey db 'OK'
mov AX,word ptr okey
Здесь оба байта из байтовой переменной okey переносятся в регистр АХ. При этом первый по порядку байт, т.е. байт с меньшим адресом, содержащий букву "О" (можно считать, что он является младшим в слове
"ОК"), отправится в младшую половину АХ — регистр AL, а второй по порядку байт, с буквой "К", займет регистр АН.
До сих пор речь шла о данных, которые, в сущности, являлись переменными, в том смысле, что под них выделялась память и их можно было модифицировать. Язык ассемблера позволяет также использовать константы, которые являются символическими обозначениями чисел и могут использоваться всюду в тексте программы, как наглядные эквиваленты этих чисел:
maxsize - OFFFFh mov CX,maxsize mov CX,OFFFFh
Последние две команды полностью эквивалентны.
При определении констант допустимо выполнение арифметических операций. Пусть нам надо задать позицию символа (или строки символов) на экране. Учитывая, что каждый символ записывается в видеопамяти в двух байтах (в первом — код ASCII символа, а во втором — его атрибут), строка экрана имеет длину 80 символов, а высота экрана составляет 25 строк, то для вывода некоторого символа в середину экрана его смещение в видеопамяти от начала видеостраницы можно определить следующим образом:
position=80*2*12+40*2
Такая запись достаточно наглядна, и ее легко модифицировать, если мы решим вывести символ в какую-то другую область экрана.
Константами удобно пользоваться для определения длины текстовых строк:
* mes db 'Ждите'
mes_len = S-mes
В этом примере константа mes_len получает значение длины строки mes (в данном случае 5 байт), которая вычисляется как разность значения счетчика текущего адреса после определения строки и ее начального адреса mes. Такой способ удобен тем, что при изменении содержимого строки достаточно перетранслировать программу, и та же константа mes_len автоматически получит новое значение.
2.4. Структуры и записи
Структуры
Структуры представляют собой шаблоны с описаниями форматов данных, которые можно накладывать на различные участки памяти, чтобы затем обращаться к полям этих участков с помощью мнемонических имен, определенных в описании структуры. Структуры особенно удобны в тех случаях, когда мы обращаемся к областям памяти, не входящим в сегменты программы, т.е. к полям, которые нельзя описать с помощью сим-
Глава 2
^Основы программирования
волических имен. Используются структуры также и в тех случаях, когда в программе многократно повторяются сложные коллекции данных с единым строением, но различающимися значениями.
Пусть в программе, выполняющей обработку медицинской информации о пациентах, надо объявить несколько блоков данных с однородными сведениями о нескольких пациентах. Такой комплект данных удобно оформить в виде структуры, придав как всей структуре, так и составляющим ее данным наглядные имена:
meddata struc;Структура с именем meddata
index dd 0;Номер карты \
sex db 0;Пол \
birth dw 0;Год рождения
datein db' / / ';Дата поступления
dateout db' / / ';Дата выписки
meddata ends;Конец описания структуры
Описание структуры можно располагать в любом месте программы, но до описания конкретных структурных переменных. Транслятор, встретившись с описанием структуры, не транслирует ее текст, т.е. не выделяет место в памяти, а просто запоминает приведенное описание, чтобы воспользоваться им в дальнейшем, если в программе встретятся объявления переменных типа этой структуры.
В сегменте данных можно объявить любое количество переменных, соответствующих по составу описанной ранее структуре, дав им произвольные имена. Эти переменные можно заполнить при их объявлении конкретными данными (разумеется, соответствующими элементам описанной ранее структуры), но можно и не указывать конкретных данных, если данную переменную предполагается инициализировать не на этапе ее объяшхения, а по ходу выполнения программы. В последнем случае транслятор выделяет под переменную место в памяти (в нашем примере 23 байт), заполнив ее той конкретной информацией, которая была указана в описании структуры:
data segment
patl meddata <1234567,'M',1955,'13/06/98Yl5/06/98'>
pat2 meddata <1982234,V, 1932,' 18/06/98','25/06/98'>
pat3 meddata <4389012,'ж',1966,'01/12/97','15/12/97'>
pattemp meddata <> -;
data |
Имена patl, pat2 и т.д. будут служить именами переменных, каждая из которых содержит полный комплект данных об одном пациенте. Угловые скобки ограничивают конкретные данные, поступающие в каждую структурную переменную. Для переменной с именем pattemp транслятор выделит в памяти 23 байт, поместив в нее в точности то, что было указано в описании структуры (нули и два символьные шаблона для даты): |
ends
о,о,о; //•;//'
При обработке данных в программе можно пользоваться мнемоничес-обозначениями всей структуры и ее составляющих, причем имена элементов структуры должны отделяться точкой:
mov EAX,patl.index;EAX=1234567
mov SI,offset patl.datein;SI=CMeni;eHHe элемента patl.datein
mov DL,pat3.sex;ОЬ='ж'
Особенности использования в приложениях DOS 32-разрядных регистров (ЕАХ в первой строке приведенного фрагмента) будут описаны в гл. 4.
Адрес конкретной структурной переменной можно поместить в базовый или индексный регистр, и пользоваться им в конструкциях с косвенной адресацией:
mov BX,offset pat3;ВХ=смещение pat3
mov EAX,[BX].index;EAX=4389012
mov [BX].sex='M';Программная инициализация
Имена элементов структуры являются, в сущности, смещениями к этим элементам от начала структуры. В некоторых случаях их можно использовать в этом качестве и без предваряющей точки: