воскресенье, 31 мая 2009 г.

Описание данных

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

Формат описания данных:

[имя] Dn выражение

Имя элемента данных не обязательно (это указывается квадратными скобками), но если в программе имеются ссылки (обращения) на некоторый элемент, то это делается посредством имени.
Имена данных могут включать латинские буквы, цифры (не в качестве первого знака имени) и некоторые специальные знаки, например, знаки подчеркивания (_), доллара ($) и коммерческого at (@). Длину имени некоторые ассемблеры ограничивают (например, ассемблер MASM - 31 символом), другие - нет, но в любом случае слишком длинные имена затрудняют чтение программы. С другой стороны, имена данных следует выбирать таким образом, чтобы они отражали назначение конкретного данного, например counter для счетчика или filename для имени файла:

counter dw 10000
filename db 'a:\myfile.001'

Для определения данных используются, главным образом, три директивы ассемблера: db (define byte, определить байт) для записи байтов, dw (define word, определить слово) для записи слов и dd (define double, определить двойное слово) для записи двойных слов. Кроме перечисленных, имеются и другие директивы, например df (define fanvord, определить поле из 6 байт), dq (define quadword, определить четверное слово) или dt (define tcraword, определить 10-байтовую переменную), но они используются значительно реже.

Выражение может содержать числовое данное, например:

FLD1 DB 25

или знак вопроса для неопределенного значения, например

FLDB DB ?

Выражение может содержать несколько числовых данных, разделенных запятыми и ограниченными только длиной строки:

FLD3 DB 11, 12, 13, 14, 15, 16, ...

Ассемблер определяет эти данные в виде последовательности смежных байт. Ссылка по имени FLD3 указывает на первое число, 11, по FLD3+1 - на второе, 12. (FLD3 можно представить как FLD3+0). Например команда

MOV AL,FLD3+3

загружает в регистр AL значение 14 (шест. 0E).

Выражение допускает также повторение константы в следующем формате:

[имя] Dn число-повторений DUP (выражение)

Следующие три примера иллюстрируют повторение:

DW 10 DUP(?) ;Десять неопределенных слов
DB 5 DUP(14) ;Пять байт, содержащих шест.14
DB 3 DUP(4 DUP(8));Двенадцать восьмерок

В третьем примере сначала генерируется четыре копии десятичной 8 (8888), и затем это значение повторяется три раза, давая в результате двенадцать восьмерок.

Выражение может содержать символьную строку или числовое данное.

Символьная строка используется для описания данных, таких как, например, имена людей или заголовки страниц. Содержимое строки отмечается одиночными кавычками, например, 'PC' или двойными кавычками - "PC". Ассемблер переводит символьные строки в объектный код в обычном формате ASCII. Символьная строка определяется только директивой DB, в которой указывается более два или более символов в нормальной последовательности слева направо. Следовательно, директива DB представляет единственно возможный формат для определения символьных данных.

Числовое данное используются для арифметических величин, для адресов памяти и т.п. Ассемблер преобразует все числовые данные в шестнадцатеричные и записывает байты в объектном коде в обратной последовательности - справа налево.
Значения числовых данных можно записывать в формате различных систем счисления; чаще других используются десятичная и шестнадцатеричная запись:

size dw 256 ;В ячейку size записывается
;десятичное число 256
setb7 db 80h ;В ячейку setb7 записывается
;16-ричное число 80h

Необходимо отметить неточность приведенных выше комментариев. В памяти компьютера могут храниться только двоичные коды. Если мы говорим, что в какой-то ячейке записано десятичное число 128, мы имеем в виду не физическое содержимое ячейки, а лишь форму представления этого числа в исходном тексте программы. В слове с именем size фактически будет записан двоичный код 0000000100000000, являющийся двоичным эквивалентом десятичного числа 256. Во втором случае в байте с именем setb7 будет записан двоичный эквивалент шестнадцатеричного числа 80h, который составляет 10000000 (т.е. байт с установленным битом 7, откуда и получила имя эта ячейка).

