Программирование на машинном языке затруднительно для программиста. Например, для сложения содержимого двух длинных слов с именами TAX и TOTAL программисту следовало бы написать в машинном коде такую инструкцию, как
Операнд | Операнд | Код операции |
---|---|---|
E4 AF |
F2 AF |
С0 |
При этом программист должен помнить, что ^XC0 - это код операции инструкции ADDL2, a ^XAF - спецификатор операнда, определяющий режим относительной адресации с заданным в формате байта смещением. Кроме того, чтобы получить доступ к длинным словам TAX и TOTAL, программист должен вычислить их смещение. В данном примере смещение для длинного слова TAX равно ^XF2, а для длинного слова TOTAL - ^XE4. Чтобы оценить ситуацию, с которой сталкивается программист при кодировании, стоит отметить, что ЭВМ семейства VAX имеют несколько сотен различных кодов операций; к тому же обычно в программе используются несколько тысяч ячеек памяти.
Языки ассемблера намного снижают умственную нагрузку программиста, позволяя использовать вместо чисел символические имена. Например, предыдущую инструкцию на машинном языке можно написать на языке ассемблера следующим образом:
ADDL2 TAX,TOTAL
Программа, называемая ассемблером, выполняет трансляцию программы на языке ассемблера в программу на машинном языке, заменяя символические имена и специальные символы соответствующими числами. Для приведённого выше оператора языка ассемблера. Ассемблер произведёт замену мнемоники ADDL2 на код операции ^XC0, символического имени TAX на коды ^XAF и ^XF2, имени TOTAL на коды ^XAF и ^XE4. Обратите внимание, что операторы языка ассемблера читаются обычным образом - слева направо. Рассматриваемый оператор языка ассемблера следует читать как "Сложить содержимое длинного слова, начинающегося с адреса TAX, с содержимым длинного слова, начинающегося с адреса TOTAL, а результат поместить в длинное слово, начинающееся с адреса TOTAL".
Помимо того, что ассемблер позволяет программисту использовать символические имена, он может также выполнять некоторые вспомогательные вычисления, такие как вычисление смещений и преобразование чисел из одной системы счисления в другую.
Обычно для каждого типа (или модели) ЭВМ имеется свой собственный язык ассемблера. Но бывает и так, что иногда для ЭВМ одного типа (или модели) существуют различные языки ассемблера. Фактически для ЭВМ семейства VAX имеются по крайней мере два различных ассемблера. Одним из них является программа-ассемблер, выполняемая под управлением операционной системы UNIX. Вторым является ассемблер, называемый VAX-11 MACRO или просто MACRO - макроассемблер, поставляемый в составе ОС VMS производителем ЭВМ семейства VAX - фирмой Digital Equipment Corporation. В данной книге будет рассматриваться только ассемблер VAX-11 MACRO.
На рис. 4.1 показана программа на языке ассемблера, в которой выполняется сложение чисел 26, 38 и 5, представленных в формате байта, а полученная сумма (число 69) помещается в ячейку памяти SUM. Как показано на рисунке, операторы языка ассемблера состоят из четырёх полей. Первое поле называется полем метки и используется для определения символических адресов, таких как A, B, C, SUM и START. Вторым полем является поле кода операции, в нем может находиться мнемоническое обозначение кода операции, например MOVB, ADDB2 и RET. Кроме того, в этом поле могут помещаться директивы ассемблера, например .BYTE, .BLKB, .WORD и .END. Директивы дают указание ассемблеру выполнить некоторые специальные действия. Третье поле, называемое полем операндов, может использоваться для размещения в нём операндов инструкции. Например, в инструкции
ADDB2 B,SUM ; SUM := SUM+B
операнды B и SUM указывают, что байт, расположенный по адресу B, должен быть сложен с байтом, расположенным по адресу SUM. Последнее поле - поле комментариев, используется для документирования программы.
Метка | Код операции | Операнды | Примечания |
---|---|---|---|
A: |
.BYTE |
26 |
; БАЙТ ДАННЫХ |
B: |
.BYTE |
38 |
; БАЙТ ДАННЫХ |
C: |
.BYTE |
5 |
; БАЙТ ДАННЫХ |
SUM: |
.BLKB |
1 |
; РЕЗЕРВИРОВАНИЕ БАЙТА |
START: |
.WORD |
0 |
; В СЛОВЕ СОДЕРЖИТСЯ 0 |
MOVB |
A,SUM |
; SUM := А |
|
ADDB2 |
B,SUM |
; SUM := SUM+B |
|
ADDB2 |
C,SUM |
; SUM := SUM+C |
|
RET |
; ВОЗВРАТ |
||
.END |
START |
Рис. 4.1. Программа на языке ассемблера
Машинный код | Язык ассемблера | |||||
---|---|---|---|---|---|---|
Содержимое | Адрес | Стр. | Метка | Код операции |
Операнды | Примечания |
1А |
0000 |
1 |
A: |
.BYTE |
26 |
; БАЙТ ДАННЫХ |
26 |
0001 |
2 |
B: |
.BYTE |
38 |
; БАЙТ ДАННЫХ |
05 |
0002 |
3 |
C: |
.BYTE |
5 |
; БАЙТ ДАННЫХ |
00000004 |
0003 |
4 |
SUM: |
.BLKB |
1 |
; РЕЗЕРВИРОВАНИЕ БАЙТА |
0000 |
0004 |
5 |
START: |
.WORD |
0 |
; В СЛОВЕ СОДЕРЖИТСЯ 0 |
F8 AF F7 AF 90 |
0006 |
6 |
MOVB |
A,SUM |
; SUM := А |
|
F3 AF F3 AF 80 |
000В |
7 |
ADDB2 |
B,SUM |
; SUM := SUM+B |
|
ЕЕ AF EF AF 80 |
0010 |
8 |
ADDB2 |
C,SUM |
; SUM := SUM+C |
|
04 |
0015 |
9 |
RET |
; ВОЗВРАТ |
||
0016 |
10 |
.END |
START |
Рис. 4.2. Листинг программы на языке ассемблера
При ассемблировании ассемблер транслирует программу на языке ассемблера в программу на машинном языке. На рис. 4.2 показан фрагмент листинга программы на языке ассемблера.
Как видно из рисунка, исходный текст программы на языке ассемблера располагается на правой стороне листинга. Результат трансляции программы на машинный язык располагается слева. В центральной части рисунка показано, что ассемблер присваивает каждой строке программы номер, и это помогает при разборе сообщений об ошибках.
Ниже приведена строка с номером 6 листинга программы
Машинный код | Язык ассемблера | |||||
---|---|---|---|---|---|---|
Содержимое | Адрес | Стр. | Метка | Код операции |
Операнды | Примечания |
F8 AF F7 AF 90 |
0006 |
6 |
MOVB |
A,SUM |
; SUM := А |
Ассемблер странслировал мнемоническое обозначение кода операции MOVB в код операции ^X90 и поместил его по адресу ^X00000006, на листинге этот адрес приводится в краткой записи 0006. Адреса, приводимые в тексте книги, могут сокращаться подобным образом тогда, когда ясно, что имеется в виду. Для адресации операнда A ассемблером был выбран режим относительной адресации с заданным в формате байта смещением (спецификатор операнда ^XAF) и произведено вычисление значения этого смещения (^XF7). Для адресации операнда SUM ассемблером был использован также режим относительной адресации, смещение данного операнда равно ^XF8. Строки программы с номерами 7-9 обрабатываются аналогично, начиная со следующего доступного адреса 000В.
На листинге ассемблера строка с номером 1 имеет следующий вид:
Машинный код | Язык ассемблера | |||||
---|---|---|---|---|---|---|
Содержимое | Адрес | Стр. | Метка | Код операции |
Операнды | Примечания |
1А |
0000 |
1 |
A: |
.BYTE |
26 |
; БАЙТ ДАННЫХ |
Директива ассемблера .BYTE применяется для резервирования памяти и задания начального значения данных, представляемых в формате байта. Если не указано противное, то ассемблер назначает адреса, начиная с адреса ^X0000. В результате метка A в рассматриваемом операторе является символическим указанием адреса ^X0000 и поэтому называется символическим адресом. По умолчанию ассемблер считает, что следующее за директивой .BYTE значение представляет собой десятичное число. Следовательно, число 26, рассматриваемое как десятичное, преобразуется ассемблером в шестнадцатеричное число ^X1A и помещается по адресу ^X0000. Строки программы с номерами 2 и 3 обрабатываются аналогично.
В четвёртой строке листинга директива .BLKB (BLock Byte - блок байтов) применяется для резервирования байта памяти без задания начального значения. Значение, следующее за директивой .BLKB, определяет число резервируемых байтов памяти. (Резервирование пространства памяти для массива из 20 байтов могло быть осуществлено с помощью директивы ассемблера .BLKB 20). В данном случае число байтов равно 1, таким образом, SUM - это просто символическое имя байта памяти, содержимое которого не определено перед началом выполнения программы. Число 00000004, расположенное в графе "Содержимое" раздела листинга "Машинный код", показывает, что следующим адресом, который может использовать ассемблер, является адрес ^X00000004. В действительности эта информация не включается в программу на машинном языке, а используется другими системными программами в процессе подготовки данной программы для выполнения. Программисту следует учитывать, что перед началом выполнения программы содержимое области памяти с адресом SUM не определено.
Директива .WORD в пятой строке программы подобна директиве .BYTE, отличаясь только тем, что по директиве .WORD резервируется память и задаётся начальное значение для данных в формате 16-битового слова. Символический адрес START является адресом точки входа в программу[1]. Операционная система VAX/VMS обращается с программами пользователей как с подпрограммами или процедурами, передавая им управление с помощью инструкций CALLS или CALLG, описанных в гл. 9. Эти инструкции требуют задания 16-битовой маски, которая обычно для основных программ равна 0. Её назначение поясняется в гл. 9. Первая выполняемая инструкция программы следует непосредственно за маской.
Директива .END в строке с номером 10 помечает физический конец программы. Символическое имя START в директиве .END определяет имя START как адрес точки входа в программу.
Процесс трансляции программы, написанной на языке ассемблера, в программу на машинном языке называется ассемблированием и в основном заключается в замене имён числами. В процессе замены интенсивно используются две таблицы: таблица постоянных имён и таблица имён, определяемых пользователем. Таблица постоянных имён содержит значения для неизменяемых символических имён, таких как мнемонический код операций и директивы ассемблера. Приведём небольшую часть таблицы постоянных имён:
Постоянное имя |
Шестнадцатеричное значение |
---|---|
ADDB2 |
80 |
ADDB3 |
81 |
ADDL2 |
С0 |
ADDL3 |
С1 |
ADDW2 |
А0 |
ADDW3 |
А1 |
Таблица имён, определяемых пользователем, содержит значения для заданных пользователем символических имён, таких как метки. В качестве примера приведём таблицу имён, определяемых пользователем, для программы рис. 4.2:
Имя, определённое пользователем |
Шестнадцатеричное значение |
---|---|
A |
00000000 |
B |
00000001 |
C |
00000002 |
START |
00000004 |
SUM |
00000003 |
В этой таблице каждому символическому имени ставится в соответствие числовое значение. В частности, A - символическое имя для адреса ^X0000, B - символическое имя для адреса ^X0001 и т.д. В листинге ассемблера таблица имён даётся в алфавитном порядке, как и в нашем примере.
Если таблица постоянных имён и таблица имён пользователя заданы, то процесс ассемблирования происходит следующим образом. Ассемблер просматривает (сканирует) программу строка за строкой. Когда будет обнаружено символическое имя в поле кода операции, производится поиск этого имени в таблице постоянных имён и устанавливается, является это имя кодом операции или директивой ассемблера. Директивы ассемблера вызывают выполнение соответствующих подпрограмм в программе-ассемблере.
Коды операций обрабатываются следующим образом. В таблице постоянных имён каждому символическому коду ставится в соответствие числовой код, а также информация о количестве и типе операндов. Ассемблер помещает числовой код операции в программу в машинном коде, а затем обрабатывает операнды. Он размещает соответствующую информацию в программе, в машинном коде, используя несколько простых правил. При этом, в частности, формируются спецификаторы операндов и определяется значение смещения. В таблице имён, определяемых пользователем, производится поиск в тех случаях, когда необходимо определить значения каких-либо символических имён, заданных в поле операндов[2].
В предыдущем разделе предполагалось, что таблицы постоянных имён и имён, определяемых пользователем, уже имеются. Таблица постоянных имён не меняется от программы к программе, поэтому она "встроена" в ассемблер.
И наоборот, таблица имён, определяемых пользователем, специфична для каждой исходной программы, и поэтому ассемблер создаёт её для каждой программы заново. Ассемблер MACRO создаёт таблицу исходя из предположения, что числа должны размещаться в смежных байтах памяти. Используя довольно простые правила, ассемблер вычисляет число байтов, порождаемых при ассемблировании каждой строки программы. При условии, что первый сгенерированный байт должен размещаться по адресу ^X00000000, ассемблер может определить точный адрес каждой инструкции и любых данных в программе. Поскольку некоторые строки программы содержат метки, меткам ставятся в соответствие определённые адреса и таким образом формируется таблица имён, определяемых пользователем.
Более строго: ассемблер создаёт таблицу имён, определяемых пользователем, отслеживая единственное значение, а именно - адрес следующего доступного байта памяти. Ассемблер запоминает текущее значение этого адреса в длинном слове, которое называется счётчиком адресов. (Опытные программисты пишут программы по частям. Для обеспечения такого стиля работы ассемблер VAX позволяет использовать несколько счётчиков адресов, по одному для каждой программной секции.)
В счётчике адресов обычно устанавливается начальное значение ^X00000000. Ассемблер просматривает исходную программу на языке ассемблера с начала до конца, соблюдая следующие правила.
Правило 1. Если ассемблер встречает символическое имя, после которого следует двоеточие (например, START:, A: или SUM:), он считает его символическим адресом. Символическое имя вместе с текущим значением счётчика адресов заносится в таблицу имён, определяемых пользователем. Содержимое счётчика адресов не меняется.
Правило 2. Когда ассемблер обнаруживает символическое имя в поле кода операции, то прибавляет некоторое соответствующее значение к содержимому счётчика адресов. Это значение представляет собой просто количество байтов машинного кода, которое будет сгенерировано при обработке этой инструкции, включая байты, занимаемые операндами. Например,
Код операции |
Операнды | Соответствующее число байт |
---|---|---|
.BYTE |
57 |
1 |
.WORD |
0 |
2 |
.BLKB |
1 |
1 |
.END |
START |
0 |
MOVB |
A,SUM |
ПЕРЕМЕННОЕ ДЛЯ ИНСТРУКЦИИ |
ADDB2 |
B,SUM |
ПЕРЕМЕННОЕ ДЛЯ ИНСТРУКЦИИ |
RET |
1 |
Объём памяти, занимаемый инструкциями MOVB и ADDB2, зависит от режима адресации и правил, используемых ассемблером для подбора спецификатора операнда. На рис. 4.2 для каждой из инструкций MOVB и ADDB2 требуется по 5 байтов. Применяя эти правила к программе на рис. 4.2, получаем приведённую выше таблицу имён.
Нетрудно заметить, что счётчик адресов при ассемблировании играет ту же роль, что и программный счётчик во время выполнения программы. Однако есть существенные различия между этими двумя счётчиками. Счётчик адресов - это ячейка памяти (длинное слово), помещённая внутри программы, называемой ассемблером. Он определяет место в памяти, куда будет занесён следующий байт программы, сгенерированный в процессе ассемблирования. Программный счётчик - это особый регистр (т.е. специальная часть аппаратуры) процессора ЭВМ VAX, Он определяет адрес начала следующей подлежащей выполнению инструкции в машинном коде.
На рис. 4.3 дан модифицированный вариант программы, приведённой на рис. 4.1. Он отличается от предыдущего только тем, что директивы резервирования памяти для данных A, B, C и SUM перенесены в конец программы.
Посмотрим, что произойдёт при ассемблировании этой программы. Вторая строка программы содержит оператор MOVB A,SUM. Пытаясь сгенерировать для этой строки машинный код, ассемблер может подставить числовой код операции ^X90 вместо мнемонического кода MOVB. Однако он не может вычислить значения спецификаторов операндов и смещения для операндов A и SUM, они являются символическими адресами и будут внесены в таблицу имён только при обработке шестой и седьмой строк этой программы.
Метка | Код операции | Операнды | Примечания |
---|---|---|---|
START: |
.WORD |
0 |
; В СЛОВЕ СОДЕРЖИТСЯ 0 |
MOVB |
A,SUM |
; SUM := А |
|
ADDB2 |
B,SUM |
; SUM := SUM+B |
|
ADDB2 |
C,SUM |
; SUM := SUM+C |
|
RET |
; ВОЗВРАТ |
||
A: |
.BYTE |
26 |
; БАЙТ ДАННЫХ |
B: |
.BYTE |
38 |
; БАЙТ ДАННЫХ |
C: |
.BYTE |
5 |
; БАЙТ ДАННЫХ |
SUM: |
.BLKB |
1 |
; РЕЗЕРВИРОВАНИЕ БАЙТА |
.END |
START |
Рис. 4.3. Модифицированная программа на языке ассемблера
Чтобы решить проблему, ассемблер делает два прохода по программе, т.е. просматривает программу на языке ассемблера дважды. Сначала не генерируется никакого машинного кода, поскольку адреса ещё не определены. После первого просмотра программы адреса уже можно определить и ассемблер может сформировать таблицу имён, определяемых пользователем. Затем, при втором проходе по программе, генерируется программа в машинном коде.
Процесс формирования таблицы имён, определяемых пользователем, при сканировании программы на языке ассемблера называется первым проходом процесса ассемблирования. Программа в машинном коде создаётся во время второго прохода. При этом ассемблер просматривает программу на языке ассемблера второй раз и, пользуясь уже сформированными при первом проходе таблицами постоянных имён и имён, определяемых пользователя, подставляет вместо символических имён числа для формирования программы в машинном коде.
Но и при двух проходах остаётся проблема вычисления смещения. Например, инструкция MOVB A,SUM во второй строке данной программы может занимать от 5 до 11 байтов в зависимости от того, используется ли для адресации операндов A и SUM относительная адресация со смещением, заданным в формате байта, слова или длинного слова.
При первом проходе ассемблер должен выбрать длину инструкции, чтобы поставить в соответствие символическим адресам правильные числовые значения. Если именам A и SUM выше уже присвоены значения, то ассемблер может легко выбрать один из форматов смещения - байт, слово, длинное слово для относительной адресации A и SUM. В данной программе, однако, имена A и SUM определяются ниже и при первом проходе ассемблер не может определить, насколько далеко от инструкции MOVB будут размещаться байты с именами A и SUM.
Чтобы разрешить эту проблему, ассемблер выбирает относительную адресацию со смещением, заданным в формате длинного слова, во всех случаях, когда на первом проходе обнаруживается неопределённое символическое имя[3], Как показано на рис. 4.4, это существенно увеличивает длину программы. (Программа рис. 4.2 занимает ^X16, или 22 байта, а функционально эквивалентная программа на рис. 4.4 занимает ^X28, или 40 байтов.)
Машинный код | Язык ассемблера | ||||||
---|---|---|---|---|---|---|---|
Содержимое | Адрес | Стр. | Метка | Код операции |
Операнды | Примечания | |
0000 0000 |
1 |
START: |
.WORD |
0 |
; В СЛОВЕ СОДЕРЖИТСЯ 0 |
||
00000027'EF |
00000024'EF |
90 0002 |
2 |
MOVB |
A,SUM |
; SUM := А |
|
00000027'EF |
00000024'EF |
80 000D |
3 |
ADDB2 |
B,SUM |
; SUM := SUM+B |
|
00000027'EF |
00000024'EF |
80 0018 |
4 |
ADDB2 |
C,SUM |
; SUM := SUM+C |
|
04 0023 |
5 |
RET |
; ВОЗВРАТ |
||||
1А 0024 |
6 |
A: |
.BYTE |
26 |
; БАЙТ ДАННЫХ |
||
26 0025 |
7 |
B: |
.BYTE |
38 |
; БАЙТ ДАННЫХ |
||
05 0026 |
8 |
C: |
.BYTE |
5 |
; БАЙТ ДАННЫХ |
||
00000028 0027 |
9 |
SUM: |
.BLKB |
1 |
; РЕЗЕРВИРОВАНИЕ БАЙТА |
||
0016 |
10 |
.END |
START |
Рис. 4.4. Листинг модифицированной программы на языке ассемблера
Чтобы избежать таких потерь, программисты, пишущие на языке ассемблера VAX, обычно размещают область данных в начале программы (см. рис. 4.1), если намереваются использовать относительную адресацию. Не следует создавать программы со структурой, аналогичной структуре программы рис. 4.3.
Если программу на языке ассемблера рис. 4.1 сассемблировать, скомпоновать и выполнить, она вычислит сумму 26, 38 и 5 и в результате будет получено 69. Однако в этой программе есть недостатки, которые считаются следствием плохого стиля программирования. Улучшенный вариант этой программы показан на рис. 4.5. Первое отличие от программы рис. 4.1 заключается в использовании директивы .TITLE в начале программы. Как отмечалось в гл. 1, эта директива не требуется для выполнения, но настоятельно рекомендуется для документирования программы. Символическое имя, следующее за директивой .TITLE, в данном случае IMPROVED, рассматривается как имя программы. Остальной текст в строке (УСОВЕРШЕНСТВОВАННАЯ ПРОГРАММА НА АССЕМБЛЕРЕ) комментарий. Строки, начинающиеся с точки с запятой обрабатываются как комментарии, что показано на примере строк 2-6. Пустые строки комментариев, например строчка 5, полезно вводить в программу для деления её на разделы, что облегчает чтение программы.
Указание директивы .ENTRY в строке 13 программы - это предпочтительный способ определения точки входа в программу. Оператор .ENTRY START,0 на рис. 4.5 заменяет оператор START: .WORD 0 на рис. 4.1. Первое символическое имя (START), следующее за директивой .ENTRY, определяет адрес точки входа в программу. По этой директиве адрес START не только заносится в таблицу имён, но и помечается как некоторый специальный адрес (в данном случае - адрес точки входа, который операционная система VAX/VMS использует для передачи управления программе). Кроме того, по директиве .ENTRY генерируется 16-битовая маска, которая используется, как уже описывалось инструкциями CALLS и CALLG. Число 0, следующее за адресом START, определяет, что значение этой маски равно 0, т.е. равно значению, задаваемому ранее директивой .WORD.
Ещё одно отличие между программами рис. 4.1 и рис. 4.5 заключается в том, что инструкция RET заменена символическим именем $EXIT_S. Этот способ возврата управления операционной системе VAX/VMS наиболее предпочтителен. Имя $EXIT_S - не мнемонический код инструкции (как MOVB) и не директива (как .ENTRY), а имя макроинструкции. Макроинструкцией называется несколько строчек текста программы, которым дано некоторое имя и которые сохраняются в некотором, известном ассемблеру месте. Если в программе используется имя макроинструкции (в данном случае $EXIT_S), ассемблер автоматически заменяет его соответствующими строками текста программы. В случае макроинструкции $EXIT_S заменяющие её строки текста служат для возврата управления ОС VAX/VMS. Чтобы подчеркнуть то, что ассемблер для ЭВМ VAX включает возможности макрообработки, фирма DEC дала ему название VAX-11 MACRO. Подробно макроинструкции будут рассмотрены в гл. 10.
Код Метка операции Операнды Примечания .TITLE IMPROVED - УСОВЕРШЕНСТВОВАННАЯ ПРОГРАММА НА АССЕМБЛЕРЕ ; ОПИСАНИЕ ПРИМЕР ВЫЧИСЛЕНИЯ СУММЫ 26+38+5 ; ПРОГРАММИСТ КЭПС И СТАФФОРД ; ДАТА ИЮЛЬ 1,1985 ; ; ОБЛАСТЬ ДАННЫХ A: .BYTE 26 ; БАЙТ ДАННЫХ B: .BYTE 38 ; БАЙТ ДАННЫХ C: .BYTE 5 ; БАЙТ ДАННЫХ SUM: .BLKB 1 ; РЕЗЕРВИРОВАНИЕ БАЙТА ; ;ОБЛАСТЬ ПРОГРАММЫ .ENTRY START,0 ; АДРЕС ТОЧКИ ВХОДА MOVB A,SUM ; SUM := А ADDB2 B,SUM ; SUM := SUM+B ADDB2 C,SUM ; SUM := SUM+C $EXIT_S ; ВОЗВРАТ .END START
Рис. 4.5. Улучшенный вариант программы на языке ассемблера
В ассемблере требуется, чтобы операторы языка ассемблера удовлетворяли определённым синтаксическим правилам. Часть правил касается написания символических имён, определяемых программистом, таких как A, B, C, SUM и START в программе на рис. 4.5. Символические имена могут иметь длину от 1 до 31 символа и включают буквы латинского алфавита (A-Z), цифры (0-9) и три специальных символа: знак доллара ($), точку (.) и знак подчёркивания (_). Однако символические имена не должны начинаться с цифры (0-9). Приведём примеры допустимых символических имён:
X TAX R2D2 THISISALONGSYMBOLICNAME A_MORE_READABLE_LONG_NAME JULY_4_1976 $14.96
А следующие имена недопустимы по перечисленным ниже причинам:
Недопустимое имя | Причина |
---|---|
4_JULY_1776 |
ИМЯ НАЧИНАЕТСЯ С ЦИФРЫ |
WAGE RATE |
ИМЯ СОДЕРЖИТ НЕДОПУСТИМЫЙ СИМВОЛ - ПРОБЕЛ |
GROSS-PAY |
ИМЯ СОДЕРЖИТ НЕДОПУСТИМЫЙ СИМВОЛ - ЗНАК МИНУС |
$1,234,56 |
ИМЯ СОДЕРЖИТ НЕДОПУСТИМЫЙ СИМВОЛ - ЗАПЯТУЮ |
THIS_NAME_IS_LONGER_THAN_31_CHARACTERS |
ДЛИНА ИМЕНИ БОЛЬШЕ ЧЕМ 31 СИМВОЛ |
Выбирать имена надо с осторожностью. Употребление осмысленных имён, таких как WAGE (заработная плата), HOURS_WORKED (отработанные часы) и MONTH (месяц), сделает программу на языке ассемблера более простой для отладки и сопровождения. Следует помнить, что знак доллара не следует использовать в символических именах. В операционной системе VAX/VMS знак доллара присутствует в именах, которые применяются для наименования специальных системных функций, имена, содержащие знак доллара, резервируются для названий системных программ. Если программист случайно использует одно из этих имён, это может привести к ошибкам или непредсказуемым последствиям. Например, по макроинструкции $EXIT_S генерируется вызов системной программы SYS$EXIT. Если пользователь определит в своей программе символический адрес SYS$EXIT, то макроинструкция $EXIT_S не сможет работать нормально.
Поле метки содержит метки или имена символических адресов. Каждая метка - это символическое имя (адрес) некоторой области памяти. (Обычно остальные поля в каждой строке описывают содержимое одного или нескольких байтов, начинающихся по этому адресу.) Метка должна быть допустимым символическим именем, завершающимся двоеточием (:)[4]. По соглашению метки обычно размещаются в начале строки, но в принципе могут находиться в любом месте строки, если только перед меткой нет никаких символов, кроме пробелов. В общем случае метка в некоторой строке необходима только тогда, когда на эту строку есть ссылки из других строк программы.
Поле кода операции может начинаться с любой позиции строки после метки, если, данный оператор имеет метку. В противном случае началом этого поля считается первый отличный от пробела символ. Однако по соглашению поле кода операции начинается с первой позиции табуляции, т.е. обычно с девятой позиции строки. В итоге коды операций начинаются с одной колонки, облегчая чтение программы. (Обычно позиции табуляции размещаются через 8 символов, т.е. в позициях 9, 17, 25, 33, ...)
Если оператор начинается с метки, в имени которой содержится более 7 символов, эту метку можно поместить на отдельной строчке, чтобы все коды операций размещались в одной колонке. Например, для записи инструкции с меткой A_LONG_NAME можно использовать две строки:
Код Метка операции Операнды A_LONG_NAME: ADDL2 READ_VALUE,X_TOTAL
Некоторые программисты применяют такой формат для всех инструкций с метками независимо от длины имени метки.
Поле кода операции может содержать три типа символических имён:
Следует отметить, что все три типа имён удовлетворяют правилам, приведённым выше для символических имён, определяемых пользователем.
Поле операндов может начинаться с любой позиции после поля кода операции и должно отделяться от последнего по крайней мере одним пробелом или символом табуляции. Однако по соглашению поле операндов начинается с 17-й позиции, т.е. 2-й позиции табуляции. Поле операндов состоит из некоторого числа операндов, разделённых запятыми. Число операндов зависит от типа инструкции, т.е. от содержимого поля кода операции. В директиве .BLKB требуется один операнд, в инструкции ADDB3 требуется три операнда, а в инструкции RET операнды вообще отсутствуют. Если пользователь укажет неверное число операндов, ассемблер выдаст сообщение об ошибке.
Ассемблер будет выдавать также сообщения об ошибках и при нелепых комбинациях кода операции и операндов. Например, в случае оператора
Код Метка операции Операнды J: .BLKL #1
будет порождено сообщение об ошибке, поскольку знак номера (#) представляет собой спецификатор операнда, который допустим только тогда, когда в поле кода операции содержится код операции.
Аналогично оператор
Код Метка операции Операнды FIRST: MOVL #5,#3
приведёт к ошибке, поскольку бессмысленно заменять непосредственный операнд - число 3 непосредственным операндом - числом 5. (Значение константы не может меняться.)
Комментарии должны начинаться со знака ;. Все, что расположено после точки с запятой, игнорируется, т.е. не рассматривается как часть программы. Комментарии могут начинаться с любой позиции строки после поля операндов (или после поля кода операции, если операнды не нужны). Когда для комментариев нужно использовать всю строку, точка с запятой ставится в начале строки.
Ассемблер предоставляет программисту целый ряд дополнительных возможностей. Одной из них является преобразование чисел с различными основаниями. Обычно числа в программе на ассемблере интерпретируются как десятичные. Однако если перед числом стоят символы ^X, то это число будет рассматриваться как шестнадцатеричное. Например, действие следующих двух инструкций идентично:
MOVL #64,K MOVL #^X40,K
Каждая из этих инструкций пересылает десятичное число 64 (шестнадцатеричное - 40) в длинное слово K. Именно по этой причине обозначение ^X использовалось, чтобы отличить в тексте шестнадцатеричные числа.
Аналогично если число начинается с символов ^B (binary), то оно интерпретируется как двоичное. В частности, все следующие инструкции эквивалентны:
MOVL #24,K MOVL #^X18,K MOVL #^B11000,K
Каждая из этих трёх инструкций пересылает десятичную константу 24 в длинное слово K. Программист может выбрать любое представление, которое более всего соответствует решаемой задаче.
Неправильная запись чисел может привести к ошибкам. Ниже приведены некорректно заданные числа и указаны допущенные в них ошибки.
Число | Причина ошибки |
---|---|
^B1012 |
ДВОИЧНЫЕ ЧИСЛА МОГУТ ВКЛЮЧАТЬ ТОЛЬКО ЦИФРЫ 0 И 1 |
1O |
ИСПОЛЬЗОВАНА БУКВА 0 ВМЕСТО ЦИФРЫ 0 |
X1234ABCD |
ПЕРЕД ЧИСЛОМ ПРОПУЩЕН СИМВОЛ (^) |
123,456 |
ЧИСЛО СОДЕРЖИТ ЗАПЯТУЮ (,) - ЭТО НЕДОПУСТИМО |
Как уже говорилось в разд. 4.2, директива .BYTE используется для резервирования памяти и задания начального значения для данных в формате байта. Например, директивы
BOND: .BYTE 007 MAX_BYTE: .BYTE ^XFF UPDOWN: .BYTE ^B10101010
установят для байтов с адресами BOND, MAX_BYTE и UPDOWN начальные значения 7, ^XFF (десятичное число 255) и ^B10101010 (десятичное число 170) соответственно. В процессе ассемблирования каждой строки содержимое счётчика адресов будет увеличиваться на 1, а к программе в машинном коде будет добавляться один байт информации. Наибольшее допустимое значение операнда в директиве .BYTE равно 255.
Директива .WORD очень похожа на директиву .BYTE, за исключением того, что она резервирует и задаёт начальное значение для 16-битового слова. Например, операторы
YEAR: .WORD 1984 MAX_WORD: .WORD ^XFFFF GROSS: .WORD 144
установят начальные значения слов с адресами YEAR, MAX_WORD и GROSS равными 1984, ^XFFFF (65535 - десятичное) и 144 соответственно. При первом проходе ассемблера каждый из этих операторов вызовет увеличение содержимого счётчика адресов на 2. Во время второго прохода операнды будут преобразованы, если это необходимо, в шестнадцатеричную форму и помещены в последовательно расположенных парах байтов программы. Заметим, что ^XFFFF, или 65535, - наибольшее допустимое значение операнда в директиве .WORD.
Аналогично для инициализации 32-битовых длинных слов используется директива .LONG. В отличие от других ЭВМ в ЭВМ семейства VAX директивы .BYTE, .WORD и .LONG могут располагаться в любом порядке. Например, операторы
HOMERS: .BYTE 61 MAX_LONG: .LONG ^XFFFFFFFF PERMUTES: .WORD 5040
установят для байта с адресом HOMERS начальное значение 61, для длинного слова MAX_LONG - значение ^XFFFFFFFF, которое приблизительно равно 4 млрд, и для слова PERMUTES - значение 5040. При обработке этих операторов значение счётчика адресов увеличится на 7, при этом будет сгенерировано 7 байтов машинного кода.
Существуют два различных способа присваивания байту, слову или длинному слову не-которого значения. Например, предположим, что данное слово COUNT должно содержать число 10. Конечно, это значение можно было бы задать с помощью директивы
COUNT: .LONG 10
Этот же результат можно получить, используя два оператора:
COUNT: .BLKL 1 . . . MOVL #10,COUNT
При первом способе инициализация COUNT осуществляется только один раз во время ассемблирования. При применении второго способа инструкцию MOVL можно поместить в цикл, тогда при каждом проходе цикла значение COUNT будет установлено равным 10. Второй способ аналогичен использованию операторов COUNT=10 или COUNT:=10; языков Фортран или Паскаль. Присваивание значений первым способом аналогично действию оператора Фортрана DATA COUNT/10/. В стандартном языке Паскаль нет аналога первому способу.
При выборе одного из этих двух способов задания значения надо руководствоваться следующими соображениями.
Как уже упоминалось, директивы .BLKB и .BLKL используются при резервировании памяти для данных в формате байтов и длинных слов. Аналогично директива .BLKW используется для резервирования слов. Рассмотрим следующие операторы:
ALPHA: .BLKB 1 BETA: .BLKL 1 GAMMA: .BLKW 1 THETA: .BLKB 1
Эти четыре директивы будут резервировать память для двух байтов (ALPHA и THETA), одного слова (GAMMA) и одного длинного слова (BETA). Все вместе они вызовут увеличение значения счётчика адресов 1 + 1 + 2 + 4 = 8.
Как описано в гл. 7, различные директивы .BLКх могут использоваться для резервирования памяти под массивы. Например, оператор
VECTOR: .BLKL 20
означает, что VECTOR - это начальный адрес массива, состоящего из 20 длинных слов. Этот оператор увеличит значение счётчика адресов на 4 * 20 = 80.
Ранее были описаны символические имена разного типа метки (символические адреса), мнемонические коды операций, директивы ассемблера и макроинструкции. Было бы желательно также применять символические имена для обозначения чисел в программе.
Замена чисел символическими именами осуществляется следующим образом: если в программе есть оператор
Код Метка операции Операнды FACTORIAL_7=5040
то при каждом появлении в программе символического имени FACTORIAL_7 оно будет заменяться числом 5040. Например, для инструкции
Код Метка операции Операнды FACTORIAL_7=5040 . . . MOVL #FACTORIAL_7,ANS
будет порождён тот же машинный код, что и для инструкции
MOVL #5040,ANS
В процессе ассемблирования знак равенства (=) рассматривается аналогично двоеточию (:). При первом проходе ассемблера двоеточие указывает на то, что предшествующее ему имя надо занести в таблицу имён, определяемых пользователем вместе с текущим значением счётчика адресов. При обнаружении знака равенства также происходит занесение предшествующего ему имени в таблицу имён, определяемых пользователем. Однако значение, заносимое в эту таблицу, представляет собой число, стоящее справа от знака равенства. Символические имена, обозначающие числа (FACTORIAL_7), следует определять (FACTORIAL_7 = 5040) до того, как они будут использованы (MOVL #FACTORIAL_7,ANS). По соглашению все имена определяются обычно в начале программы сразу после директивы .TITLE. Определения имён (FACTORIAL_7 = 5040) могут помещаться в любом месте строки. Однако по соглашению они начинаются с начала строчки. Использование имён для обозначения чисел имеет два важных преимущества: программа становится наглядной и облегчается её сопровождение. Для того чтобы убедиться, что использование поименованных констант облегчает чтение текста программы, рассмотрим следующий оператор:
MOVL #12,COUNT
Он показывает, что осуществляется подсчёт чего-то (COUNT - счётчик), но ничего не сообщает о том, что именно считается. При этом #12 может означать число единиц в некоторой дюжине, число дюймов в футе, число месяцев в году, число участников Тайной Вечери, число лет, начиная с которых человека можно считать подростком, и число нот в октаве. Если же пользователь определит символические имена, например, следующим образом:
DOZEN=12 INCHES_IN_FOOT=12 MONTHS_IN_YEAR=12 DISCIPLES_AT_SUPPER=12 PRE_TEEN_YEARS=12 NOTES_IN_OCTAVE=12
то он сможет применять в своей программе уже гораздо более осмысленные операторы, такие как
MOVL #NOTES_IN_OCTAVE,COUNT
Наконец, использование символических имён для обозначения чисел позволяет облегчить сопровождение программы. Например, программа для бухгалтерского учёта и управления ресурсами в кондитерском магазине, вероятно, неоднократно использует число 12 для преобразования в дюжины единиц. Если программа должна эксплуатироваться в другом магазине, где дюжина соответствует "чертовой дюжине", то необходимо просмотреть всю программу, чтобы заменить определённые константы 12 на 13. В программе, написанной надлежащим образом, следует изменить только один оператор DOZEN=12 на DOZEN=13. Если этот пример покажется слишком надуманным, то вспомните, как много программ требуется модифицировать для перевода из английской системы мер (фунты, футы, кварты) в метрическую систему (грамм, метр, литр). Использование имён для представления чисел намного упростит такое преобразование.
Оператор DOZEN = 12 в языке ассемблера аналогичен следующим операторам языка Фортран:
Фортран | Паскаль |
---|---|
PARAMETER (DOZEN=12) |
VAR |
Определение имён в языке ассемблера с помощью оператора, подобного A = 7, кажется очень похожим на операторы присваивания в языках высокого уровня (A = 7 в Фортране или A := 7; в Паскале). Однако сходство обманчиво. В языке ассемблера все выражения вычисляются во время ассемблирования. При обработке оператора A = 7 имя и значение 7 заносится в таблицу имён. Когда ассемблер находит в дальнейшем тексте имя A, он сразу заменяет его на значение 7.
В большинстве языков высокого уровня выражения вычисляются во время выполнения программы. В процессе компиляции оператор присваивания, такой как A = 7 или A := 7, в действительности преобразуется в инструкцию, такую как MOVL #7, A. Во время выполнения эта инструкция осуществит пересылку числа 7 в ячейку памяти A.
Как было упомянуто, таблица имён, определяемых пользователем, может содержать два различных типа объектов: имена, обозначающие адреса, и имена, представляющие числа. Адреса - это числа специального вида, на которые наложен целый ряд ограничений.
Например, адреса - это 32-битовые числа без знака. Кроме того, администраторы вычислительных систем VAX, используя особенности аппаратуры и программного обеспечения системы, обычно ограничивают диапазон адресов, доступных программисту. Нарушение этих ограничений при выполнении программы приводит к ошибке адресации. Числа, разумеется, таким ограничениям не подвергаются.
Хотя числа и адреса довольно сильно отличаются, имена, применяемые для их обозначения, очень похожи. Основное отличие между ними - то, что при определении символического адреса обычно используется двоеточие, а при определении имени для обозначения числа - знак равенства. В результате легко использовать число как адрес и наоборот. Например, в следующих операторах:
MILE=5280 . . . MOVL MILE,COUNT
программист случайно пропустил символ # в операнде MILE. В результате имя MILE, определённое как число (MILE=5280), используется в инструкции MOVL в качестве адреса. В процессе выполнения инструкции MOVL будет произведена попытка выборки длинного слова, расположенного по адресу 5280 (или ^X14A0). Последствия этого предсказать очень трудно. В зависимости от конкретной ситуации такая инструкция может вызвать сообщение об ошибке во время ассемблирования, сообщение об ошибке во время выполнения или просто будет получен неправильный результат.
Ошибка возникает и тогда, когда там, где не надо, указывается знак #. Например, в операторах
ALT: .BLKL 1 . . . MOVL #ALT,COUNT
знак # ошибочно поставлен перед операндом ALT. При выполнении инструкции MOVL операнд COUNT получает в качестве значения адрес ALT, а не его содержимое. На самом деле встречаются ситуации, в которых адреса обрабатываются как данные и ЭВМ VAX имеют специальные инструкции выполнения операций над адресами (см. гл. 7). Поэтому обычно считается, что использование знака # вместе с символическим адресом свидетельствует о плохом стиле программирования.
A: .BYTE 20 B: .BYTE 128 C: .BYTE -128 D: .WORD 128 E: .WORD -128 F: .LONG ^X128 G: .BYTE ^B10000000 H: .LONG 4096 .ENTRY INST,0 $EXIT_S .END INST
J: .BLKL 1 K: .BLKL 1 DIF: .BLKL 1 .ENTRY ADDRESS,0 FIRST: MOVL #512,J MOVL #64,K SUBL3 K,J,DIF LAST: $EXIT_S .END ADDRESS
Отлаживать одну большую программу обычно гораздо труднее, чем несколько небольших. Поэтому хорошие программисты обычно разбивают большую программу на несколько небольших модулей. При таком разбиении один из этих модулей указывается в качестве основной программы, которая, первой во время выполнения получает управление. Остальные модули по очереди вызываются основной программой и называются подпрограммами. На ЭВМ VAX имеется несколько инструкций для вызова подпрограмм и возврата из них. Некоторые из этих инструкций будут описаны в следующей главе.
В принципе программные модули можно написать на разных языках программирования. Например, основная программа А может представлять собой программу в машинном коде, полученную в результате трансляции с языка Фортран, а подпрограмма В может быть результатом трансляции с языка ассемблера. Разбивая сложную задачу на небольшие подзадачи, программист может выбрать для решения каждой подзадачи наиболее подходящий язык.
Поскольку два модуля в одной и той же программе не могут занимать одно и то же место в памяти, необходимо, чтобы каждый модуль занимал свое собственное множество адресов. Такую работу не могут выполнить ни ассемблер, ни компиляторы языков высокого уровня, так как обычно они не располагают информацией о всех модулях, составляющих программу. Для разрешения этой проблемы применяется программа, называемая компоновщиком (linker).
Компоновщик выполняет две функции: перемещает различные модули, составляющие программу, так, чтобы они занимали непересекающиеся области памяти; в случае необходимости делает адреса, определённые в одном модуле, доступными для других модулей. (Такие адреса требуются для передачи управления из одного модуля в другой). Эта программа называется компоновщиком потому, что она компонует взаимозависимые модули машинного кода в одну программу, готовую к выполнению.
Обычно предполагается, что программа на ассемблере должна быть обработана компоновщиком, даже если она состоит только из одного модуля. Программа сразу после ассемблирования не может быть выполнена. Программа, полученная в результате работы ассемблера, называется объектным модулем (object module) и хранится в объектном файле, имеющем тип OBJ. Результат работы компиляторов языков высокого уровня, таких как Фортран или Паскаль, имеет такой же формат, и помещается в файлы типа OBJ.
Компоновщик считывает файлы типа OBJ, перемещает и связывает объектные модули, чтобы сформировать единую, готовую к выполнению программу. Программа в этом виде называется выполняемым образом (executable image) и хранится в файле выполняемого образа, имеющем тип EXE.
В конце гл. 3 было отмечено, что относительная адресация по сравнению с абсолютной даёт кроме экономии памяти и времени, другие существенные преимущества: использование относительной адресации внутри модуля означает то, что относительные адреса не должны модифицироваться при перемещении этого модуля. Внутри модуля расстояние (смещение) между инструкциями и данными, к которым они обращаются, не должно изменяться при перемещении модуля. Напротив, если в модуле используется абсолютная адресация, то при перемещении этого модуля все абсолютные адреса будут изменены. Поэтому абсолютные адреса в объектном модуле должны быть помечены ассемблером, для того чтобы компоновщик смог модифицировать их при создании выполняемого образа. (Модули, в которых не требуется модификация адресов при компоновке, называются позиционно независимыми. Эта важная тема подробно рассматривается в гл. 7 и 15.)
В качестве иллюстрации рассмотрим процесс ассемблирования, компоновки и выполнения программы, написанной на языке ассемблера. Предполагается, что читатель должен использовать терминал некоторого типа. В рассматриваемом примере программист зарегистрирован в системе под именем JOHNDOE. Предполагается, что он с помощью одного из редакторов текста, имеющихся на вычислительных системах VAX, создал исходный файл с именем SAMPLE.MAR. Если теперь программист введёт с терминала команду DIRECTORY (каталог) или просто DIR, то на экране дисплея появится
$ DIR Directory DISK$USER: [JOHNDOE] SAMPLE.MAR;1 Total of 1 file. $
Символы, которые вводит с клавиатуры пользователь, выделяются подчёркиванием. Знак доллара $ в последней строке служит признаком того, что операционная система VAX/VMS готова принять очередную команду.
Пользователь может посмотреть содержимое файла SAMPLE.MAR, выведя его на свой терминал с помощью команды ТУРЕ:
$ TYPE SAMPLE.MAR .TITLE SQUARES -ТАБЛИЦА КВАДРАТНЫХ КОРНЕЙ- .DISABLE GLOBAL ; УКАЗАТЬ НЕОПРЕДЕЛЁННЫЕ ИМЕНА ; ВЫЧИСЛИТЬ ТАБЛИЦУ КВАДРАТНЫХ КОРНЕЙ С ПОМОЩЬЮ МЕТОДА КОНЕЧНЫХ РАЗНОСТЕЙ ; АВТОР - ДЖОН ДОУ ; ДАТА СОЗДАНИЯ - ИЮЛЬ 31,1985 SECOND_DIF=2 ; КОНСТАНТА - ВТОРАЯ РАЗНОСТЬ FIRST_DIF_INIT=1 ; НАЧАЛЬНАЯ ПЕРВАЯ РАЗНОСТЬ SQUARE_INIT=1 ; КОРЕНЬ ИЗ ЕДИНИЦЫ FIRST_DIF: ; ПЕРЕМЕННАЯ - ПЕРВАЯ РАЗНОСТЬ .BLKL 1 SQUARE_1: ; СОДЕРЖИТ ЗНАЧЕНИЕ ПЕРВОГО КОРНЯ .BLKL 1 SQUARE_2: ; СОДЕРЖИТ ЗНАЧЕНИЕ ВТОРОГО КОРНЯ .BLKL 1 SQUARE_3: ; СОДЕРЖИТ ЗНАЧЕНИЕ ТРЕТЬЕГО КОРНЯ .BLKL 1 .ENTRY BEGIN,0 ; ТОЧКА ВХОДА MOVL #FIRST_DIF_INIT,FIRST_DIF ; ЗАДАТЬ ПЕРВУЮ РАЗНОСТЬ MOVL #SQUARE_INIT,SQUARE_1 ; ВЫЧИСЛИТЬ ПЕРВЫЙ КОРЕНЬ ADDL2 #SECOND_DIF,FIRST_DIF ; СКОРРЕКТИРОВАТЬ ПЕРВУЮ РАЗНОСТЬ ADDL3 FIRST_DIF,SQUARE_1,SQUARE_2 ; ВЫЧИСЛИТЬ ВТОРОЙ КОРЕНЬ ADDL2 #SECOND_DIF,FIRST_DIF ; СКОРРЕКТИРОВАТЬ ВТОРУЮ РАЗНОСТЬ ADDL3 FIRST_DIF,SQUARE_2,SQUARE_3 ; ВЫЧИСЛИТЬ ТРЕТИЙ КОРЕНЬ $EXIT_S ; ЗАВЕРШИТЬ ПРОГРАММУ .END BEGIN $
Эта программа вычисляет квадраты чисел от единицы до трёх с помощью метода разностей. В основу этого метода положен тот факт, что разности между квадратами соседних чисел натурального ряда образуют ряд 1, 3, 5, 7, ... Заметим, что разность между смежными членами последнего ряда постоянна и равна 2. Иллюстрация этого метода приведена ниже.
Квадраты | 0 | 1 | 4 | 9 | 16 | ... | |||||
Первая разность | 1 | 3 | 5 | 7 | ... | ||||||
Вторая разность | 2 | 2 | 2 | ... |
Для вычисления каждого следующего члена ряда квадратов надо прибавить 2 к последней вычисленной разности (например, 7 + 2 = 9). Полученный результат надо прибавить к последнему вычисленному квадрату (9 + 16 = 25).
Ассемблирование программы, написанной на языке ассемблера, производится по команде $MACRO/DEBUG/LIST SAMPLE. Параметры /DEBUG и /LIST, следующие за командой MACRO, устанавливают дополнительные режимы работы ассемблера. Параметр /DEBUG информирует ассемблер о том, что пользователю потребуются средства отладки во время выполнения программы. Параметр /LIST указывает ассемблеру, что требуется выдать листинг программы как в машинном коде, так и на языке ассемблера.
Результат выполнения команды MACRO можно обнаружить, просмотрев каталог:
$ MACRO/DEBUG/LIST SAMPLE $ DIR Directory DISK$USER: [JOHNDOE] SAMPLE.LIS;1 SAMPLE.MAR;1 SAMPLE.OBJ;1 Total of 3 files. $
Новый файл SAMPLE.OBJ содержит вариант программы в машинном коде. Не надо пытаться вывести этот файл на терминал, он имеет не символьный, а двоичный формат. Ещё один файл, SAMPLE.LIS, содержит листинг программы на языке ассемблера. По умолчанию ассемблер присваивает объектному файлу тип .OBJ, файлу листинга - тип .LIS. Так как файл SAMPLE.LIS символьный, его содержимое можно вывести на терминал пользователя:
$ TYPE SAMPLE.LIS SQUARES -ТАБЛИЦА КВАДРАТНЫХ КОРНЕЙ- 24-JUN-1985 20:00:43 VAX-11 Macro V03-00 Page 1 24-JUN-1985 19:54:02 DISK$USER:[JOHNDOE]SAMPLE.MAR; 1 (1) 0000 1 .TITLE SQUARES -ТАБЛИЦА КВАДРАТНЫХ КОРНЕЙ- 0000 2 .DISABLE GLOBAL ; УКАЗАТЬ НЕОПРЕДЕЛЁННЫЕ ИМЕНА 0000 3 ;ВЫЧИСЛИТЬ ТАБЛИЦУ КВАДРАТНЫХ КОРНЕЙ С ПОМОЩЬЮ МЕТОДА КОНЕЧНЫХ РАЗНОСТЕЙ 0000 4 ;АВТОР - ДЖОН ДОУ 0000 5 ;ДАТА СОЗДАНИЯ - ИЮЛЬ 31, 1985 00000002 0000 6 SECOND_DIF=2 ;КОНСТАНТА - ВТОРАЯ РАЗНОСТЬ 00000001 0000 7 FIRST_DIF_INIТ=1 ; НАЧАЛЬНАЯ ПЕРВАЯ РАЗНОСТЬ 00000001 0000 8 SQUARE_INIT=1 ; КОРЕНЬ ИЗ ЕДИНИЦЫ 0000 9 FIRST_DIF: ; ПЕРЕМЕННАЯ - ПЕРВАЯ РАЗНОСТЬ 00000004 0000 10 .BLKL 1 0004 11 SQUARE_1: ; СОДЕРЖИТ ЗНАЧЕНИЕ ПЕРВОГО КОРНЯ 00000008 0004 12 .BLKL 1 0008 13 SQUARE_2: ; СОДЕРЖИТ ЗНАЧЕНИЕ ВТОРОГО КОРНЯ 0000000С 0008 14 .BLKL 1 000С 15 SQUARE_3: ; СОДЕРЖИТ ЗНАЧЕНИЕ ТРЕТЬЕГО КОРНЯ 00000010 000С 16 .BLKL 1 0000 0010 17 .ENTRY BEGIN,0 ; ТОЧКА ВХОДА ЕА AF 01 D0 0012 18 MOVL #FIRST_DIF_INIT,FIRST_DIF ; ЗАДАТЬ ПЕРВУЮ РАЗНОСТЬ EA AF 01 D0 0016 19 MOVL #SQUARE_INIT,SQUARE_1 ; ВЫЧИСЛИТЬ ПЕРВЫЙ КОРЕНЬ E2 AF 02 С0 001A 20 ADDL2 #SECOND_DIF,FIRST_DIF ; СКОРРЕКТИРОВАТЬ ПЕРВУЮ РАЗНОСТЬ E3 AF E1 AF DF AF C1 001E 21 ADDL3 FIRST_DIF,SQUARE_1,SQUARE_2 ; ВЫЧИСЛИТЬ ВТОРОЙ КОРЕНЬ D7 AF 02 С0 0025 22 ADDL2 #SECOND_DIF,FIRST_DIF ; СКОРРЕКТИРОВАТЬ ВТОРУЮ РАЗНОСТЬ DС AF DA AF D4 AF Cl C029 23 ADDL3 FIRST_DIF,SQUARE_2,SQUARE_3 ; ВЫЧИСЛИТЬ ТРЕТИЙ КОРЕНЬ 0030 24 $EXIT_S ; ЗАВЕРШИТЬ ПРОГРАММУ 0039 25 .END BEGIN SQUARES -ТАБЛИЦА КВАДРАТНЫХ КОРНЕЙ- 24-JUN-1985 20:00:43 VAX-11 Macro V03-00 Page 2 Symbol table 24-JUN-1985 19:54:02 DISK$USER:[JOHNDOE] SAMPLE. MAR;1 (1) BEGIN 00000010 RG D 01 FIRST_DIF 00000000 R D 01 FIRST_DIF_INIT = 00000001 D SECOND_DIF = 00000002 D SQUARE_1 00000004 R D 01 SQUARE_2 00000008 R D 01 SQUARE_3 0000000C R D 01 SQUARE_INIT = 00000001 D SYS$EXIT ******** G 01 +----------------+ ! Psect synopsis ! +----------------+ (данная секция листинга пропущена) +------------------------+ ! Performance indicators ! +------------------------+ (данная секция листинга пропущена) +--------------------------+ ! Macro library statistics ! +--------------------------+ (данная секция листинга пропущена) There were no errors, warnings or information messages. /DEBUG/LIST SAMPLE $
Листинг ассемблерной программы и программы в машинном коде достаточно понятен, за исключением строк 18, 19, 20 и 22. В соответствии с изложенным выше можно было ожидать, что при обработке оператора ассемблера
MOVL #FIRST_DIF_INIТ,FIRST_DIF
будет сгенерирован код
Операнд | Операнд | Код операции | Адрес |
---|---|---|---|
ЕЕ AF |
00000001 |
8F D0 |
0012 |
где ^X0012 - начальный адрес, ^X8F - спецификатор операнда (байт) непосредственной адресации, ^X00000001 - число, соответствующее имени FIRST_DIF_INIT. Однако в строке 18 сгенерирован следующий код:
Операнд | Операнд | Код операции | Адрес |
---|---|---|---|
ЕЕ AF |
01 |
D0 |
0012 |
Вообще говоря, существует два способа задания непосредственных операндов, требующих меньше и больше памяти. Первый - наиболее экономный способ применим только для небольших чисел от 0 до 63. Любые другие числа задаются вторым способом со спецификатором операнда ^X8F. Первый способ задания называется литеральным режимом адресации и позволяет хранить число непосредственно в байте спецификатора операнда. Для задания таких чисел используются спецификаторы операндов со значениями от ^X00 до ^X3F. Более подробно литеральный режим адресации описан в гл. 7.
После распечатки программы ассемблер печатает таблицу имён, определённых пользователем. Строка
FIRST_DIF 00000000 R D 01
в этой таблице сообщает, что значениям символического имени FIRST_DIF является ^X00000000. Стоящая далее буква R показывает, что FIRST_DIF - переместимый адрес, т.е. адрес будет настраиваться при перемещении программы. Буква D означает, что это символическое имя доступно отладчику. Число 01 - ссылка на программную секцию (см. гл. 9).
Переместимый адрес BEGIN отличается от остальных адресов в программе, поскольку он определён директивой .ENTRY. Это означает, что хотя адрес BEGIN определяется в данной программе, но на него можно ссылаться в других программах (например, из операционной системы VAX/VMS). Символические имена, которые определены в данной программе, но доступны другим программам, называются глобальными именами (global symbol). В данном случае буквы RG означают, что BEGIN - это переместимое глобальное имя. В последней строке таблицы имён содержится запись
SYS$EXIT ******** G 01
Это имя генерируется макроинструкцией $EXIT_S, которая используется для возврата управления операционной системе VAX/VMS. Макроинструкция $EXIT_S в действительности генерирует инструкции, передающие управление по адресу SYS$EXIT, значение которого определяется операционной системой VAX/VMS. Поскольку адрес SYS$EXIT используется в этой программе, а определён в другой, он обозначается как глобальный (G). Символы ******** означают, что имя SYS$EXIT в настоящий момент не определено. Более подробно глобальные имена описаны в гл. 9.
Как следует из последней строки таблицы имён, ассемблер допускает наличие неопределённых имён, таких как SYS$EXIT. По умолчанию ассемблер считает неопределённые имена глобальными и предполагает, что компоновщик где-нибудь найдёт значение для этих имён, Это свойство, хотя и является полезным, приводит к тому, что некоторые ошибки пользователя, такие как неправильно заданные имена, ассемблер не обнаруживает. Данную возможность ассемблера можно исключить с помощью помещаемого в начало программы оператора
.DISABLE GLOBAL ; УКАЗАТЬ НЕОПРЕДЕЛЁННЫЕ ИМЕНА
При наличии такого оператора ассемблер генерирует сообщение об ошибке каждый раз, когда обнаруживает неопределённое имя в программе пользователя. (Имя SYS$EXIT не вызовет сообщения об ошибке, поскольку в макроинструкции $EXIT_S оно определено как внешнее.)
Теперь можно описать действия, выполняемые при указании параметра /DEBUG, команды MACRO. Когда этот параметр отсутствует, то в создаваемый объектный файл ассемблер включает информацию только о глобальных именах. Оставшаяся часть таблицы имён теряется с окончанием ассемблирования. Параметр /DEBUG указывает ассемблеру на необходимость сохранить в объектном файле таблицу имён целиком. Это позволяет программисту использовать при отладке символические имена.
Программа компонуется по команде операционной системы VAX/VMS LINK/DEBUG SAMPLE. Результат выполнения команды LINK обнаруживается в каталоге пользователя:
$ LINK/DEBUG SAMPLE $ DIR Directory DISK$USER:[JOHNDOE] SAMPLE.EXE;1 SAMPLE.LIS;1 SAMPLE.MAR;1 SAMPLE.OBJ;1 Total of 4 files. $
Новый файл SAMPLE.EXE - это выполняемый вариант пользовательской программы в машинном коде, В процессе преобразования объектной программы SAMPLE.OBJ в выполняемую программу SAMPLE.EXE компоновщик настраивает программу так, чтобы она начиналась с адреса ^X0200, а не с адреса ^X0000. Процесс настройки очень прост, поскольку в программе используется только относительная адресация. Причины, по которым производится эта настройка, описаны в разделе, посвящённом ошибкам программирования и отладке. Параметр /DEBUG в команде LINK указывает на то, что программисту потребуются средства отладки при выполнении программы SAMPLE.EXE.
Программу пользователя можно запустить на выполнение, введя команду RUN SAMPLE. Так как при компоновке к программе были присоединены средства отладки (был указан параметр /DEBUG в команде LINK), то теперь управление передаётся не пользовательской программе, а программе-отладчику DEBUG. (Отладчик можно также активизировать командой RUN/DEBUG SAMPLE.) Отладчик выводит на терминал две строки информации, а затем сообщение DBG):
$ RUN SAMPLE VAX-11 DEBUG Version V3.4-2 %DEBUG-I-INITIAL,language is MACRO,module set to 'SQUARES' DBG>
Точно так же, как операционная система VAX/VMS использует символ $ как приглашение к вводу следующей команды операционной системы, так и отладчик использует сообщение DBG>: для указания того, что пользователю следует вводить команду отладчика. Обычно в этот момент вводят команду GO, по которой начинается выполнение программы пользователя.
DBG>GO routine start at SQUARES\BEGIN %DEBUG-I-EXITSTATUS,is '%SYSTEM-S-NORMAL, normal successful completion' DBG>
На этом этапе обычно просматривают содержимое памяти по команде Е (Examine - посмотреть) , например
DBG>E SQUARE_1 SQUARES\SQUARE_1: 00000001 DBG>E SQUARE_2 SQUARES\SQUARE_2: 00000004 DBG>E/DEC SQUARE_3 SQUARES\SQUARE_3: 9 DBG>
По команде E SQUARE_1 выводится на терминал содержимое длинного слова SQUARE_1 в программе SQUARES (SQUARES\SQUARE_1), оно равно ^X00000001; по второй команде Е SQUARE_2 выводится содержимое длинного слова SQUARE_2, равное ^X00000004. Наконец, по команде E/DEC SQUARE_3 выводится содержимое адреса SQUARE_3 в десятичном виде; число 9 соответствует ожидаемому результату. Отметьте, что в то время, как по умолчанию в ассемблере используется десятичная система счисления, в отладчике по умолчанию числа выводятся в шестнадцатеричном виде.
С помощью отладчика можно посмотреть область памяти, где расположены инструкции. Например, по команде
DBG>E BEGIN+2 SQUARES\BEGIN+2: 0EAAF01D0 DBG>
выдаётся машинный код первой инструкции программы. (Так как BEGIN - точка входа программы, этот адрес является адресом 16-битовой маски (два байта). Первая инструкция расположена непосредственно за маской, поэтому её адрес BEGIN+2. Инструкция располагается в длинном слове справа налево: ^XD0 - код операции, ^X01 - литеральный операнд ^X01, ^XAF - спецификатор второго операнда и ^X0E - смещение, заданное в формате байта.
Отладчик не может определить, что содержится по данному адресу: байт, слово, длинное слово или инструкция. Поэтому, если не указано что-либо другое, по команде EXAMINE отображается содержимое длинного слова Содержимое байтов и слов можно посмотреть с помощью команд E/BYTE и Е/WORD соответственно. Командой Е/INST можно посмотреть инструкцию в символьном формате. Поскольку в команде MACRO присутствовал параметр /DEBUG, отладчику доступна таблица имён, а это означает, что он может частично восстановить по машинному коду заданный оператор исходной ассемблерной программы, например
DBG>E/INST BEGIN+2 SQUARES\BEGIN+2: MOVL #01,B^SQUARES\FIRST_DIF DBG>
Обратите внимание, что второй операнд инструкции начинается с символов В^. Эти символы означают, что для имени FIRST_DIF в программе SQUARES используется относительная адресация с заданным в формате байта смещением.
Команда Examine может использоваться для просмотра содержимого памяти по абсолютным адресам. Следующие шесть команд позволяют посмотреть содержимое различных ячеек памяти с адресами от ^X0000 до ^X020A:
DBG>E 0 %DEBUG-E-N0ACCESSR,no read access to virtual address 00000000 DBG>E 1FF %DEBUG-E-N0ACCESSR,no read access to virtual address 000001FF DBG>E 200 SQUARES\FIRST_DIF: 00000005 DBG>E 204 SQUARES\SQUARE_1: 00000001 DBG>E/WORD 210 SQUARES\BEGIN: 0000 DBG>E/INST 212 SQUARES\BEGIN+2: MOVL #01,B^SQUARES\FIRST_DIF DBG>EXIT $
Как показывают первые две команды, невозможно посмотреть содержимое памяти с адреса ^X0000 до адреса ^X01FF. Программисту эти адреса не нужны, поскольку компоновщик размещает программу с адреса ^X0200. Третья команда Е 200 выводит на экран содержимое длинного слова с символическим адресом FIRST_DIF. В результате перемещения программы абсолютный адрес FIRST_DIF равен ^X0200. При указании соответствующих адресов выводится содержимое ячеек с именами SQUARE_1. BEGIN и BEGIN+2. Наконец, команда отладчика EXIT возвращает управление операционной системе VAX/VMS.
Ошибки программирования в ассемблерной программе могут вызвать сообщения об ошибках разных типов. Чтобы продемонстрировать, как проявляются ошибки, в строку 19 предыдущей программы будет внесено несколько ошибок. Строка без ошибок выглядит следующим образом:
MOVL #SQUARE_INIT,SQUARE_1 ; ВЫЧИСЛИТЬ ПЕРВЫЙ КОРЕНЬ
В приведённой ниже строке вместо кода операции MOVL ошибочно указан код MOV:
MOV #SQUARE_INIT,SQUARE_1 ; ВЫЧИСЛИТЬ ПЕРВЫЙ КОРЕНЬ
В результате ассемблер не может найти значение для имени MOV и выдаёт следующее сообщение об ошибке (неопределённый оператор) :
$ MACRO SAMPLE 0016 19 MOV #SQUARE_INIT,SQUARE_1 korenx %MACRO-E-UNRECSTMT, Unrecognised statement There were 1 error, 0 warnings and 0 information messages, on lines: 19 (1)
Сходное сообщение будет выдано, если после метки будет пропущено двоеточие.
Весьма различные ошибки могут возникать при использовании числа в качестве адреса. Например, в следующей строке программист забыл поставить знак # перед именем SQUARE_INIT:
MOVL SQUARE_INIT,SQUARE_1 ; ВЫЧИСЛИТЬ ПЕРВЫЙ КОРЕНЬ
Ни ассемблер, ни компоновщик не сообщает об ошибке. Во время выполнения программа попытается произвести выборку длинного слова, расположенного по абсолютному адресу ^X00000001. Так как пользователю адреса, меньшие ^X0200, недоступны, то выдаётся следующее сообщение об ошибке (нарушение доступа):
$ MACRO SAMPLE $ LINK SAMPLE $ RUN SAMPLE %SYSTEM-F-ACCVIO, access violation, reason mask=00, virtual address=00000001, PC=0000021 %TRACE-F-TRACEBACK, symbolic stack dump follows module name routine name line rel PC abs PC SQUARES . BLANK . 0000001A 0000021A
По этому сообщению программист должен найти оператор, который, по всей вероятности, сгенерировал запрещённый виртуальный адрес ^X00000001. После символов abs PC распечатывается адрес ^X0000021A, который содержался в программном счётчике в момент обнаружения ошибки. Просмотр содержимого памяти вблизи адреса ^X021A с помощью отладчика может помочь локализовать ошибку. Адрес ^X0000001A, выведенный после символов rel PC, - это тот же адрес, но относительно начала модуля. Просмотр адресов вблизи адреса ^X001A по распечатке ассемблерной программы также может помочь обнаружить ошибку. Адрес abs PC больше адреса rel PC на ^X200, так как компоновщик настроил модуль на адрес ^X0200.
К генерации младших запрещённых адресов может привести множество ошибок программирования, в том числе и пропуск символа #. Если программа пытается обратить по адресу, меньшему ^X0200, то операционная система VAX/VMS прекращает выполнение программы. Фактически поэтому в операционной системе VAX/VMS перемещается начало основной программы в адрес ^X0200, а младшие адреса делаются недоступными для пользователя.
Использование адресов в качестве чисел может привести к тому, что программы будут выполняться без ошибок, но при этом будут выдавать неправильные результаты. Например, рассмотрим, что произойдёт, если в строке 6 заменить SQUARE_INIT=1 на SQUARE_INIT=^X210. Если ассемблировать, скомпоновать и запустить на выполнение такую программу, то она благополучно завершится без выдачи сообщений об ошибке. Однако значения переменных SQUARE_1, SQUARE_2 и SQUARE_3 будут совершенно неверными. Пусть программист, используя число как адрес, случайно сгенерировал допустимый адрес. Программа пытается произвести выборку длинного слова, расположенного по абсолютному адресу ^X0210. Так как программа начинается с адреса ^X0200, то искомое длинное слово размещается на ^X10 байт дальше от начала программы и содержит ^X01D00000. Первые четыре шестнадцатеричные цифры ^X01D0 - это первые байты машинного кода инструкции (см. строку 18 распечатки), а последние четыре цифры ^X0000 - это 16-битовая маска, сгенерированная в строке 17.
Отладка ассемблерных программ более сложна, чем отладка программ на языках высокого уровня. Иногда программист затрудняется найти ошибку, и поэтому приходится проверять ассемблерную программу символ за символом. Особенно осторожно следует обходиться с символами, вызывающими генерацию спецификаторов операндов, например со знаком #.
< НАЗАД | ОГЛАВЛЕНИЕ | ВПЕРЁД > |
[1] Адрес, с которого начинается выполнение программы. - Прим. ред.
[2] Символические имена, выбранные как из поля кода операции, так и из поля операндов, ассемблер VAX-11 MACRO пытается прежде найти по таблице имён, определяемых пользователем. Если в ней отсутствует данное символическое имя, то ассемблер просматривает таблицу постоянных имён. Такой порядок обращения к таблицам позволяет пользователям переопределять символические имена, описанные в таблице постоянных имён.
[3] Ассемблер разрешает программисту явно переопределять некоторые правила, принимаемые в языке ассемблера по умолчанию. В данном случае программист может задать смещение в формате байта, слова или длинного слова для относительной адресации с помощью префиксов В^ W^ и L^, помещаемых перед операндом. В частности, в инструкции ADDB3 В^X, W^Y, L^Z ассемблеру даётся указание, что для относительной адресации X, Y и Z надо использовать смещение в формате байта, слова и длинного слова соответственно.
[4] Допустимы пробелы и символы табуляции между именем и двоеточием. Это позволяет программисту при желании выравнивать двоеточия в разных строках.