- •Э. Таненбаум
- •Глава 2. Организация компьютерных систем 56
- •Глава 3. Цифровой логический уровень 139
- •Глава 4. Микроархитектурный уровень 230
- •Глава 5. Уровень архитектуры команд 334
- •Глава 6. Уровень операционной системы 437
- •Глава 7. Уровень языка ассемблера 517
- •Глава 8. Архитектуры компьютеров параллельного
- •Глава 9. Библиография 647
- •Глава 8 (архитектура компьютеров параллельного действия) полностью изменена. В ней подробно описываются мультипроцессоры (uma, numa и сома) и мультикомпьютеры (мрр и cow).
- •Глава 1
- •Глава 2 знакомит читателей с основными компонентами компьютера: процессорами, памятью, устройствами ввода-вывода. В ней дается краткое описание системной архитектуры и введение к следующим главам.
- •Глава 2
- •Центральный процессор Центральный процессор
- •12 Битов б
- •24 Входные линии
- •50 Входных линий
- •Глава 4
- •Старший бит
- •Блок выборки команд
- •Сигналы управления
- •Глава 5
- •Intel ia-64
- •Глава 6
- •Глава 7
- •3. Сведения о том, можно ли получить доступ к символу извне процедуры.
- •Глава 8
- •64 Элемента на каждый регистр
- •Intel/Sandia Option Red
- •00 Процессор 2
- •Глава 9
- •4. Mazidi and Mazidi, The 80x86ibm pc and Compatible Computers, 2nd ed.
- •5. McKee et al., Smarter Memory: ImprovingBandwidthforStreamed References.
- •4. McKusick et al., Design and Implementation ofthe 4.4bsd Operating System.
- •3. Hill, Multiprocessors Should Support Simple Memory-Consistency Models.
- •Ieee Scalable Coherent Interface Working Group, ieee, 1989.
- •Ieee Micro Magazine, vol. 18, p. 60-75, July/Aug. 1998b.
- •3Rd ed., Reading, ma: Addison-Wesley, 1998.
- •1988 Int'l. Conf. On Parallel Proc. (Vol. 11), ieee, p. 94-101, 1988.
- •Implementation of the 4.4 bsd Operating System», Reading, ma: Addison-Wesley, 1996.
- •In Shared Memory Multiprocessing», ieee Computer Magazine, vol. 30, p. 4450, Dec. 1997.
- •78Jan.-March 1999.
- •0 123456789Abcdef
- •I и Ijmii him
- •Э. Таненбаум
Глава 7
Уровень языка ассемблера
В четвертой, пятой и шестой главах мы обсуждали три уровня, которые имеются в большинстве современных компьютеров. В этой главе речь пойдет о еще одном уровне, который также присутствует практически во всех современных машинах.
Это уровень языка ассемблера. Уровень языка ассемблера существенно отличается от трех предыдущих, поскольку он реализуется с помощью трансляции, а не с помощью интерпретации.
Программы, которые преобразуют пользовательские программы, написанные на каком-либо определенном языке, в другой язык, называются трансляторами. Язык, на котором изначально написана программа, называется входным языком, а язык, на который транслируется эта программа, называется выходным языком. Входной язык и выходной язык определяют уровни. Если имеется процессор, который может выполнять программы, написанные на входном языке, то нет необходимости транслировать исходную программу на другой язык.
Трансляция используется в том случае, если есть аппаратный или программный процессор для выходного языка и нет процессора для входного языка. Если трансляция выполнена правильно, то оттранслированная программа будет давать точно такие же результаты, что и исходная программа (если бы существовал подходящий для нее процессор). Следовательно, можно организовать новый уровень, который сначала будет транслировать программы на выходной уровень, а затем выполнять полученные программы.
Важно понимать разницу между трансляцией и интерпретацией1. При трансляции исходная программа на входном языке не выполняется сразу. Сначала она преобразуется в эквивалентную программу, так называемую объектную программу, или исполняемую двоичную программу, которая выполняется только после завершения трансляции. При трансляции нужно пройти следующие два шага:
Создание эквивалентной программы на выходном языке.
Выполнение полученной программы.
Эти два шага выполняются не одновременно. Второй шаг начинается только после завершения первого. В интерпретации есть только один шаг: выполнение
исходной программы. Никакой эквивалентной программы порождать не нужно,
1
В отечественной литературе принято и
интерпретацию, и компиляцию (именно
компиляцию автор здесь называет
трансляцией) называть трансляцией.
Другими словами, трансляторы могут
быть либо компиляторами, либо
интерпретаторами. — Примеч.
научн. ред.
f
Во время выполнения объектной программы задействовано только три уровня:
микроархитектурный уровень, уровень команд и уровень операционной системы.
Следовательно, во время работы программы в памяти компьютера можно найти три программы: пользовательскую объектную программу, операционную систему и микропрограмму (если она есть). Никаких следов исходной программы не остается. Таким образом, число уровней, присутствующих при выполнении программы, может отличаться от числа уровней, присутствующих до трансляции. Следует отметить, что хотя мы определяем уровень по командам и языковым конструкциям, доступным программистам этого уровня (а не по технологии реализации), некоторые авторы иногда проводят различие между уровнями, реализованными интерпретаторами, и уровнями, реализованными при трансляции.
Введение в язык ассемблера
Трансляторы можно разделить на две группы в зависимости от отношения между входным и выходным языком. Если входной язык является символической репрезентацией числового машинного языка, то транслятор называется ассемблером, а входной язык называется языком ассемблера. Если входной язык является языком высокого уровня (например, Java или С), а выходной язык является либо
числовым машинным языком, либо символической репрезентацией последнего,
то транслятор называется компилятором.
Что такое язык ассемблера?
Язык ассемблера — это язык, в котором каждое высказывание соответствует ровно одной машинной команде. Иными словами, существует взаимно однозначное соответствие между машинными командами и операторами в программе на языке ассемблера. Если каждая строка в программе на языке ассемблера содержит ровно один оператор и каждое машинное слово содержит ровно одну команду, то программа на языке ассемблера в п строк произведет программу на машинном языке из п слов.
Мы используем язык ассемблера, а не программируем на машинном языке (в шестнадцатеричной системе счисления), поскольку на языке ассемблера программировать гораздо проще. Использовать символьные имена и адреса вместо двоичных и восьмеричных намного удобнее. Многие могут запомнить, что обозначениями для сложения (add), вычитания (subtract), умножения (multiply) и деления (divide) служат команды JDJD, SUE, ML и DIV, но мало кто может запомнить соответствующие числа, которые использует машина. Программисту на языке ассемблера нужно знать только символические названия, поскольку ассемблер транслирует их в машинные команды.
Это утверждение касается и адресов. Программист на языке ассемблера может дать имена ячейкам памяти, и уже ассемблер должен будет выдавать правильные числа. Программист на машинном языке всегда должен работать с числовыми номерами адресов. Сейчас уже нет программистов, которые пишут программы на
машинном языке, хотя несколько десятилетий назад до изобретения ассемблеров
программы именно так и писались.
Язык ассемблера имеет несколько особенностей, отличающих его от языков высокого уровня. Во-первых, это взаимно однозначное соответствие между высказываниями языка ассемблера и машинными командами (об этом мы уже говорили). Во-вторых, программист на языке ассемблера имеет доступ ко всем объектам и командам, присутствующим на целевой машине. У программистов на языках высокого уровня такого доступа нет. Например, если целевая машина содержит бит переполнения, программа на языке ассемблера может проверить его, а программа на языке Java не может. Программа на языке ассемблера может выполнить любую команду из набора команд целевой машины, а программа на языке высокого уровня не может. Короче говоря, все, что можно сделать в машинном языке, можно сделать и на языке ассемблера, но многие команды, регистры и другие объекты недоступны для программиста, пишущего программы на языке высокого уровня. Языки для системного программирования (например С) часто занимают промежуточное положение. Они обладают синтаксисом языка высокого уровня, но при этом с точки зрения возможностей доступа ближе к языку ассемблера.
Наконец, программа на языке ассемблера может работать только на компьютерах одного семейства, а программа, написанная на языке высокого уровня, потенциально может работать на разных машинах. Возможность переносить программное обеспечение с одной машины на другую очень важна для многих прикладных программ.
Зачем нужен язык ассемблера?
Язык ассемблера довольно труден. Написание программы на языке ассемблера
занимает гораздо больше времени, чем написание той же программы на языке высокого уровня. Кроме того, очень много времени занимает отладка.
Но зачем же тогда вообще писать программы на языке ассемблера? Есть две причины: производительность и доступ к машине. Во-первых, профессиональный программист языка ассемблера может составить гораздо меньшую по размеру программу, которая будет работать гораздо быстрее, чем программа, написанная на языке высокого уровня. Для некоторых программ скорость и размер весьма важны. Многие встроенные прикладные программы, например программы в кредитных карточках, сотовых телефонах, драйверах устройств, а также процедуры BIOS попадают в эту категорию.
Во-вторых, некоторым процедурам требуется полный доступ к аппаратному обеспечению, что обычно невозможно сделать на языке высокого уровня. В эту категорию попадают прерывания и обработчики прерываний в операционных системах, а также контроллеры устройств во встроенных системах, работающих в режиме реального времени.
Первая причина (достижение высокой производительности) является более важной, поэтому мы рассмотрим ее подробнее. В большинстве программ лишь
небольшой процент всего кода отвечает за большой процент времени выполнения программы. Обычно 1% программы отвечает за 50% времени выполнения, а 10% программы отвечает за 90% времени выполнения.
Предположим, что для написания программы на языке высокого уровня требуется 10 человеко-лет и что полученной программе требуется 100 секунд, чтобы выполнить некоторую типичную контрольную задачу. (Контрольная задача — это программа проверки, которая используется для сравнения компьютеров, компиляторов и т. п.). Написание всей программы на языке ассемблера может занять 50 человеко-лет. Полученная в результате программа будет выполнять контрольную задачу примерно за 33 секунды, поскольку хороший программист может оказаться в три раза умнее компилятора (хотя об этом можно спорить бесконечно). Ситуация проиллюстрирована в табл. 7.1.
Так как только крошечная часть программы отвечает за большую часть времени выполнения этой программы, возможен другой подход. Сначала программа пишется на языке высокого уровня. Затем проводится ряд измерений, чтобы определить, какие части программы отвечают за большую часть времени выполнения. Для таких измерений обычно используется системный тактовый генератор. С его помощью можно узнать, сколько времени затрачивается на каждую процедуру, сколько раз выполняется каждый цикл и т. п.
Предположим, что 10% программы отвечает за 90% времени ее выполнения. Это значит, что из всех 100 секунд работы 90 секунд проводится в этих 10%, а 10 секунд — в оставшихся 90% программы. Эти 10% программы можно усовершенствовать, переписав их на язык ассемблера. Этот процесс называется настройкой (tuning). Он проиллюстрирован в табл. 7.1. На переделку основных процедур потребуется еще 5 лет, но время выполнения программы сократится с 90 секунд до 30 секунд.
Таблица 7.1. Сравнение программирования на языке ассемблера и на языке высокого уровня(с настройкой и без настройки)
Количество человеко-лет, Время выполнения затрачиваемых программы в секундах
на написание программы
Язык ассемблера |
50 |
33 |
Язык высокого уровня |
10 |
100 |
Смешанный подход до настройки |
|
|
Критические 10% |
1 |
90 |
Остальные 90% |
9 |
10 |
Всего |
10 |
100 |
Смешанный подход после настройки |
|
|
Критические 10% |
6 |
30 |
Остальные 90% |
9 |
10 |
Всего |
15 |
40 |
Сравним этот смешанный подход, в котором используется и язык ассемблера,
и язык высокого уровня, с подходом, в котором применяется только язык ассемблера (табл. 7.1). При втором подходе программа работает примерно на 20% быстрее (33 секунды против 40 секунд), но более чем за тройную цену (50 человеко-лет против 15). Более того, у смешанного подхода есть еще одно преимущество: гораздо проще переделать в код ассемблера уже отлаженную процедуру, написанную на языке высокого уровня, чем писать процедуру на языке ассемблера с нуля.
Отметим, что если бы написание программы занимало только 1 год, соотношение между смешанным подходом и подходом, при котором используется только язык ассемблера, составляло бы 4:1 в пользу смешанного подхода.
Программист, который использует язык высокого уровня, не занят перемещением битов и может так решить задачу, так построить программу, что в конце концов достигнет действительно большого увеличения производительности. А программисты, пишущие программы на языке ассемблера, обычно стараются так построить команды, чтобы сэкономить несколько циклов, поэтому у них такой ситуации возникнуть не может.
Расскажем о двух экспериментах, проведенных во время разработки системы MULTICS. Грехем [49] описал процедуру PL/I, за три месяца переделанную в новую версию, которая была в 26 раз меньше и работала в 50 раз быстрее, чем исходная. Он описал еще одну процедуру PL/L, которая получилась в 20 раз меньше исходной и работала в 40 раз быстрее, чем исходная, после двух месяцев работы. Кор-бато [27] описал процедуру PL/I, размер кода которой был сокращен с 50 000 слов до 1000 слов менее чем за месяц, а контроллер уменьшен с 65 000 до 30 000 слов с увеличением производительности в 8 раз за 4 месяца. Здесь важно понимать, что у программистов языков высокого уровня глобальный подход к тому, что они делают, поэтому они гораздо быстрее могут разработать лучший алгоритм.
Однако, несмотря на все это, существует по крайней мере 4 веские причины для изучения языка ассемблера. Во-первых, желательно уметь писать программы на языке ассемблера, поскольку успех или неудача большого проекта может зависеть от того, можно ли повысить производительность какой-то важной процедуры в 2 или 3 раза.
Во-вторых, язык ассемблера может быть единственным возможным выходом из-за недостатка памяти. Кредитные карты, например, содержат центральный процессор, но у них нет мегабайта памяти и жесткого диска. Однако они должны выполнять сложные вычисления при наличии ограниченных ресурсов. Процессоры, встроенные в электроприборы, часто имеют минимальное количество памяти, поскольку они должны быть достаточно дешевыми. Различные электронные устройства, работающие на батарейках, обычно содержат очень маленькую память, поэтому здесь тоже нужен эффективный код.
В-третьих, компилятор должен либо давать на выходе программу, которая используется ассемблером, либо самостоятельно выполнять процесс ассемблирования. Таким образом, понимание языка ассемблера существенно для понимания того, как работает компьютер. И вообще, кто-то ведь должен писать компилятор
и его ассемблер.
Наконец, изучение языка ассемблера дает прекрасное представление о реальной машине. Для тех, кто изучает архитектуру компьютеров, написание программы на языке ассемблера — единственный способ узнать, что собой представляет
машина на архитектурном уровне.
Формат оператора в языке ассемблера
Хотя структура оператора в языке ассемблера отражает структуру соответствующей машинной команды, языки ассемблера для разных машин и разных уровней во многом сходны друг с другом, что позволяет говорить о языке ассемблера вооб
ще. В таблицах 7.2-7.4 показаны фрагменты программ на языке ассемблера для Pentium II, Motorola 680x0 и (Ultra)SPARC. Все эти программы выполняют вычисление N=I+J. Во всех трех примерах операторы над пропуском в таблице выполняют вычисление. Операторы под пропуском — это указания ассемблеру зарезервировать память для переменных I, J и N. Последние не являются символьными репрезентациями машинных команд.
1:
J: N:
.WORD3 .WORD 4 .WORD 0
3
4
0
резервируем 4 байта и устанавливаем знач. 3 резервируем 4 байта и устанавливаем знач. 4 резервируем 4 байта и устанавливаем знач. 0
Для компьютеров семейства Intel существует несколько ассемблеров, которые отличаются друг от друга по синтаксису. В этой книге мы будем использовать язык ассемблера Microsoft MASM. Мы будем говорить о процессоре Pentium II, но все, что мы будем обсуждать, применимо и к процессорам 386,486, Pentium и Pentium Pro. Для процессора SPARC мы будем использовать ассемблер Sun. Все это также применимо к более ранним 32-битным версиям. В книге коды операций и регистры всегда обозначаются прописными буквами, причем не только в ассемблере для
Pentium II, как это обычно принято, но и в ассемблере Sun, где по соглашению
используются строчные буквы.
Высказывания языка ассемблера состоят из четырех полей: поля метки, поля
операции, поля операндов и поля комментариев. Метки используются для того, чтобы обеспечить символические имена для адресов памяти. Они нужны для того, чтобы можно было совершить переход к командам. Они также нужны для слов с данными, чтобы по символическому имени можно было получить доступ к тому месту, где они хранятся. Если высказывание снабжено меткой, то эта метка обычно располагается в колонке 1.
В каждом из трех примеров есть 4 метки: FORMULA, I, J и N. Отметим, что в языках ассемблера для SPARC после каждой метки нужно ставить двоеточие, а для Motorola — нет. В компьютерах Intel двоеточия ставятся только после меток команд, но не после меток данных. Данное различие вовсе не является фундаментальным. Разработчики разных ассемблеров имеют разные вкусы. Архитектура
машины никак не определяет тот или иной выбор. Единственное преимущество двоеточия состоит в том, что метку можно писать на отдельной строке, а код операции — на следующей строке в колонке 1. Это упрощает работу компилятора: без двоеточия нельзя было бы отличить метку на отдельной строке от кода операции на отдельной строке.
В некоторых ассемблерах длина метки ограничена до 6 или 8 символов. А в большинстве языков высокого уровня длина имен произвольна. Длинные и хорошо подобранные имена упрощают чтение и понимание программы другими людьми.
В каждой машине содержится несколько регистров, но всем им даны совершенно разные названия. Регистры в Pentium II называются ЕАХ, ЕВХ, ЕСХ и т. д. Регистры в Motorola называются DO, Dl, D2. Регистры в машине SPARC имеют несколько названий. Здесь для их обозначения мы будем использовать %R1 и %R2.
В поле кода операции содержится либо символическая аббревиатура этого кода (если высказывание является символической репрезентацией машинной команды), либо команда для самого ассемблера. Выбор имени — дело вкуса, и поэтому разные разработчики языков ассемблера называют их по-разному. Разработчики ассемблера Intel решили использовать обозначение МУ и для загрузки регистра из
памяти, и для сохранения регистра в память. Разработчики ассемблера Motorola
выбрали обозначение M3E для обеих операций. А разработчики ассемблера SPARC решили использовать LD для первой операции и ST для второй. Очевидно, что выбор названий в данном случае никак не связан с архитектурой машины.
Напротив, необходимость использовать две машинные команды для доступа к памяти объясняется устройством архитектуры SPARC, поскольку виртуальные адреса могут быть 32-битными (как в SPARC Version 8) и 44-битными (как в SPARC
Version 9), а команды могут содержать максимум 22 бита данных. Следовательно, чтобы передать все биты полного виртуального адреса, всегда требуется две команды. Команда Sethi янкп.та
обнуляет старшие 32 бита и младшие 10 битов 64-битного регистра R1, а затем помещает старшие 22 бита 32-битного адреса переменной I в регистр R1 в битовые позиции с 10 по 31. Следующая команда юсш+шхш.да
складывает R1 и младшие 10 битов адреса I (в результате чего получается полный адрес I), вызывает данное слово из памяти и помещает его в регистр R1.
Процессоры семейства Pentium, 680x0 и SPARC — все допускают операнды
разной длины (типа byte (байт), word (слово) и long). Каким образом ассемблер
определит, какую длину использовать? И опять разработчики ассемблера приняли разные решения. В Pentium II регистры разной длины имеют разные названия. Так, для перемещения 32-битных элементов используется название ЕАХ, для 16-битных — АХ, а для 8-битных — AL и АН. Разработчики ассемблера Motorola решили прибавлять к каждому коду операции суффикс .L для типа long, .W — для типа word и .В для типа byte. В SPARC для операндов разной длины используются разные коды операций (например, для загрузки байта, полуслова (halfword) и слова в 64-битный регистр используются коды операций LDSB, ПШ и IDW соответственно). Как видите, разработка языка произвольна.
Три ассемблера, которые мы рассматриваем, различаются по способу резервирования пространства для данных. Разработчики языка ассемблера для Intel выбрали DW (Define Word — определить слово). Позднее был введен альтернативный вариант ЖЗЮ В Motorola используется DC (Define Constant — определить константу). Разработчики SPARC с самого начала предпочли ЖЗЮ И слова различия произвольны.
В поле операндов определяются адреса и регистры, которые являются операндами для машинной команды. В поле операндов команды целочисленного сложения сообщается, что и к чему нужно прибавить. Поле операндов команд перехода определяет, куда нужно совершить переход. Операндами могут быть регистры,
константы, ячейки памяти и т. д.
В поле комментариев приводятся пояснения о действиях программы. Они могут понадобиться программистам, которые будут использовать и переделывать чужую программу, или программисту, который изначально писал программу и возвратился к работе над ней через год. Программа на ассемблере без таких комментариев совершенно непонятна программистам (даже автору этой программы). Комментарии нужны только человеку. Они никак не влияют на работу программы.
Директивы
Программа на языке ассемблера должна не только определять, какие машинные команды нужно выполнить, но и содержать команды, которые должен выполнять сам ассемблер (например, потребовать от него определить местонахождение какой-либо сохраненной информации или выдать новую страницу листинга). Команды для ассемблера называются псевдокомандами или директивами ассемблера. Мы уже видели одну типичную псевдокоманду DW (см. табл. 7.2). В табл. 7.5 приведены некоторые другие псевдокоманды (директивы). Они взяты из ассемблера MASM для семейства Intel.
Таблица 7.5. Некоторые директивы ассемблера MASM Директива Значение
SEGMENT |
Начинает новый сегмент (текста, данных и т.п.) с определенными атрибутами |
ENDS |
Завершает текущий сегмент |
ALIGN |
Контролирует выравнивание следующей команды или данных |
EQU |
Определяет новый символ, равный данному выражению |
DB |
Выделяет память для одного или нескольких байтов |
DD |
Выделяет память для одного или нескольких 16-битных полуслов |
DW |
Выделяет память для одного или нескольких 32-битных слов |
DQ |
Выделяет память для одного или нескольких 64-битных двойных слов |
PROC |
Начинает процедуру |
ENDP |
Завершает процедуру |
MACRO |
Начинает макроопределение |
ENDM |
Завершает макроопределение |
PUBLIC |
Экспортирует имя, определенное в данном модуле |
EXTERN |
Импортирует имя из другого модуля |
INCLUDE |
Вызывает другой файл и включает его в текущий файл |
IF |
Начинает условную компоновку программы на основе данного выражения |
ELSE |
Начинает условную компоновку программы, если условие IF над директивой |
|
не выполнено |
ENDIF |
Завершает условную компоновку программы |
COMMENT |
Определяет новый отделитель комментариев |
PAGE |
Совершает принудительный обрыв страницы в листинге |
END |
Завершает программу ассемблирования |
Директива SKMENT начинает новый сегмент, а директива ENDS завершает его. Разрешается начинать текстовый сегмент, затем начинать сегмент данных, затем переходить обратно к текстовому сегменту и т. д.
Директива ALKN переводит следующую строку (обычно данные) в адрес, который делим на аргумент данной директивы. Например, если текущий сегмент уже содержит 61 байт данных, тогда следующим адресом после ALKN 4 будет адрес 64.
Директива EQU дает символическое название некоторому выражению. Например, после записи
BASE EQU 1000
символ BASE можно использовать вместо 1000. Выражение, которое следует за EQU, может содержать несколько символов, соединенных арифметическими и другими операторами, например:
LIMIT EQU 4 * BASE + 2000
Большинство ассемблеров, в том числе MASM, требуют, чтобы символ был определен в программе до появления в некотором выражении.
Сяод'юта;1\&4дафек\'таът^В,т vA ръетсределэдэт тосштсъ для одшл там. нескольких переменных размером 1, 2,4 и 8 байтов соответственно. Например, TABLE D8 11. 23. 49
выделяет пространство для 3 байтов и присваивает им начальные значения 11, 23 и 49 соответственно. Эта директива, кроме того, определяет символ TABLE, равный тому адресу, где хранится число 11.
Директивы EROC и ENCP определяют начало и конец процедур языка ассемблера. Процедуры в языке ассемблера выполняют ту же функцию, что и в языках программирования высокого уровня. Директивы MOO и EMM определяют начало и конец макроса. О макросах мы будем говорить ниже.
Далее идут директивы PUBLIC и EXTERN. Программы часто пишут в виде совокупности файлов. Часто процедуре, находящейся в одном файле, нужно вызвать процедуру или получить доступ к данным, определенным в другом файле. Чтобы такие отсылки между файлами стали возможными, обозначение (имя), которое нужно сделать доступным для других файлов, экспортируется с помощью директивы PUBLIC. Чтобы ассемблер не ругался по поводу использования символа, который не определен в данном файле, этот символ может быть объявлен внешним (EXTERN), это сообщит ассемблеру, что символ определен в каком-то другом файле. Символы, которые не определены ни в одной из этих директив, используются только в пределах одного файла. Поэтому даже если символ F00 используется в нескольких файлах, это не вызовет никакого конфликта, поскольку этот символ локален по отношению к каждому файлу.
Директива jNCXLDE приказывает ассемблеру вызвать другой файл и включить его в текущий файл. Такие включенные файлы часто содержат определения, макросы и другие элементы, необходимые для разных файлов.
Многие языки ассемблера, в том числе MASM, поддерживают условную компоновку программы. Например, программа
WORDSIZE EQU 16 IF WORDSIZE GT 16 WSIZE: DW32 ELSE
WSIZE: DW 16
ENDIF
выделяет в памяти одно 32-битное слово и вызывает его адрес WSTZE Этому слову придается одно из значений: либо 32, либо 16 в зависимости от значения WCKDSZE (в данном случае 16). Такая конструкция может использоваться в программах для 16-битных машин (как 8088) или для 32-битных машин (как Pentium II). Если в начале и в конце машинозависимого кода поставить IF и ENDIF, а затем изменить одно определение, WORDSIZE, программу можно автоматически установить на один из двух размеров. Применяя такой подход, можно сохранять одну такую исходную программу для нескольких разных машин. В большинстве случаев все машинозависимые определения, такие как WORDSIZE, сохраняются в одном файле, причем для разных машин должны быть разные файлы. Путем включения файла с нужными определениями программу можно легко перекомпилировать на разные машины.
Директива COMMENT позволяет пользователю изменять символ комментария на что-либо отличное от точки с запятой. Директива PGE используется для управления листингом программы. Наконец, директива END отмечает конец программы.
В ассемблере MASM есть еще много директив. Другие ассемблеры для Pentium II содержат другой набор директив, поскольку они определяются не в соответствии с архитектурой машины, а по желанию разработчиков ассемблера.
Макросы
Программистам на языке ассемблера часто приходится повторять одни и те же цепочки команд по несколько раз. Проще всего писать нужные команды всякий раз, когда они требуются. Но если последовательность достаточно длинная или если ее нужно повторять очень много раз, то это становится утомительным.
Альтернативный подход — оформить эту последовательность в процедуру и вызывать ее в случае необходимости. У такой стратегии тоже есть свои недостатки, поскольку в этом случае каждый раз придется выполнять специальную команду вызова процедуры и команду возврата. Если последовательности команд короткие (например, всего две команды), но используются часто, то вызов процедуры может сильно снизить скорость работы программы. Макросы являются простым
и эффективным решением этой проблемы.
Макроопределение, макровызов и макрорасширение
Макроопределение — это способ дать имя куску текста. После того как макрос
был определен, программист может вместо куска программы писать имя макроса. В сущности, макрос — это обозначение куска текста. В листинге 7.1 приведена программа на языке ассемблера для Pentium II, которая дважды меняет местами содержимое переменных р и q. Эти последовательности команд можно определить как макросы (листинг 7.2). После определения макроса каждое имя SWP в программе замещается следующими четырьмя строками:
MOV EAX.P MOV EBX.Q
MOV Q.EAX MOV P.EBX
Программист определил SWP как обозначение для этих четырех операторов. Хотя разные языки ассемблера используют немного разные записи для определения макросов, все они состоят из одних и тех же базовых частей:
Заголовок макроса, в котором дается имя определяемого макроса.
Текст, в котором приводится тело макроса.
Директива, которая завершает определение (например, ENDM)
Когда ассемблер наталкивается на макроопределение в программе, он сохраняет его в таблице макроопределений для последующего использования. Всякий раз, когда в программе в качестве кода операции появляется макрос (в нашем примере 5ЛЙЕ), ассемблер замещает его телом макроса. Использование имени макроса в качестве кода операции называется макровызовом, а его замещение телом макроса называется макрорасширением.
Листинг 7.1 . Код на языке ассемблера, в котором переменные р и q дважды
меняются местами (без использования макроса)
MOV |
EAX.P |
MOV |
EBX.Q |
MOV |
Q.EAX |
MOV |
Р.ЕВХ |
MOV |
EAX.P |
MOV |
EBX.Q |
MOV |
Q.EAX |
MOV |
P.EBX |
Листинг 7.2. Тот же код с использованием макроса
SWAP MACRO
MOV EAX.P
MOV EBX.Q MOV Q.EAX MOV P.EBX
ENDM SWAP SWAP
Макрорасширение происходит во время процесса ассемблирования, а не во время выполнения программы. Этот момент очень важен. Программы, приведенные в листингах 7.1 и 7.2, произведут один и тот же машинный код. По программе на машинном языке невозможно определить, использовались ли макросы при ее порождении. После завершения макрорасширения ассемблер отбрасывает макрорасширения. В полученной программе никаких признаков макросов не остается.
Макровызовы не следует путать с вызовами процедур. Основное различие состоит в том, что макровызов — это команда ассемблеру заменить имя макроса телом макроса. Вызов процедуры — это машинная команда, которая вставлена в объектную программу и которая позднее будет выполнена, чтобы вызвать процедуру. В табл. 7.6 сравниваются макровызовы и вызовы процедур.
Таблица 7.6. Сравнение макровызовов и вызовов процедур
Макровызов
Вызов процедуры
Когда совершается вызов программы?
Во время ассемблирования Во время выполнения
Вставляетсяли тело макроса
или процедуры в объектную программу
каждый раз, когда совершается вызов?
Да
Нет
Команда вызова процедуры вставляется в объектную программу, а затем выполняется?
Нет
Да
Нужно ли после вызова использовать команду возврата?
Нет
Да
Сколько копий тела макровызова или процедуры появляется в объектной программе?
Однанамакровызов
Можно считать, что процесс ассемблирования осуществляется в два прохода. На первом проходе сохраняются все макроопределения, а макровызовы расширяются. На втором проходе обрабатывается полученный в результате текст. Иными словами, исходная программа считывается, а затем трансформируется в другую программу, из которой удалены все макроопределения и в которой каждый макровызов замещен телом макроса. Полученная программа без макросов затем поступает в ассемблер.
Важно иметь в виду, что программа представляет собой цепочку символов. Это могут быть буквы, цифры, пробелы, знаки пунктуации и «возврат каретки» (переход на новую строку). Макрорасширение состоит в замене определенных подцепочек из этой цепочки другими цепочками. Макросредства — это способ манипулирования цепочками символов безотносительно их значений.
Макросы с параметрами
Макросредства, описанные ранее, можно использовать для сокращения программ, в которых часто повторяется точно одна и та же последовательность команд. Однако очень часто программа содержит несколько похожих, но не идентичных последовательностей команд (листинг 7.3). Здесь первая последовательность меняет
местами Р и Q, а вторая последовательность меняет местами R и S.
Листинг 7.3. Почти идентичные последовательности команд без использования макроса
MOV EAX.P MOV EBX.Q MOV Q.EAX MOV P.EBX
MOV EAX.R
MOV EBX.S MOV S.EAX MOV R.EBX
Листинг 7.4. Те же последовательности с использованием макроса CHANGE MACRO P1.P2
MOV EAX.P1
MOV EBX.P2
MOV P2.EAX MOV Pl.EBX
ENDM
CHANGE P.Q CHANGE R.S
Для работы с такими почти идентичными последовательностями предусмотрены макроопределения, которые обеспечивают формальные параметры, и макровызовы, которые обеспечивают фактические параметры. Когда макрос расширяется, каждый формальный параметр, который появляется в теле макроса, замещается соответствующим фактическим параметром. Фактические параметры помещаются в поле операндов макровызова. В листинге 7.4. представлена программа из листинга 7.3, в которую включен макрос с двумя параметрами. Символы Р1 и Р2 — это формальные параметры. Во время расширения макроса каждый символ Р1 внутри тела макроса замещается первым фактическим параметром, а символ Р2 замещается вторым фактическим параметром. В макровызове
CHANGE P.Q
Р — это первый фактический параметр, a Q — это второй фактический параметр. Таким образом, программы в листингах 7.3 и 7.4 идентичны.
Расширенные возможности
Большинство макропроцессоров содержат целый ряд расширенный особенностей, которые упрощают работу программиста на языке ассемблера. В этом разделе мы
рассмотрим несколько расширенных особенностей MASM. Во всех ассемблерах есть одна проблема: дублирование меток. Предположим, что макрос содержит команду условного перехода и метку, к которой совершается переход. Если макрос
вызывается два и более раз, метка будет дублироваться, что вызовет ошибку. Поэтому программист должен приписывать каждому вызову в качестве параметра отдельную метку. Другое решение (оно применяется в MASM) — объявлять метку локальной (LOCAL), при этом ассемблер автоматически будет порождать другую метку при каждом расширении макроса. В некоторых ассемблерах номерные метки автоматически считаются локальными.
MASM и большинство других ассемблеров позволяют определять макросы
внутри других макросов. Эта особенность очень полезна в сочетании с условной компоновкой программы. Обычно один и тот же макрос определятся в обеих частях оператора IF:
Ml MACRO
IF WORDSIZE GT 16 M2 MACRO
ENDM ELSE
M2 MACRO
ENDM
ENDIF
tNDM
В любом случае макрос М2 б> дет определен, но определение зависит от того, на какой машине ассемблируется программа: на 16-битной или на 32-битной. Если Ml не вызывается, макрос М2 вообще не будет определен.
Наконец, одни макросы могут вызывать другие макросы, в том числе самих себя. Если макрос рекурсивный, то есть вызывает самого себя, он должен передавать самому себе параметр, который изменяется при каждом расширении, а также проверять этот параметр и завершать рекурсию, когда параметр достигает определенного значения. В противном случае получится бесконечный цикл.
Реализация макросредств в ассемблере
Для реализации макросов ассемблер должен уметь выполнять две функции: сохранять макроопределения и расширять макровызовы. Мы рассмотрим эти функции по очереди.
Ассемблер должен сохранять таблицу всех имен макросов, в которой каждое имя сопровождается указателем на определение этого макроса, чтобы его можно
было получить в случае необходимости. В одних ассемблерах предусмотрена отдельная таблица для имен макросов, а другие содержат общую таблицу, в которой находятся не только имена макросов, но и все машинные команды и директивы.
Когда встречается макроопределение, создается новый элемент таблицы с именем макроса, числом параметров и указателем на другую таблицу — таблицу макроопределений, где будет храниться тело макроса. Список формальных параметров
тоже создается в это время. Затем считывается тело макроса и сохраняется в таблице макроопределений. Формальные параметры, которые встречаются в теле
цикла, указываются специальным символом.
Ниже приведен пример внутреннего представления макроса CHANGE. В качестве символа возврата каретки используется точка с запятой, а в качестве символа формального параметра — амперсант.
MOV EAX.&P1;MOV EBX.&P2:M0V &P2EAX;M0V &P1.EBX:
В таблице макроопределений тело макроса представляет собой просто цепочку символов.
Во время первого прохода ассемблирования отыскиваются коды операций, а
макросы расширяются. Всякий раз, когда встречается макроопределение, оно сохраняется в таблице макросов. При вызове макроса ассемблер временно приостанавливает чтение входных данных из входного устройства и начинает считывать сохраненное тело макроса. Формальные параметры, извлеченные из тела макроса, замещаются фактическими параметрами, которые предоставляются вызовом. Амперсант перед параметрами позволяет ассемблеру узнавать их.
Процесс ассемблирования
В следующих разделах мы опишем, как работает ассемблер. И хотя на каждой машине есть свой определенный ассемблер, отличный от других, процесс ассемблирования по сути один и тот же.
Двухпроходной ассемблер
Поскольку программа на языке ассемблера состоит из ряда операторов, на первый взгляд может показаться, что ассемблер сначала должен читать оператор, затем транслировать его на маигинньш язык и, наконец, переносить полученный машинный язык в файл, а соответствующий кусок листинга — в другой файл. Этот процесс будет повторяться до тех пор, пока вся программа не будет оттранслирована. Но, к сожалению, такая стратегия не работает.
Рассмотрим ситуацию, где первый оператор — переход к L. Ассемблер не может ассемблировать это оператор, пока не будет знать адрес L. L может находиться где-нибудь в конце программы, и тогда ассемблер не сможет найти этот адрес, не
прочитав всю программу. Эта проблема называется проблемой опережающей ссылки, поскольку символ L используется еще до того, как он определен (то есть было сделано обращение к символу, определение которого появится позднее).
Опережающие ссылки можно разрешать двумя способами. Во-первых, ассемблер действительно может прочитать программу дважды. Каждое прочтение исходной программы называется проходом, а транслятор, который читает исходную программу дважды, называется двухпроходным транслятором. На первом проходе собираются и сохраняются в таблице все определения символов, в том числе метки. К тому времени как начнется второй проход, значения символов уже известны, поэтому никакой опережающей ссылки не будет, и каждый оператор можно читать и ассемблировать. При этом требуется дополнительный проход по исходной программе, но зато такая стратегия относительно проста.
При втором подходе программа на языке ассемблера читается один раз и преобразуется в промежуточную форму, и эта промежуточная форма сохраняется
в таблице в памяти. Затем совершает второй проход, но уже не по исходной программе, а по таблице. Если физической памяти (или виртуальной памяти) достаточно для этого подхода, то будет сэкономлено время, затрачиваемое на процесс ввода-вывода. Если требуется вывести листинг, тогда нужно сохранить полностью исходное выражение, включая комментарии. Если листинг не нужен, то промежуточную форму можно сократить, оставив только голые команды.
Еще одна задача первого прохода — сохранить все макроопределения и расширить вызовы по мере их появления. Следовательно, в одном проходе происходит и определение символов, и расширение макросов.
Первый проход
Главная функция первого прохода — построить таблицу символов, в которой содержатся значения всех имен. Символом может быть либо метка, либо значение, которому с помощью директивы приписывается определенное имя:
BUFSIZE EQU 8192
Приписывая значение символьному имени в поле метки команды, ассемблер должен знать, какой адрес будет иметь эта команда во время выполнения программы. Для этого ассемблер во время процесса ассемблирования сохраняет счетчик адреса команд (ILC — Instruction Location Counter) (специальную переменную). Эта переменная устанавливается на 0 в начале первого прохода и увеличивается после каждой обработанной команды на длину этой команды (табл. 7.7.). Пример написан для Pentium П. Мы не будем давать примеры для SPARC и Motorola, поскольку различия между языками ассемблера не очень важны и одного примера
будет достаточно. Кроме того, язык ассемблера для SPARC неудобочитаем. При первом проходе в большинстве ассемблеров используется по крайней мере
3 таблицы: таблица символьных имен, таблица директив и таблица кодов операций. В случае необходимости используется еще литеральная таблица. Таблица символьных имен содержит один элемент для каждого имени, как показано в табл. 7.8.
Символьные имена либо используются в качестве меток, либо явным образом
определяются (например, с помощью EQU В каждом элементе таблицы символьных
имен содержится само имя (или указатель на него), его численное значение и иногда некоторая дополнительная информация. Она может включать:
Длину поля данных, связанного с символом.
Биты перераспределения памяти (которые показывают, изменяется ли значение символа, если программа загружается не в том адресе, в котором предполагал ассемблер).