Форматы записи числовых данных

Десятичный формат допускает десятичные цифры от 0 до 9 и обозначается последней буквой D, которую можно не указывать, например, 125 или 125D. Несмотря на то, что ассемблер позволяет кодирование в десятичном формате, он преобразует эти значения в шестнадцатеричный объектный код. Например, десятичное число 125 преобразуется в 7Dh.

Шестнадцатеричный формат допускает шестнадцатеричные цифры от 0 до F и бозначается последней буквой H. Так как ассемблер полагает, что с буквы начинаются идентификаторы, то первой цифрой шестнадцатеричного данного должна быть цифра от 0 до 9. Например, 2EH или 0FFFH, которые ассемблер преобразует соответственно в 2E и FF0F (байты во втором примере записываются в объектный код в обратной последовательности).

Двоичный формат допускает двоичные цифры 0 и 1 и обозначается последней буквой B. Двоичный формат обычно используется для более четкого представления битовых значений в логических командах AND, OR, XOR и TEST.

Восьмеричный формат допускает восьмеричные цифры от 0 до 7 и обозначается последней буквой Q или O, например, 253Q. На сегодня восьмеричный формат используется весьма редко.

При записи символьных и числовых данных следует помнить, что, например, символьная строка, определенная как DB '12', представляет символы ASCII и генерирует шестнадцатеричное значение 3132h, а числовое данное, определенное как DB 12, представляет десятичное число и генерирует шестнадцатеричное 0Ch.

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

mov AX,size

занесет в регистр АХ содержимое ячейки size (число 256), независимо от того, в каком месте сегмента данных эта ячейка определена, и в какое место физической памяти она попала. Однако программист, использующий язык ассемблера, должен иметь отчетливое представление о том, каким образом назначаются адреса ячейкам программы, и уметь работать не только с символическими обозначениями, но и со значениями адресов. Для обсуждения этого вопроса рассмотрим пример сегмента данных, в котором определяются данные различных типов. В левой колонке укажем смещения данных (в шестнадцатеричной форме), вычисляемые относительно начала сегмента.

data segment
0000h counter dw 10000
0002h pages db "Страница 1"
000Ch numbers db 0, 1, 2, 3, 4
0011h page_addr dw pages
data ends

Сегмент данных начинается с данного по имени counter, которое описано, как слово (2 байт) и содержит число 10000. Очевидно, что его смещение равно 0. Поскольку это данное занимает 2 байт, следующее за ним данное pages получило смещение 2. Данное pages описывает строку текста длиной 10 символов и занимает в памяти столько же байтов, поэтому следующее данное numbers получило относительный адрес 2 + 10 = 12 = Ch. В поле numbers записаны 5 байтовых чисел, поэтому последнее данное сегмента с именем page_addr размещается по адресу Ch + 5 = 11h.
Ассемблер, начиная трансляцию сегмента (в данном случае сегмента данных) начинает отсчет его относительных адресов. Этот отсчет ведется в специальной переменной транслятора (не программы!), которая называется счетчиком текущего адреса и имеет символическое обозначение знака доллара ($). По мере обработки полей данных, их символические имена сохраняются в создаваемой ассемблером таблице имен вместе с соответствующими им значениями счетчика текущего адреса. Другими словами, введенные нами символические имена получают значения, равные их смещениям. Таким образом, с точки зрения транслятора counter равно 0, pages - 2, numbers - Ch и т.д. Поэтому предложение

page_addr dw pages

трактуется ассемблером, как

page_addr dw 2

и приводит к записи в слово с относительным адресом 11h числа 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? Да никакого, если к этим числам мы все равно обращаемся по отдельности. Удобнее было объявить этот массив таким образом:

nmb0 db 0
nmbl db 1
nmb2 db 2
nmb3 db 3
nmb4 db 4

