Изучаем mips-ассемблер

Команды (инструкции)

Синтаксис команд ассемблера или инструкций ассемблера выглядит следующим образом:

<label>: <instruction operands>

Метка (label:) обязательно завершается двоеточием и может располагаться в отдельной строке. Метки используются для того, чтобы ссылаться на команды внутри программы.

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

В роли операндов команды могут выступать:

  • регистры, обращение к которым происходит по их именам;
  • константы;
  • адреса.

Беглый обзор.

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

Мы рассчитываем, что вы сами будете изучать выкладываемый код в свободное время. Пользуйтесь созданными для Вас возможностями: DOS-1.rar . TASMED насторен и готов к работе. При внесении изменения в код не забываем жать F2 для сохранения. Если при сборке кода возникает непонятная ошибка (!) — не забываем удалять ранее сгенерированные фалы (с расширениями *.OBJ, *.CODE). Они, как и проекты находятся в папке D:\WORK\Ваша_директория… и будут появляться вновь и вновь при удачной сборке программы. Удалять можно не выходя из DOS: выходим из TASMED (ALT+X), удаляем файлики (в NC — F8) и снова запускаем TASMED — откроется сразу с кодом.

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

директива аргументы описание
.align N выравнивание по 2^N. Например, .align 9 это выравнивание на 2^9 = 512 байт
.bss секция нулевых данных в ОЗУ
.data секция ОЗУ
.equ name, val присвоить макроконстанте name значение val. Например, .equ RLED, 5 заменит везде в тексте RLED на 5
.global name глобально видимое имя для стыковки с другими модулями
.macro / .endm name создание макроса по имени name
.section name войти в подсекцию name
.short N] объявить одну или несколько переменных размером 2 байта с заданными значениями
.text секция кода
.weak name “слабое” имя, которое может быть перекрыто другим
.word N] см. .short, только размер 4 байта
инструкция аргументы описание
add rd, r1, r2 rd = r1 + r2
addi rd, r1, N rd = r1 + N
and rd, r1, r2 rd = r1 & r2
andi rd, r1, N rd = r1 & N
beq r1, r2, addr if(r1==r2)goto addr
beqz r1, addr if(r1==0)goto addr
bgeu r1, r2, addr if(r1>=r2)goto addr
bgtu r1, r2, addr if(r1> r2)goto addr
bltu r1, r2, addr if(r1< r2)goto addr
bne r1, r2, addr if(r1!=r2)goto addr
bnez r1, addr if(r1!=0)goto addr
call func вызов функции func
csrr rd, csr rd = csr
csrrs rd, csr, N rd = csr; csr |= N, атомарно
csrs scr, rs csr |= rs
csrs scr, N csr |= N
csrw csr, rs csr = rs
ecall провоцирование исключения для входа в ловушку
j addr goto addr
la rd, addr rd = addr
lb rd, N(r1) считать 1 байт по адресу r1+N
lh rd, N(r1) считать 2 байта по адресу r1+N
li rd, N rd = N
lw rd, N(r1) считать 4 байта по адресу r1+N
mret возврат из обработчика исключения
mv rd, rs rd = rs
or rd, r1, r2 rd = r1 | r2
ori rd, r1, N rd = r1 | N
ret возврат из функции
sb rs, N(r1) записать 1 байт по адресу r1+N
sh rs, N(r1) записать 2 байта по адресу r1+N
slli rd, r1, N rd = r1 << N
srli rd, r1, N rd = r1 >> N
sw rs, N(r1) записать 4 байта по адресу r1+N
xor rd, r1, r2 rd = r1 ^ r2
xori rd, r1, N rd = r1 ^ N

Директивы данных

Языки высокого уровня (C++, Pascal) являются типизированными. То есть, в них используются данные, имеющие определенный тип, имеются функции их обработки и т. д. В языке программирования ассемблер подобного нет. Существует всего 5 директив для определения данных:

  1. DB — Byte: выделить 1 байт под переменную.
  2. DW — Word: выделить 2 байта.
  3. DD — Double word: выделить 4 байта.
  4. DQ — Quad word: выделить 8 байтов.
  5. DT — Ten bytes: выделить 10 байтов под переменную.

Буква D означает Define.

