Показаны сообщения с ярлыком синтаксис ассемблера. Показать все сообщения
Показаны сообщения с ярлыком синтаксис ассемблера. Показать все сообщения

суббота, 6 июня 2009 г.

Данные сложного типа. Массивы.

Массив - структурированный тип данных, состоящий из некоторого числа элементов одного типа.

Для того чтобы разобраться в возможностях и особенностях обработки массивов в программах на ассемблере, нужно ответить на следующие вопросы:
  • Как описать массив в программе?
  • Как инициализировать массив, то есть как задать начальные значения его элементов?
  • Как организовать доступ к элементам массива?
  • Как организовать массивы с размерностью более одной?
  • Как организовать выполнение типовых операций с массивами?
Описание и инициализация массива в программе

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

1) Перечислением элементов массива в поле операндов одной из директив описания данных. При перечислении элементы разделяются запятыми. К примеру:

;массив из 5 элементов.Размер каждого
элемента 4 байта:
mas dd 1,2,3,4,5

2) Используя оператор повторения dup. К примеру:

;массив из 5 нулевых элементов.
;Размер каждого элемента 2 байта:
mas dw 5 dup (0)

Такой способ определения используется для резервирования памяти с целью размещения и инициализации элементов массива.

3) Используя директивы label и rept. Пара этих директив может облегчить описание больших массивов в памяти и повысить наглядность такого описания. Директива rept относится к макросредствам языка ассемблера и вызывает повторение указанное число раз строк, заключенных между директивой и строкой endm. К примеру, определим массив байт в области памяти, обозначенной идентификатором mas_b. В данном случае директива label определяет символическое имя mas_b, аналогично тому, как это делают директивы резервирования и инициализации памяти. Достоинство директивы label в том, что она не резервирует память, а лишь определяет характеристики объекта. В данном случае объект — это ячейка памяти. Используя несколько директив label, записанных одна за другой, можно присвоить одной и той же области памяти разные имена и разный тип, что и сделано в следующем фрагменте:

...
n=0
...
mas_b label byte
mas_w label word
rept 4
dw 0f1f0h
endm

В результате в памяти будет создана последовательность из четырех слов f1f0. Эту последовательность можно трактовать как массив байт или слов в зависимости от того, какое имя области мы будем использовать в программе — mas_b или mas_w.

4) Использование цикла для инициализации значениями области памяти, которую можно будет впоследствии трактовать как массив.
Посмотрим на примере, каким образом это делается.

;prg_12_1.asm
MASM
MODEL small
STACK 256
.data
mes db 0ah,0dh,'Массив- ','$'
mas db 10 dup (?) ;исходный массив
i db 0
.code
main:
mov ax,@data
mov ds,ax
xor ax,ax ;обнуление ax
mov cx,10 ;значение счетчика цикла в cx
mov si,0 ;индекс начального элемента в cx
go: ;цикл инициализации
mov bh,i ;i в bh
mov mas[si],bh ;запись в массив i
inc i ;инкремент i
inc si ;продвижение к следующему
;элементу массива
loop go ;повторить цикл
;вывод на экран получившегося массива
mov cx,10
mov si,0
mov ah,09h
lea dx,mes
int 21h
show:
mov ah,02h ;функция вывода значения
;из al на экран
mov dl,mas[si]
add dl,30h ;преобразование числа в символ
int 21h
inc si
loop show
exit:
mov ax,4c00h ;стандартный выход
int 21h
end main ;конец программы


Доступ к элементам массива

При работе с массивами необходимо четко представлять себе, что все элементы массива располагаются в памяти компьютера последовательно.
Само по себе такое расположение ничего не говорит о назначении и порядке использования этих элементов. И только лишь программист с помощью составленного им алгоритма обработки определяет, как нужно трактовать эту последовательность байт, составляющих массив. Так, одну и ту же область памяти можно трактовать как одномерный массив, и одновременно те же самые данные могут трактоваться как двухмерный массив. Все зависит только от алгоритма обработки этих данных в конкретной программе. Сами по себе данные не несут никакой информации о своем “смысловом”, или логическом, типе. Помните об этом принципиальном моменте.
Эти же соображения можно распространить и на индексы элементов массива. Ассемблер не подозревает об их существовании и ему абсолютно все равно, каковы их численные смысловые значения.
Для того чтобы локализовать определенный элемент массива, к его имени нужно добавить индекс. Так как мы моделируем массив, то должны позаботиться и о моделировании индекса. В языке ассемблера индексы массивов — это обычные адреса, но с ними работают особым образом. Другими словами, когда при программировании на ассемблере мы говорим об индексе, то скорее подразумеваем под этим не номер элемента в массиве, а некоторый адрес.

Давайте еще раз обратимся к описанию массива. К примеру, в программе статически определена последовательность данных:

dim dw 0011h,2233h,4455h,6677h,8899h

Пусть эта последовательность чисел трактуется как одномерный массив. Размерность каждого элемента определяется директивой dw, то есть она равна 2 байта. Чтобы получить доступ к числу 6677h, нужно к адресу массива прибавить 6. Нумерация элементов массива в ассемблере начинается с нуля.
В общем случае для получения адреса элемента в массиве необходимо начальный (базовый) адрес массива сложить с произведением индекса этого элемента на размер элемента массива:

база + (индекс*размер элемента)

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

Эта программа ни чего не выводит на экран. Предназначена просто для медитации на нее под отладчиком. Внимание!!! При отладке, чтобы увидеть работу цикла, на команде loop go, нужно нажимать клавишу F7.
Зарисовка из медитации на эту прогу:

Двухмерные массивы

С представлением одномерных массивов в программе на ассемблере и организацией их обработки все достаточно просто. А как быть если программа должна обрабатывать двухмерный массив? Все проблемы возникают по-прежнему из-за того, что специальных средств для описания такого типа данных в ассемблере нет. Двухмерный массив нужно моделировать. На описании самих данных это почти никак не отражается — память под массив выделяется с помощью директив резервирования и инициализации памяти.

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

Если последовательность однотипных элементов в памяти трактуется как двухмерный массив, расположенный по строкам, то адрес элемента (i, j) вычисляется по формуле

(база + количество_элементов_в_строке * размер_элемента * i+j)

Здесь i = 0...n–1 указывает номер строки, а j = 0...m–1 указывает номер столбца.

Например, пусть имеется массив чисел (размером в 1 байт) mas(i, j) с размерностью 4 на 4
(i= 0...3, j = 0...3):

23 04 05 67
05 06 07 99
67 08 09 23
87 09 00 08

В памяти элементы этого массива будут расположены в следующей последовательности:

23 04 05 67 05 06 07 99 67 08 09 23 87 09 00 08

Если мы хотим трактовать эту последовательность как двухмерный массив, приведенный выше, и извлечь, например, элемент
mas(2, 3) = 23, то проведя нехитрый подсчет, убедимся в правильности наших рассуждений:

Эффективный адрес mas(2, 3) = mas + 4 * 1 * 2 + 3 = mas + 11

Посмотрите на представление массива в памяти и убедитесь, что по этому смещению действительно находится нужный элемент массива. Прога для медитации:

Эта программа, тоже ни чего не выводит, так как предназначена для выполнения ее под отладчиком, для пущего научения.

пятница, 5 июня 2009 г.

Операторы

Возможные типы операндов мы уже рассмотрели. Перечислим теперь возможные типы операторов ассемблера и синтаксические правила формирования выражений ассемблера.
  • Арифметические операторы
  • Операторы сдвига
  • Операторы сравнения
  • Логические операторы
  • Индексный оператор
  • Оператор переопределения типа
  • Оператор переопределения сегмента
  • Оператор именования типа структуры
  • Оператор получения сегментной составляющей адреса выражения
  • Оператор получения смещения выражения
В табл. 2 приведены поддерживаемые языком ассемблера операторы и перечислены их приоритеты. Дадим краткую характеристику операторов:

Арифметические операторы. К ним относятся:
  • унарные “+” и “–”;
  • бинарные “+” и “–”;
  • умножения “*”;
  • целочисленного деления “/”;
  • получения остатка от деления “mod”.
Эти операторы расположены на уровнях приоритета 6, 7, 8 в табл. 2. Например,

tab_size equ 50 ;размер массива в байтах
size_el equ 2 ;размер элементов

;вычисляется число элементов массива
;и заносится в
регистр cx
mov cx,tab_size / size_el ;оператор “/”

Операторы сдвига выполняют сдвиг выражения на указанное количество разрядов.

Пример:
mask_b equ 10111011

mov al,mask_b shr 3 ;al=00010111

Операторы сравнения (возвращают значение “истина” или “ложь”) предназначены для формирования логических выражений (см. табл.1). Логическое значение “истина” соответствует цифровой единице, а “ложь” — нулю.

Пример:

tab_size equ 30 ;размер таблицы

mov al,tab_size ge 50 ;загрузка размера
;таблицы в al
cmp al,0 ;если tab_size меньше 50, то
je m1 ;переход на m1

m1: …

В этом примере если значение tab_size больше или равно 50, то результат в al равен 0ffh, а если tab_size меньше 50, то al равно 00h. Команда cmp сравнивает значение al с нулем и устанавливает соответствующие флаги в flags/eflags. Команда je на основе анализа этих флагов передает или не передает управление на метку m1.

Таблица 1. Операторы сравнения
Оператор Значение
eq ИСТИНА, если выражение_1 равно выражение_2
ne ИСТИНА, если выражение_1 не равно выражение_2
lt ИСТИНА, если выражение_1 меньше выражение_2>ИСТИНА, если выражение_1 не равно выражение_2
le ИСТИНА, если выражение_1 меньше или равно выражение_2
gt ИСТИНА, если выражение_1 больше выражение_2
ge ИСТИНА, если выражение_1 больше или равно выражение_2

Логические операторы выполняют над выражениями побитовые операции. Выражения должны быть абсолютными, то есть такими, численное значение которых может быть вычислено транслятором.

Пример:

flags equ 10010011

mov al,flags xor 01h ;
al=10010010;пересылка в al поля flags с
;инвертированным правым битом

Индексный оператор [ ]. Не удивляйтесь, но скобки тоже являются оператором, и транслятор их наличие воспринимает как указание сложить значение выражение_1 за этими скобками с выражение_2, заключенным в скобки.

Пример:

mov ax,mas[si] ;пересылка слова по адресу
;mas+(si) в регистр ax

Заметим, что в литературе по ассемблеру принято следующее обозначение: когда в тексте речь идет о содержимом регистра, то его название берут в круглые скобки. Мы также будем придерживаться этого обозначения.
К примеру, в нашем случае запись в комментариях последнего фрагмента программы mas + (si) означает вычисление следующего выражения: значение смещения символического имени mas плюс содержимое регистра si.

Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением. Тип может принимать одно из следующих значений: byte, word, dword, qword, tbyte, near, far.

Пример:

d_wrd dd 0
...
mov al,byte ptr d_wrd+1 ;пересылка второго байта
;из двойного слова

Поясним этот фрагмент программы. Переменная d_wrd имеет тип двойного слова. Что делать, если возникнет необходимость обращения не ко всей переменной, а только к одному из входящих в нее байтов (например, ко второму)? Если попытаться сделать это командой
mov al,d_wrd+1, то транслятор выдаст сообщение о несовпадении типов операндов. Оператор ptr позволяет непосредственно в команде переопределить тип и выполнить команду.

Оператор переопределения сегмента : (двоеточие) заставляет вычислять физический адрес относительно конкретно задаваемой сегментной составляющей: “имя сегментного регистра”, “имя сегмента” из соответствующей директивы SEGMENT или “имя группы”.

Этот момент очень важен, поэтому поясню его подробнее. При обсуждении сегментации мы говорили о том, что микропроцессор на аппаратном уровне поддерживает три типа сегментов — кода, стека и данных. В чем заключается такая аппаратная поддержка? К примеру, для выборки на выполнение очередной команды микропроцессор должен обязательно посмотреть содержимое сегментного регистра cs и только его. А в этом регистре, как мы знаем, содержится (пока еще не сдвинутый) физический адрес начала сегмента команд. Для получения адреса конкретной команды микропроцессору остается умножить содержимое cs на 16 (что означает сдвиг на четыре разряда) и сложить полученное 20-битное значение с 16-битным содержимым регистра ip. Примерно то же самое происходит и тогда, когда микропроцессор обрабатывает операнды в машинной команде. Если он видит, что операнд — это адрес (эффективный адрес, который является только частью физического адреса), то он знает, в каком сегменте его искать — по умолчанию это сегмент, адрес начала которого записан в сегментном регистре ds.

А что же с сегментом стека? Вспоминаем что уже писалось про регистры. В контексте нашего рассмотрения нас интересуют регистры sp и bp. Если микропроцессор видит в качестве операнда (или его части, если операнд — выражение) один из этих регистров, то по умолчанию он формирует физический адрес операнда используя в качестве его сегментной составляющей содержимое регистра ss. Что подразумевает термин “по умолчанию”? Это набор микропрограмм в блоке микропрограммного управления, каждая из которых выполняет одну из команд в системе машинных команд микропроцессора. Каждая микропрограмма работает по своему алгоритму. Изменить его, конечно же, нельзя, но можно чуть-чуть подкорректировать. Делается это с помощью необязательного поля префикса машинной команды. Если мы согласны с тем, как работает команда, то это поле отсутствует. Если же мы хотим внести поправку (если, конечно, она допустима для конкретной команды) в алгоритм работы команды, то необходимо сформировать соответствующий префикс.
Префикс представляет собой однобайтовую величину, численное значение которой определяет ее назначение. Микропроцессор распознает по указанному значению, что этот байт является префиксом, и дальнейшая работа микропрограммы выполняется с учетом поступившего указания на корректировку ее работы. Сейчас нас интересует один из них - префикс замены (переопределения) сегмента. Его назначение состоит в том, чтобы указать микропроцессору (а по сути, микропрограмме) на то, что мы не хотим использовать сегмент по умолчанию. Возможности для подобного переопределения, конечно, ограничены. Сегмент команд переопределить нельзя, адрес очередной исполняемой команды однозначно определяется парой cs:ip. А вот сегменты стека и данных — можно. Для этого и предназначен оператор “:”. Транслятор ассемблера, обрабатывая этот оператор, формирует соответствующий однобайтовый префикс замены сегмента.

Пример:

.code
...
jmp met1 ;обход обязателен, иначе поле ind
;будет трактоваться как очередная команда
ind db 5 ;описание поля данных в сегменте команд
met1:
...
mov al,cs:ind ;переопределение сегмента
;позволяет работать с
данными,
;определенными внутри сегмента кода


Оператор именования типа . (точка) также заставляет транслятор производить определенные вычисления, если он встречается в выражении.

Оператор получения сегментной составляющей адреса выражения seg возвращает физический адрес сегмента для выражения, в качестве которого могут выступать метка, переменная, имя сегмента, имя группы или некоторое символическое имя.

Оператор получения смещения выражения offset позволяет получить значение смещения выражения в байтах относительно начала того сегмента, в котором выражение определено.
Пример:

.data
pole dw 5
...
.code
...
mov ax,seg pole
mov es,ax
mov dx,offset pole ;теперь в паре es:dx
;полный адрес pole

Как и в языках высокого уровня, выполнение операторов ассемблера при вычислении выражений осуществляется в соответствии с их приоритетами (см. табл. 2). Операции с одинаковыми приоритетами выполняются последовательно слева направо. Изменение порядка выполнения возможно путем расстановки круглых скобок, которые имеют наивысший приоритет.

Таблица 2. Операторы и их приоритет
Оператор Приоритет
length, size, width, mask, (, ), [, ], <, > 1
. 2
: 3
ptr, offset, seg, type, this 4
high, low 5
+, - (унарные) 6
*, /, mod, shl, shr 7
+, -, (бинарные) 8
eq, ne, lt, le, gt, ge 9
not 10
and 11
or, xor 12
short, type 13

четверг, 4 июня 2009 г.

Операнды

Возможно провести следующую классификацию операндов:
  • постоянные, или непосредственные, операнды
  • адресные операнды
  • перемещаемые операнды
  • счетчик адреса
  • регистровый операнд
  • базовый и индексный операнды
  • структурные операнды
  • Записи
Рассмотрим подробнее характеристику операндов из приведенной классификации:
  • Постоянные или непосредственные операнды — число, строка, имя или выражение, имеющие некоторое фиксированное значение. Имя не должно быть перемещаемым, то есть зависеть от адреса загрузки программы в память. К примеру, оно может быть определено операторами equ или =.

    num equ 5
    imd = num-2
    mov al,num ;эквивалентно mov al,5
    ;5 здесь непосредственный операнд
    add [si],imd ; imd=3 - непосредственный операнд
    mov al,5 ;5 - непосредственный операнд

    В данном фрагменте определяются две константы, которые затем используются в качестве непосредственных операндов в командах пересылки mov и сложения add.

  • Адресные операнды — задают физическое расположение операнда в памяти с помощью указания двух составляющих адреса: сегмента и смещения (рис.1).
    К примеру:

    mov ax,0000h
    mov ds,ax
    mov ax,ds:0000h ;записать слово в ax из области памяти по
    ;физическому адресу DS:0000
    Здесь третья команда mov имеет адресный операнд.

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

    К примеру:

    data segment
    mas_w dw 25 dup (0)

    code segment

    lea si,mas_w ;mas_w - перемещаемый операнд

    В этом фрагменте mas_w — символьное имя, значением которого является начальный адрес области памяти размером 25 слов. Полный физический адрес этой области памяти будет известен только после загрузки программы в память для выполнения.

  • Счетчик адреса — специфический вид операнда. Он обозначается знаком $.

    Специфика этого операнда в том, что когда транслятор ассемблера встречает в исходной программе этот символ, то он подставляет вместо него текущее значение счетчика адреса. Значение счетчика адреса, или, как его иногда называют, счетчика размещения, представляет собой смещение текущей машинной команды относительно начала сегмента кода.
    В формате листинга счетчику адреса соответствует вторая или третья колонка (в зависимости от того, присутствует или нет в листинге колонка с уровнем вложенности). Если взять в качестве пример любой листинг, то видно, что при обработке транслятором очередной команды ассемблера счетчик адреса увеличивается на длину сформированной машинной команды. Важно правильно понимать этот момент.
    К примеру, обработка директив ассемблера не влечет за собой изменения счетчика. Директивы, в отличие от команд ассемблера, — это лишь указания транслятору на выполнение определенных действий по формированию машинного представления программы, и для них транслятором не генерируется никаких конструкций в памяти. В качестве примера использования в команде значения счетчика адреса можно привести следующий:

    jmp $+3 ;безусловный переход на команду mov
    cld ;длина команды cld составляет 1 байт
    mov al,1

    При использовании подобного выражения для перехода не забывайте о длине самой команды, в которой это выражение используется, так как значение счетчика адреса соответствует смещению в сегменте команд данной, а не следующей за ней команды. В нашем примере команда jmp занимает 2 байта. Но будьте осторожны, длина команды зависит от того, какие в ней используются операнды. Команда с регистровыми операндами будет короче команды, один из операндов которой расположен в памяти. В большинстве случаев эту информацию можно получить, зная формат машинной команды и анализируя колонку листинга с объектным кодом команды.

  • Регистровый операнд — это просто имя регистра. В программе на ассемблере можно использовать имена всех регистров общего назначения и большинства системных регистров.

    mov al,4 ;константу 4 заносим в регистр al
    mov dl,pass+4 ;байт по адресу pass+4 в регистр dl
    add al,dl ;команда с регистровыми операндами

  • Базовый и индексный операнды. Этот тип операндов используется для реализации косвенной базовой, косвенной индексной адресации или их комбинаций и расширений.
  • Структурные операнды используются для доступа к конкретному элементу сложного типа данных, называемого структурой.
  • Записи (аналогично структурному типу) используются для доступа к битовому полю некоторой записи.
Операнды являются элементарными компонентами, из которых формируется часть машинной команды, обозначающая объекты, над которыми выполняется операция.
В более общем случае операнды могут входить как составные части в более сложные образования, называемые выражениями.
Выражения представляют собой комбинации операндов и операторов, рассматриваемые как единое целое.

Результатом вычисления выражения может быть адрес некоторой ячейки памяти или некоторое константное (абсолютное) значение.

среда, 3 июня 2009 г.

Синтаксис ассемблера

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

На этих рисунках:
  • имя метки — идентификатор, значением которого является адрес первого байта того предложения исходного текста программы, которое он обозначает;
  • имя — идентификатор, отличающий данную директиву от других одноименных директив. В результате обработки ассемблером определенной директивы этому имени могут быть присвоены определенные характеристики;
  • код операции (КОП) и директива — это мнемонические обозначения соответствующей машинной команды, макрокоманды или директивы транслятора;
  • операнды — части команды, макрокоманды или директивы ассемблера, обозначающие объекты, над которыми производятся действия. Операнды ассемблера описываются выражениями с числовыми и текстовыми константами, метками и идентификаторами переменных с использованием знаков операций и некоторых зарезервированных слов.
Как использовать синтаксические диаграммы?
Очень просто: для этого нужно всего лишь найти и затем пройти путь от входа диаграммы (слева) к ее выходу (направо). Если такой путь существует, то предложение или конструкция синтаксически правильны. Если такого пути нет, значит эту конструкцию компилятор не примет. При работе с синтаксическими диаграммами обращайте внимание на направление обхода, указываемое стрелками, так как среди путей могут быть и такие, по которым можно идти справа налево. По сути, синтаксические диаграммы отражают логику работы транслятора при разборе входных предложений программы.

Допустимыми символами при написании текста программ являются:
  1. все латинские буквы: A—Z, a—z. При этом заглавные и строчные буквы считаются эквивалентными;
  2. цифры от 0 до 9;
  3. знаки ?, @, $, _, &;
  4. разделители , . [ ] ( ) < > { } + / * % ! ' " ? \ = # ^
Предложения ассемблера формируются из лексем, представляющих собой синтаксически неразделимые последовательности допустимых символов языка, имеющие смысл для транслятора.

Лексемами являются:
  • идентификаторы — последовательности допустимых символов, использующиеся для обозначения таких объектов программы, как коды операций, имена переменных и названия меток. Правило записи идентификаторов заключается в следующем: идентификатор может состоять из одного или нескольких символов. В качестве символов можно использовать буквы латинского алфавита, цифры и некоторые специальные знаки — _, ?, $, @. Идентификатор не может начинаться символом цифры. Длина идентификатора может быть до 255 символов, хотя транслятор воспринимает лишь первые 32, а остальные игнорирует. Регулировать длину возможных идентификаторов можно с использованием опции командной строки mv. Кроме этого существует возможность указать транслятору на то, чтобы он различал прописные и строчные буквы либо игнорировал их различие (что и делается по умолчанию). Для этого применяются опции командной строки /mu, /ml, /mx;
  • цепочки символов — последовательности символов, заключенные в одинарные или двойные кавычки;
  • целые числа в одной из следующих систем счисления: двоичной, десятичной, шестнадцатеричной, восьмеричной. Отождествление чисел при записи их в программах на ассемблере производится по определенным правилам:
    • Десятичные числа не требуют для своего отождествления указания каких-либо дополнительных символов, например 25 или 139. Но можно, для наглядности добавлять символ "d".
    • Для отождествления в исходном тексте программы двоичных чисел необходимо после записи нулей и единиц, входящих в их состав, поставить латинское “b”, например 10010101b.
    • Шестнадцатеричные числа имеют больше условностей при своей записи:
      • Во-первых, они состоят из цифр 0...9, строчных или прописных букв латинского алфавита a, b, c, d, e, f или A, B, C, D, E, F.
      • Во-вторых, у транслятора могут возникнуть трудности с распознаванием шестнадцатеричных чисел из-за того, что они могут состоять как из одних цифр 0...9 (например 190845), так и начинаться с буквы латинского алфавита (например ef15). Для того чтобы "объяснить" транслятору, что данная лексема не является десятичным числом или идентификатором, программист должен специальным образом выделять шестнадцатеричное число. Для этого на конце последовательности шестнадцатеричных цифр, составляющих шестнадцатеричное число, записывают латинскую букву “h”. Это обязательное условие. Если шестнадцатеричное число начинается с буквы, то перед ним записывается ведущий ноль: 0ef15h.
Таким образом, мы разобрались с тем, как конструируются предложения программы ассемблера. Но это лишь самый поверхностный взгляд.

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

Операнды могут комбинироваться с арифметическими, логическими, побитовыми и атрибутивными операторами для расчета некоторого значения или определения ячейки памяти, на которую будет воздействовать данная команда или директива.

вторник, 2 июня 2009 г.

И снова о данных

На просторах Интернета, нашел дополнительную информацию по определению и использованию данных в языке ассемблера. Хоть тут будут и небольшие повторения, но повторение - мать учения.

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

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

  3. Данные сложного типа, которые были введены в язык ассемблера с целью облегчения разработки программ. Сложные типы данных строятся на основе базовых типов, которые являются как бы кирпичиками для их построения. Введение сложных типов данных позволяет несколько сгладить различия между языками высокого уровня и ассемблером. У программиста появляется возможность сочетания преимуществ языка ассемблера и языков высокого уровня (в направлении абстракции данных), что в конечном итоге повышает эффективность конечной программы.
Понятие простого типа данных носит двойственный характер. С точки зрения размерности (физическая интерпретация), микропроцессор аппаратно поддерживает следующие основные типы данных (рис.1):
  • байт — восемь последовательно расположенных битов, пронумерованных от 0 до 7, при этом бит 0 является самым младшим значащим битом;
  • слово — последовательность из двух байт, имеющих последовательные адреса. Размер слова — 16 бит; биты в слове нумеруются от 0 до 15. Байт, содержащий нулевой бит, называется младшим байтом, а байт, содержащий 15-й бит - старшим байтом. Микропроцессоры Intel имеют важную особенность — младший байт всегда хранится по меньшему адресу. Адресом слова считается адрес его младшего байта. Адрес старшего байта может быть использован для доступа к старшей половине слова.
  • двойное слово — последовательность из четырех байт (32 бита), расположенных по последовательным адресам. Нумерация этих бит производится от 0 до 31. Слово, содержащее нулевой бит, называется младшим словом, а слово, содержащее 31-й бит, - старшим словом. Младшее слово хранится по меньшему адресу. Адресом двойного слова считается адрес его младшего слова. Адрес старшего слова может быть использован для доступа к старшей половине двойного слова.
  • учетверенное слово — последовательность из восьми байт (64 бита), расположенных по последовательным адресам. Нумерация бит производится от 0 до 63. Двойное слово, содержащее нулевой бит, называется младшим двойным словом, а двойное слово, содержащее 63-й бит, — старшим двойным словом. Младшее двойное слово хранится по меньшему адресу. Адресом учетверенного слова считается адрес его младшего двойного слова. Адрес старшего двойного слова может быть использован для доступа к старшей половине учетверенного слова.
Кроме трактовки типов данных с точки зрения их разрядности, микропроцессор на уровне команд поддерживает логическую интерпретацию этих типов (рис.2):
  • Целый тип со знаком — двоичное значение со знаком, размером 8, 16 или 32 бита. Знак в этом двоичном числе содержится в 7, 15 или 31-м бите соответственно. Ноль в этих битах в операндах соответствует положительному числу, а единица — отрицательному. Отрицательные числа представляются в дополнительном коде. Числовые диапазоны для этого типа данных следующие:
    • 8-разрядное целое — от –128 до +127;
    • 16-разрядное целое — от –32 768 до +32 767;
    • 32-разрядное целое — от –231 до +231–1.
  • Целый тип без знака — двоичное значение без знака, размером 8, 16 или 32 бита. Числовой диапазон для этого типа следующий:
    • байт — от 0 до 255;
    • слово — от 0 до 65 535;
    • двойное слово — от 0 до 232–1.
  • Указатель на память двух типов:
    • ближнего типа — 32-разрядный логический адрес, представляющий собой относительное смещение в байтах от начала сегмента. Эти указатели могут также использоваться в сплошной (плоской) модели памяти, где сегментные составляющие одинаковы;
    • дальнего типа — 48-разрядный логический адрес, состоящий из двух частей: 16-разрядной сегментной части — селектора, и 32-разрядного смещения.
  • Цепочка — представляющая собой некоторый непрерывный набор байтов, слов или двойных слов максимальной длины до 4 Гбайт.
  • Битовое поле представляет собой непрерывную последовательность бит, в которой каждый бит является независимым и может рассматриваться как отдельная переменная. Битовое поле может начинаться с любого бита любого байта и содержать до 32 бит.
  • Неупакованный двоично-десятичный тип — байтовое представление десятичной цифры от 0 до 9. Неупакованные десятичные числа хранятся как байтовые значения без знака по одной цифре в каждом байте. Значение цифры определяется младшим полубайтом.
  • Упакованный двоично-десятичный тип представляет собой упакованное представление двух десятичных цифр от 0 до 9 в одном байте. Каждая цифра хранится в своем полубайте. Цифра в старшем полубайте (биты 4–7) является старшей.
Отметим, что “Зн” на рис. 2 означает знаковый бит.

Для описания простых типов данных в программе используются специальные директивы описания и инициализации данных, которые, по сути, являются указаниями транслятору на выделение определенного объема памяти. Если проводить аналогию с языками высокого уровня, то директивы резервирования и инициализации данных являются определениями переменных.
Машинного эквивалента этим директивам нет; просто транслятор, обрабатывая каждую такую директиву, выделяет необходимое количество байт памяти и при необходимости инициализирует эту область некоторым значением.
Директивы резервирования и инициализации данных простых типов имеют формат, показанный на (рис.3).

На рис. 3 использованы следующие обозначения:
  • ? показывает, что содержимое поля не определено, то есть при задании директивы с таким значением выражения содержимое выделенного участка физической памяти изменяться не будет. Фактически, создается неинициализированная переменная;
  • значение инициализации — значение элемента данных, которое будет занесено в память после загрузки программы. Фактически, создается инициализированная переменная, в качестве которой могут выступать константы, строки символов, константные и адресные выражения в зависимости от типа данных. Подробная информация приведена в приложении 1;
  • выражение — Эта конструкция позволяет повторить последовательное занесение в физическую память выражения в скобках n раз.
  • имя — некоторое символическое имя метки или ячейки памяти в сегменте данных, используемое в программе.
Директивы описания и инициализации данных:
  • db — резервирование памяти для данных размером 1 байт.
    Директивой db можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для чисел со знаком –128...+127;
      • для чисел без знака 0...255;
    • 8-битовое относительное выражение, использующее операции HIGH и LOW;
    • символьную строку из одного или более символов. Строка заключается в кавычки. В этом случае определяется столько байт, сколько символов в строке.
  • dw — резервирование памяти для данных размером 2 байта.
    Директивой dw можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для чисел со знаком –32 768...32 767;
      • для чисел без знака 0...65 535;
    • выражение, занимающее 16 или менее бит, в качестве которого может выступать смещение в 16-битовом сегменте или адрес сегмента;
    • 1- или 2-байтовую строку, заключенная в кавычки.
  • dd — резервирование памяти для данных размером 4 байта.
    Директивой dd можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 16-битового адреса сегмента и 16-битового смещения;
    • строку длиной до 4 символов, заключенную в кавычки.
  • df — резервирование памяти для данных размером 6 байт;
  • dp — резервирование памяти для данных размером 6 байт.
    Директивами df и dp можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 32 или менее бит (для i80386) или 16 или менее бит (для младших моделей микропроцессоров Intel);
    • адресное выражение, состоящее из 16-битового сегмента и 32-битового смещения;
    • константу со знаком из диапазона –247...247–1;
    • константу без знака из диапазона 0...248-1;
    • строку длиной до 6 байт, заключенную в кавычки.
  • dq — резервирование памяти для данных размером 8 байт.
    Директивой dq можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для МП i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для МП i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 32 или менее бит (для i80386) или 16 или менее бит (для младших моделей микропроцессоров Intel);
    • константу со знаком из диапазона –263...263–1;
    • константу без знака из диапазона 0...264–1;
    • строку длиной до 8 байт, заключенную в кавычки.
  • dt — резервирование памяти для данных размером 10 байт.
    Директивой dt можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для МП i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для МП i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 32 или менее бит (для i80386) или 16 или менее бит (для младших моделей);
    • адресное выражение, состоящее из 16-битового сегмента и 32-битового смещения;
    • константу со знаком из диапазона –279...279-1;
    • константу без знака из диапазона 0...280-1;
    • строку длиной до 10 байт, заключенную в кавычки;
    • упакованную десятичную константу в диапазоне 0...99 999 999 999 999 999 999.

Очень важно уяснить себе порядок размещения данных в памяти. Он напрямую связан с логикой работы микропроцессора с данными. Микропроцессоры Intel требуют следования данных в памяти по принципу: младший байт по младшему адресу.

А теперь примерчик для лучшего усвоения прочитанного:

А так же его листинг:

Ну и теперь медитация в дебагере...

Внимательно медитируем и прозреваем...
Еще раз видим, что данные располагаются в памяти в "обратной" последовательности, то есть принцип - младший байт, по младшему адресу.
В дампе памяти видим данные вашего сегмента в двух представлениях: шестнадцатеричном и символьном. Видно, что со смещением 0000 расположены символы, входящие в строку message. Она занимает 34 байта. После нее следует байт, имеющий в сегменте данных символическое имя perem_1, содержимое этого байта offh.
Теперь обратите внимание на то, как размещены в памяти байты, входящие в слово, обозначенное символическим именем perem_2. Сначала следует байт со значением 7fh, а затем со значением 3ah. Как видите, в памяти действительно сначала расположен младший байт значения, а затем старший. Та же история и с данными обозначенными символическим именем perem_3.
Оставшуюся часть сегмента данных вы можете теперь проанализировать самостоятельно.
Остановимся лишь на двух специфических особенностях использования директив резервирования и инициализации памяти. Речь идет о случае использования в поле операндов директив dw и dd символического имени из поля имя этой или другой директивы резервирования и инициализации памяти. В нашем примере сегмента данных это директивы с именами adr и adr_full.
Когда транслятор встречает директивы описания памяти с подобными операндами, то он формирует в памяти значения адресов тех переменных, чьи имена были указаны в качестве операндов. В зависимости от директивы, применяемой для получения такого адреса, формируется либо полный адрес (директива dd) в виде двух байтов сегментного адреса и двух байтов смещения, либо только смещение (директива dw).

Любой переменной, объявленной с помощью директив описания простых типов данных, ассемблер присваивает три атрибута:

  1. Сегмент (seg) — адрес начала сегмента, содержащего переменную;
  2. Смещение (offset) в байтах от начала сегмента с переменной;
  3. Тип (type) — определяет количество памяти, выделяемой переменной в соответствии с директивой объявления переменной.

воскресенье, 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 определяет элементы данных, имеющие длину в десять байт.
Назначение этой директивы связано с упакованным двоично-десятичным форматом данных.

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