В этом случае для обращения к последнему элементу не надо вычислять его адрес, а можно воспользоваться именем nmb4. Если, с другой стороны, мы хотим работать с числами, как с массивом, используя индексы отдельных элементов (о чем речь будет идти позже), то присвоение массиву общего имени представляется естественным. Получение последнего элемента массива по его индексу выполняется с помощью такой последовательности команд:

mov SI,4 ;Индекс элемента в массиве
mov DL,numbers[SI] ;Обращение по адресу
;numbers + содержимое SI

Иногда желательно обращаться к элементам массива (обычно небольшого размера) то с помощью индексов, то по их именам. Для этого надо к описанию массива, как последовательности отдельных данных, добавить дополнительное символическое описание адреса начала массива с помощью директивы ассемблера label (метка):

numbers label byte
nmb0 db 0
nmbl db 1
nmb2 db 2
nmb3 db 3
nmb4 db 4

Метка numbers должна быть объявлена в данном случае с описателем byte, так как данные, следующие за этой меткой, описаны как байты и мы планируем работать с ними именно как с байтами. Если нам нужно иметь массив слов, то отдельные элементы массива следует объявить с помощью директивы dw, а метке numbers придать описатель word:

numbers label word
nmb0 dw 0
nmbl dw 1
nmb2 dw 2
nmb3 dw 3
nmb4 dw 4

В чем состоит различие двух последних описаний данных? Различие есть, и весьма существенное. Хотя в обоих случаях в память записывается натуральный ряд чисел от 0 до 4, однако в первом варианте под каждое число в памяти отводится один байт, а во втором - слово. Если мы в дальнейшем будем изменять значения элементов нашего массива, то в первом варианте каждому числу можно будет задавать значения от 0 до 255, а во втором - от 0 до 65535.
Выбирая для данных способ их описания, необходимо иметь в виду, что ассемблер выполняет проверку размеров используемых данных и не пропускает команды, в которых делается попытка обратиться к байтам, как к словам, или к словам - как к байтам. Рассмотрим последний вариант описания массива numbers. Хотя под каждый элемент выделено целое слово, однако реальные числа невелики и вполне поместятся в байт. Может возникнуть искушение поработать с ними, как с байтами, перенеся предварительно в байтовые регистры:

mov AL,nmb0 ;Переносим nmb0 в AL
mov DL,nmbl ;Переносим nmb1 в AL
mov CL,nmb2 ;Переносим nmb2 в AL

Так делать нельзя. Транслятор сообщит о грубой ошибке - несоответствии типов, и не будет создавать объектный файл. Однако довольно часто возникает реальная потребность в операциях такого рода. Для таких случаев предусмотрен специальный атрибутивный оператор byte ptr (byte pointer, байтовый указатель), с помощью которого можно на время выполнения одной Команды изменить размер операнда:

mov AL,byte ptr nmb0
mov DL,byte ptr nmbl
mov CL,byte ptr nmb2

Эти команды транслятор рассматривает, как правильные. Но следует заметить, что эта команда будет указывать на младший байт в слове (nmb0, nmb1 и т.д.).
Часто возникает необходимость выполнить обратную операцию - к паре байтов обратиться, как к слову. Для этого надо использовать оператор word ptr:

okey db 'OK'

mov AX,word ptr okey

Здесь оба байта из байтовой переменной okey переносятся в регистр АХ. При этом первый по порядку байт, т.е. байт с меньшим адресом, содержащий букву "О", отправится в младшую половину АХ - регистр AL, а второй по порядку байт, с буквой "К", займет регистр АН.
До сих пор речь шла о данных, которые, в сущности, являлись переменными, в том смысле, что под них выделялась память и их можно было модифицировать. Язык ассемблера позволяет также использовать константы, которые являются символическими обозначениями чисел и могут использоваться всюду в тексте программы, как наглядные эквиваленты этих чисел:

maxsize = 0FFFFh
......
mov CX,maxsize
mov CX,0FFFFh