Любая директива может быть использована для объявления любых данных и массивов. Однако для строк рекомендуется использовать DB.

Синтаксис:

<name> DQ <operand>

В качестве операнда допустимо использовать числа, символы и знак вопрос — «?», обозначающий переменную без инициализации. Рассмотрим примеры:

real1 DD 12.34
char db ‘c’
ar2 db ‘123456’,0 ; массив из 7 байт
num1 db 11001001b ; двоичное число
num2 dw 7777o ; восьмеричное число
num3 dd -890d ; десятичное число
num4 dd 0beah ; шестнадцатеричное число
var1 dd ? ; переменная без начального значения
ar3 dd 50 dup (0) ; массив из 50 инициализированных эл-тов
ar4 dq 5 dup (0, 1, 1.25) ; массив из 15 эл-тов, инициализированный повторами 0, 1 и 1.25

Переход к оперативке

В прошлой главе я обмолвился о сегменте .rodata, еще раньше без объяснений ввел сегмент .text. Теперь введем еще два сегмента: .data и .bss. Они оба предназначены для хранения глобальных переменных, но первый инициализируется при включении заранее заданными данными, а второй — нет. Причем с .bss есть еще некая неопределенность: в некоторых источниках его инициализировать и не надо вообще, в других — надо обязательно, причем нулями. Хотя и не хочется заниматься бесполезным копированием нулей, для совместимости с Си сделать это придется.

Итак, берем предыдущий пример и вместо .text указываем .data, но не спешим прошивать контроллер. Для начала заглянем в дизассемблерный файл res/firmware.lss чтобы убедиться что массив начинается именно из начала оперативной памяти, 0x2000’0000:

Упс, что-то пошло не так. Очевидно, ассемблер не знает где у нашего контроллера начало оперативной памяти. Чтобы ему это указать, создадим файл lib/gd32vf103cbt6.ld, в котором пропишем следующее:

То есть сначала мы указываем начало определенной памяти и ее размер, а потом принадлежность секций к той или иной памяти. Теперь этот файл нужно подсунуть компилятору (точнее, линкеру) при помощи ключа -T:

Вот теперь данные попали именно туда, куда надо:

Но прошивать полученным кодом контроллер все еще рано, ведь мы знаем, что оперативная память тем и отличается от постоянной, что может не сохраняться при отключении питания. Это значит, что перед работой основного кода нам в эту память надо сначала скопировать данные. Для этого компилятор заботливо сохранил наши константы в безымянном сегменте сразу после .text, это можно увидеть если посмотреть непосредственно res/firmware.hex файл.

Для большего удобства доступа к этим данным добавим в .ld-файл немного магии

Теперь мы можем использовать область флеш-памяти начиная с _data_load чтобы инициализировать собственно оперативку. Ах да, раз уж у нас есть внешний файл с адресами памяти, вынесем туда же стек:

Вот теперь наконец наш массив будет корректно читаться из оперативной памяти.

При создании переменных в секции .bss было бы странно присваивать им какие-то значения (хотя никто не запрещает, просто использованы они не будут). Вместо этого можно использовать директиву-заполнитель .comm arr, 10 (для переменной arr размером 10 байт). Стоит отметить, что использовать ее можно в любой секции, причем резервировать данные она будет только в .bss. Ниже приведены еще примеры объявления переменных различных размеров:

Сегментные регистры

Сегменты — это специфические части программы, которые содержат данные, код и стек. Есть три основных сегмента:

   Сегмент кода (Code Segment или CS) — содержит все команды и инструкции, которые должны быть выполнены. 16-битный регистр сегмента кода или регистр CS хранит начальный адрес сегмента кода.

   Сегмент данных (Data Segment или DS) — содержит данные, константы и рабочие области. 16-битный регистр сегмента данных или регистр DS хранит начальный адрес сегмента данных.

   Сегмент стека (Stack Segment или SS) — содержит данные и возвращаемые адреса процедур или подпрограмм. Он представлен в виде структуры данных «Стек». Регистр сегмента стека или регистр SS хранит начальный адрес стека.

Кроме регистров CS, DS и SS существуют и другие регистры дополнительных сегментов — ES (Extra Segment), FS и GS, которые предоставляют дополнительные сегменты для хранения данных.

При написании программ на ассемблере, программе необходим доступ к ячейкам памяти. Все области памяти в сегменте относятся к начальному адресу сегмента. Сегмент начинается с адреса, равномерно делимого на десятичное 16 или на 10. Таким образом, крайняя правая шестнадцатеричная цифра во всех таких адресах памяти равна , которая обычно не хранится в сегментных регистрах.

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

Переход к оперативке

В прошлой главе я обмолвился о сегменте .rodata, еще раньше без объяснений ввел сегмент .text. Теперь введем еще два сегмента: .data и .bss. Они оба предназначены для хранения глобальных переменных, но первый инициализируется при включении заранее заданными данными, а второй — нет. Причем с .bss есть еще некая неопределенность: в некоторых источниках его инициализировать и не надо вообще, в других — надо обязательно, причем нулями. Хотя и не хочется заниматься бесполезным копированием нулей, для совместимости с Си сделать это придется.

Итак, берем предыдущий пример и вместо .text указываем .data, но не спешим прошивать контроллер. Для начала заглянем в дизассемблерный файл res/firmware.lss чтобы убедиться что массив начинается именно из начала оперативной памяти, 0x2000’0000:

Упс, что-то пошло не так. Очевидно, ассемблер не знает где у нашего контроллера начало оперативной памяти. Чтобы ему это указать, создадим файл lib/gd32vf103cbt6.ld, в котором пропишем следующее:

То есть сначала мы указываем начало определенной памяти и ее размер, а потом принадлежность секций к той или иной памяти. Теперь этот файл нужно подсунуть компилятору (точнее, линкеру) при помощи ключа -T:

Вот теперь данные попали именно туда, куда надо:

Но прошивать полученным кодом контроллер все еще рано, ведь мы знаем, что оперативная память тем и отличается от постоянной, что может не сохраняться при отключении питания. Это значит, что перед работой основного кода нам в эту память надо сначала скопировать данные. Для этого компилятор заботливо сохранил наши константы в безымянном сегменте сразу после .text, это можно увидеть если посмотреть непосредственно res/firmware.hex файл.

Для большего удобства доступа к этим данным добавим в .ld-файл немного магии

Теперь мы можем использовать область флеш-памяти начиная с _data_load чтобы инициализировать собственно оперативку. Ах да, раз уж у нас есть внешний файл с адресами памяти, вынесем туда же стек:

Вот теперь наконец наш массив будет корректно читаться из оперативной памяти.

При создании переменных в секции .bss было бы странно присваивать им какие-то значения (хотя никто не запрещает, просто использованы они не будут). Вместо этого можно использовать директиву-заполнитель .comm arr, 10 (для переменной arr размером 10 байт). Стоит отметить, что использовать ее можно в любой секции, причем резервировать данные она будет только в .bss. Ниже приведены еще примеры объявления переменных различных размеров:

Реализация вызовов функции в ассемблере.

Кроме параметров имеет значение алгоритм работы функции, так называемое «соглашение о вызове функции». Оно связано с построением работы в различных операционных системах различных высокоуровневых языков программирования.

В настоящее время существуют целый ряд принятых соглашений (конвенций) о вызове функций (подпрограмм): cdecl (язык Си, С++), pascal (язык Pascal), stdcall (WinApi), fastcall, safecall, thiscall. Все они начинали использоваться в разное время и с разными целями, решая определённые задачи, наиболее приемлемым способом.

Необходимо помнить, что для функции в ассемблере нет определённых договорённостей — вызывать можно так, как удобнее и оптимальнее. Мы рассмотрим наиболее используемые конвенции высокоуровневых языков программирования и их реализации на ассемблере.

Изучить примеры работы компиляторов языков высокого уровня крайне полезно, если вас интересует кодокопание в любом виде (крэкинг, реверс-программирование и др.) .

.section name

Use the directive to assemble the following code into a section
named name.

This directive is only supported for targets that actually support arbitrarily
named sections; on targets, for example, it is not accepted, even
with a standard section name.

For COFF targets, the directive is used in one of the following
ways:

.section name[, "flags"]
.section name[, subsegment]

If the optional argument is quoted, it is taken as flags to use for the
section. Each flag is a single character. The following flags are recognized:

bss section (uninitialized data)
section is not loaded
writable section
data section
read-only section
executable section

If no flags are specified, the default flags depend upon the section name. If
the section name is not recognized, the default will be for the section to be
loaded and writable.

If the optional argument to the directive is not quoted, it is
taken as a subsegment number (see section ).

For ELF targets, the directive is used like this:

.section name[, "flags"[, @type]]

The optional flags argument is a quoted string which may contain any
combintion of the following characters:

section is allocatable
section is writable
section is executable

The optional type argument may contain one of the following constants:

section contains data
section does not contain data (i.e., section only occupies space)

If no flags are specified, the default flags depend upon the section name. If
the section name is not recognized, the default will be for the section to have
none of the above flags: it will not be allocated in memory, nor writable, nor
executable. The section will contain data.

For ELF targets, the assembler supports another type of
directive for compatibility with the Solaris assembler:

.section "name"[, flags...]

Note that the section name is quoted. There may be a sequence of comma
separated flags:

section is allocatable
section is writable
section is executable

.comm symbol , length

declares a common symbol named symbol. When linking, a
common symbol in one object file may be merged with a defined or common symbol
of the same name in another object file. If does not see a
definition for the symbol—just one or more common symbols—then it will
allocate length bytes of uninitialized memory. length must be an
absolute expression. If sees multiple common symbols with
the same name, and they do not all have the same size, it will allocate space
using the largest size.

When using ELF, the directive takes an optional third argument.
This is the desired alignment of the symbol, specified as a byte boundary (for
example, an alignment of 16 means that the least significant 4 bits of the
address should be zero). The alignment must be an absolute expression, and it
must be a power of two. If allocates uninitialized memory
for the common symbol, it will use the alignment when placing the symbol. If
no alignment is specified, will set the alignment to the
largest power of two less than or equal to the size of the symbol, up to a
maximum of 16.

The syntax for differs slightly on the HPPA. The syntax is
`symbol .comm, length; symbol is optional.

Регистры и функции

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

псевдоним имя назначение сохранение
zero x0 Вечный и неизменный ноль n/a
ra x1 Адрес возврата нет
sp x2 Stack pointer, указатель стека да
gp, tp x3, x4 Регистры для нужд компилятора. Лучше их вообще не использовать n/a
t0-t6 x5-x7, x28-x31 Временные регистры нет
s0-s11 x8, x9, x18-x27 Рабочие регистры да
a0-a7 x10-x17 Аргументы функции нет
a0, a1 x10, x11 Возвращаемое значение функции нет

Регистр zero предназначен для получения нуля, либо для сбрасывания в него результата вычисления, которое нам не нужно. Аналог /dev/zero и /dev/null в одном лице
О регистрах ra и sp поговорим чуть-чуть позже.

Глубокий смысл регистров gp и tp я так и не понял. Вроде бы используются компиляторами или даже транслятором ассемблера для оптимизаций, плюс для многопоточных задач и разделения прав доступа. В общем, лучше их не трогать.

Временные регистры t0 — t6 предназначены для хранения промежуточных результатов вычислений и не обязаны сохраняться при вызове функций. Это сделано для того чтобы простые функции не заморачивались сохранением всех регистров на стеке, а потом еще и восстановлением.

Рабочие регистры s0 — s11 напротив сохраняются при вызове функций. Они нужны для обратной задачи — воспользоваться ранее вычисленным значением и как-то объединить его с результатом функции и при этом опять же обойтись без лишнего использования стека.
Регистры обмена a0 — a7 используются для передачи параметров в функцию и обратно.
Очевидно, функция их должна менять, так что после вызова функции их значения не сохраняются. Интересно, что для возвращаемого значения используются только a0 и a1, а портить функции разрешено все.

О соглашениях использования регистров поговорили, пора функцию написать, а потом и вызвать. Примером функции будет задержка в 200`000 циклов, которая пока что вписана прямо в основной цикл программы. Давайте ее оформим как функцию. Принимать она должна время (в циклах) и ничего не возвращать. Отлично, значит аргумент будет храниться в a0. Помимо него можно как угодно портить a1 — a7 а также t0 — t6, но нам это пока без надобности. А вот остальные регистры портить нельзя, не забываем об этом.

Главное особенностью функций является то, что их код находится только в одном месте, но может вызываться из разных. Как же нам узнать в какую именно из точек вызова вернуться? Для этого соглашением предусмотрен специальный регистр ra, в который при выполнении соответствующей инструкции (jal, jalr или псевдоинструкции call, которая разворачивается в одну из предыдущих) происходит сохранение текущего адреса выполнения, после чего выполнение переходит на функцию. Соответственно, когда функция завершается, ей достаточно перейти по адресу, хранящемуся в ra при помощи инструкции jr ra или обертки ret. Так и запишем, не забыв заменить в теле функции регистр a5 на a0:

Конвенция вызова функций PASCAL (Pascal, Basic, Fortran и др.).

Параметры загоняются в стек слева направо — сверху вниз, стек очищается вызываемой функцией:

Функция (процедура) содержит пять параметров:myFunc (a,b,c,d,e)

;Ассемблерный код:
push a; Первый параметр (самый левый) — сверху
push b; Второй параметр
push c;
push d;
push e; Пятый параметр — снизу
call myFunc

;Функция содержит пять параметров:
myFunc:
push bp
mov bp,sp; Создаём стековый кадр. В bp — указатель на стековый кадр, регистр bp использовать нельзя!
a equ ; Первый параметр — сверху ()
b equ
c equ
d equ
e equ ; Пятый параметр

;команды, которые могут использовать стек:
mov ax,e ; Cчитать параметр 5 — . Можно и так, но это менее понятно: mov ax,
; Его адрес в сегменте стека ВР + 4, потому что при выполнении
; команды CALL при вызове функции, в стек поместили адрес возврата — 2 байта для процедуры
; типа NEAR (или 4 — для FAR), а потом еще и ВР — 2 байта (push bp — в начале нашей функции)
mov bx,с ; считать параметр 3 — . Можно и так, но это менее понятно: mov bx,
; … ещё команды

pop bp
ret 10 ; Из стека дополнительно извлекается 10 байт — стек освобождает вызываемая функция

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

;Ассемблерный код:

pusha; Первый параметр (самый левый) — сверху

pushb; Второй параметр

pushc;

pushd;

pushe; Пятый параметр — снизу

callmyFunc

 
;Функция содержит пять параметров:

myFunc

pushbp

movbp,sp; Создаём стековый кадр. В bp — указатель на стековый кадр, регистр bp использовать нельзя!

aequbp+12; Первый параметр — сверху ()

bequbp+10

cequbp+8

dequbp+6

eequbp+4; Пятый параметр

;команды, которые могут использовать стек:

movax,e; Cчитать параметр 5 — . Можно и так, но это менее понятно: mov ax,

; Его адрес в сегменте стека ВР + 4, потому что при выполнении
; команды CALL при вызове функции, в стек поместили адрес возврата — 2 байта для процедуры
; типа NEAR (или 4 — для FAR), а потом еще и ВР — 2 байта (push bp — в начале нашей функции)

movbx,с; считать параметр 3 — . Можно и так, но это менее понятно: mov bx,

; … ещё команды

popbp

ret10; Из стека дополнительно извлекается 10 байт — стек освобождает вызываемая функция

Описание внешних ссылок

Как было заявлено в п. 4.1, мы будем использовать алгоритмические языки Pascal и C/C++ в качестве помощников при изучении Ассемблера. Таким образом, сразу начинаем работать с РАЗНЫМИ модулями, да еще и на разных языках! Поэтому нам не миновать ВНЕШНИХ ссылок. Что это такое? Это — использование в одном модуле имен, описанных в других модулях.

Директива описания общих имен PUBLIC

PUBLIC       имя1 имя2…] 

Эта директива указывает компилятору и компоновщику, что данное имя (его адрес) должно быть доступно для других программ. Имена могут быть метками, переменными или именами подпрограмм.

Например, если мы хотим использовать в языке С/С++ функцию с именем Prim, реализованную в Ассемблере, то она в ассемблерном модуле должна быть описана следующим образом:

	Public	C  Prim
Prim	Proc
              ………………………………………
Prim	EndP

Обратите внимание

Язык С/С++ различает регистр букв в именах. Это же должен делать и Ассемблер, для чего служит специальный ключ, определяющий чувствительность Ассемблера к РЕГИСТРУ выбора символов: ml=all (все символы), mx=globals (только глобальные), mu=none (символы к регистру НЕ чувствительны — принято по умолчанию):

TASM имя.asm[ /ml]
TASM имя.asm[ /mx]
TASM имя.asm[ /mu]    

В нашем случае ассемблерный модуль должен быть откомпилирован ОБЯЗАТЕЛЬНО с ключом /ml или /mx. Иначе компоновщик С/С++ НЕ сможет подключить ассемблерный модуль. Если эти рассуждения вам пока НЕПОНЯТНЫ, НЕ зацикливайтесь, — мы к этому еще вернемся на КОНКРЕТНЫХ примерах.

Директива описания внешних имен EXTRN

EXTRN      имя1:тип1 

Эта директива указывает компилятору и компоновщику, что данное имя имеет определенный тип, его предполагается использовать в данном ассемблерном модуле, но память для него выделена в другом модуле. Параметр тип может принимать следующие значения: ABS (для констант), BYTE, WORD, DWORD, QWORD, TBYTE (см. соответствующий столбец в табл. 2.2), FAR, NEAR.

Например, в модуле на алгоритмическом языке Pascal (Borland/Turbo Pascal-5.5/6.0/7.0х) мы описали следующие глобальные переменные:

Var a, b, c : Integer;
       X      : LongInt;

А использовать их собираемся в ассемблерном модуле. В этом случае директива будет иметь вид:

Extrn   a:Word, b:Word, c:Word, x:Dword

Обратите внимание

Язык Pascal НЕ различает регистр букв в именах. А директива EXTRN содержит только ОДНУ гласную букву — начинающие программисты часто на этом спотыкаются… Будьте ВНИМАТЕЛЬНЫ!

Разница между директивами и командами Ассемблера

Итак, из п. 2.4.1 мы уже знаем, что в языке Ассемблера существуют команды и директивы, формат которых практически одинаков — см. рис. 2.12. В командах Имя интерпретируется как метка, поэтому за ней всегда ставится символ двоеточия ». Для Ассемблера в качестве имени (идентификатора) допускаются следующие символы:

  • Латинские буквы от A(a) до Z(z) — РЕГИСТР БЕЗРАЗЛИЧЕН. Ассемблер сам по себе НЕ различает ПРОПИСНЫЕ и строчные буквы. Но при использовании ассемблерных модулей в программе на алгоритмическом языке, чувствительном к регистру букв, например, в С/С++, буквы в разных регистрах будут разными.
  • Цифры от 0 до 9.
  • Специальные символы: ? . @ _ $.

Имя в Ассемблере может начинаться с любого допустимого символа, кроме цифры. Если имя содержит символ точки ‘.’, то он должен быть ПЕРВЫМ символом. Имя НЕ может быть зарезервированным в Ассемблере словом (имя машинной команды или директивы).

С некоторыми директивами мы с вами уже успели и поработать — см. примеры 2.9 и 2.10.

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

Команда Ассемблера всегда генерирует машинный код.

Директивы имеют разный синтаксис в режимах MASM (поддерживается компиляторами Microsoft Assembler — masm и Borland (Turbo) Assembler — tasm) и Ideal (поддерживается компилятором tasm) — см. приложение 5.

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

  • Терминальные элементы языка (название директив и команд, разделительные знаки) будем выделять жирным шрифтом.
  • Нетерминальные элементы (название параметров, операндов и других атрибутов) выделим курсивом.
  • Комментарии будем писать обычным шрифтом.
  • Необязательные элементы заключаются в квадратные скобки []. В синтаксисе языка Ассемблера есть и терминальный элемент квадратные скобки [] — в этом случае это будет особо ПОДЧЕРКНУТО.
  • Повторяющиеся элементы синтаксической структуры описываются многоточием (…).

Директив достаточно много. Практически каждая версия компилятора что-то из директив добавляет или немного изменяет синтаксис. Для определенности мы в данной главе остановимся на синтаксисе основных директив в более универсальном формате MASM для компиляторов masm-6.12 (или выше) и tasm-3.1 (или выше). Кроме того, известные компиляторы с языка программирования С/С++ (Borland C++, Visual C++) выдают ассемблерный листинг именно в формате MASM. И очень скоро мы научимся его читать…