Последние две команды полностью эквивалентны.
При определении констант допустимо выполнение арифметических операций. Пусть нам надо задать позицию символа (или строки символов) на экране. Учитывая, что каждый символ записывается в видеопамяти в двух байтах (в первом - код ASCII символа, а во втором - его атрибут), строка экрана имеет длину 80 символов, а высота экрана составляет 25 строк, то для вывода некоторого символа в середину экрана его смещение в видеопамяти от начала видеостраницы можно определить следующим образом:

position=80*2*12+40*2

Такая запись достаточно наглядна, и ее легко модифицировать, если мы решим вывести символ в какую-то другую область экрана.
Константами удобно пользоваться для определения длины текстовых строк:

mes db 'Ждите'
mes_len = $-mes

В этом примере константа mes_len получает значение длины строки mes (в данном случае 5 байт), которая вычисляется как разность значения счетчика текущего адреса после определения строки и ее начального адреса mes. Такой способ удобен тем, что при изменении содержимого строки достаточно перетранслировать программу, и та же константа mes_len автоматически получит новое значение.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ БАЙТА (DB)

Из различных директив, определяющих элементы данных, наиболее полезной является DB (определить байт). Символьное выражение в директиве DB может содержать строку символов любой длины. Числовое выражение в директиве DB может содержать одну или более однобайтовых констант. Один байт выражается двумя шестнадцатеричными цифрами. Если интерпретировать числовые значения, описанные директивой DB, как содержащие знак, то наибольшее положительное шестнадцатеричное число в одном байте это 7F, все "большие" числа от 80 до FF представляют отрицательные значения. В десятичном исчислении эти пределы выражаются числами +127 и -128.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ СЛОВА (DW)

Директива DW определяет элементы, которые имеют длину в одно слово (два байта). Символьное выражение в DW ограничено двумя символами, которые ассемблер представляет в объектном коде так, что, например, 'PC' становится 'CP'. Для определения символьных строк директива DW имеет ограниченное применение.
Два байта представляются четырьмя шестнадцатеричными цифрами. Если интерпретировать числовые значения, описанные директивой DW, как содержащие знак, то наибольшее положительное шестнадцатеричное число в двух байтах это 7FFF; все "большие" числа от 8000 до FFFF представляют отрицательные значения. В десятичном исчислении эти пределы выражаются числами +32767 и -32768.
Для форматов директив DW, DD и DQ ассемблер преобразует константы в шестнадцатеричный объектный код, но записывает его в обратной последовательности.
Таким образом десятичное значение 12345 преобразуется в шестнадцатеричное 3039, но
записывается в объектном коде как 3930.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ ДВОЙНОГО СЛОВА (DD)

Директива DD определяет элементы, которые имеют длину в два слова (четыре байта). Если интерпретировать числовые значения, описанные директивой DD , как содержащие знак, то, наибольшее положительное шестнадцатеричное число в четырех байтах это 7FFFFFFF. Все "большие" числа от 80000000 до FFFFFFFF представляют отрицательные
значения. В десятичном исчислении эти пределы выражаются числами +2147483647 и -2147483648.
Ассемблер преобразует все числовые константы в директиве DD в шестнадцатеричное представление, но записывает объектный код в обратной последовательности. Таким образом десятичное значение 12345 преобразуется в шестнадцатеричное 00003039, но
записывается в объектном коде как 39300000.
Символьное выражение директивы DD ограничено двумя символами.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ УЧЕТВЕРЕННОГО СЛОВА (DQ)

Директива DQ определяет элементы, имеющие длину четыре слова (восемь байт). Наибольшее без знаковое значение может быть FFFFFFFFFFFFFFFF, что равно десятичному 18446744073709551615. Отрицательные и положительные значения считайте сами :).

Двоично-десятичный

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ ДЕСЯТИ БАЙТ (DT)

Директива DT определяет элементы данных, имеющие длину в десять байт.
Назначение этой директивы связано с упакованным двоично-десятичным форматом данных.

Ну и на последок, кусок кода с моими экспериментами:

Комментариев нет: