
Structured Computer Organization (Архитектура компьютера) / computer_organization_2003
.pdf

5 2 4 Глава 7. Уровень языка ассемблера
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, LDSH и LDSW соответственно). Как видите, разработка языка произвольна.
Три ассемблера, которые мы рассматриваем, различаются по способу резервирования пространства для данных. Разработчики языка ассемблера для Intel выбрали DW (Define Word — определить слово). Позднее был введен альтернативный вариант .WORD. В Motorola используется DC (Define Constant — определить константу). Разработчики SPARC с самого начала предпочли .WORD. И слова различия произвольны.
Вполе операндов определяются адреса и регистры, которые являются операндами для машинной команды. В поле операндов команды целочисленного сложения сообщается, что и к чему нужно прибавить. Поле операндов команд перехода определяет, куда нужно совершить переход. Операндами могут быть регистры, константы, ячейки памяти и т. д.
Вполе комментариев приводятся пояснения о действиях программы. Они могут понадобиться программистам, которые будут использовать и переделывать чужую программу, или программисту, который изначально писал программу и возвратился к работе над ней через год. Программа на ассемблере без таких комментариев совершенно непонятна программистам (даже автору этой программы). Комментарии нужны только человеку. Они никак не влияют на работу программы.
Директивы
Программа на языке ассемблера должна не только определять, какие машинные команды нужно выполнить, но и содержать команды, которые должен выполнять сам ассемблер (например, потребовать от него определить местонахождение какой-либо сохраненной информации или выдать новую страницу листинга). Команды для ассемблера называются псевдокомандами или директивами ассем-


5 2 6 Глава 7. Уровень языка ассемблера
В,Ш v^ ръетсределэдэт тосштсъ для одшлтам.нескольких переменных размером 1, 2,4 и 8 байтов соответственно. Например,
TABLE D8 11 . 23 . 49
выделяет пространство для 3 байтов и присваивает им начальные значения 11, 23 и 49 соответственно. Эта директива, кроме того, определяет символ TABLE, равный тому адресу, где хранится число 11.
Директивы PROC и ENDP определяют начало и конец процедур языка ассемблера. Процедуры в языке ассемблера выполняют ту же функцию, что и в языках программирования высокого уровня. Директивы MACRO и ENDM определяют начало и конец макроса. О макросах мы будем говорить ниже.
Далее идут директивы PUBLIC и EXTERN. Программы часто пишут в виде совокупности файлов. Часто процедуре, находящейся в одном файле, нужно вызвать процедуру или получить доступ к данным, определенным в другом файле. Чтобы такие отсылки между файлами стали возможными, обозначение (имя), которое нужно сделать доступным для других файлов, экспортируется с помощью директивы PUBLIC. Чтобы ассемблер не ругался по поводу использования символа, который не определен в данном файле, этот символ может быть объявлен внешним (EXTERN), это сообщит ассемблеру, что символ определен в каком-то другом файле. Символы, которые не определены ни в одной из этих директив, используются только в пределах одного файла. Поэтому даже если символ F00 используется в нескольких файлах, это не вызовет никакого конфликта, поскольку этот символ локален по отношению к каждому файлу.
Директива INCLUDE приказывает ассемблеру вызвать другой файл и включить его в текущий файл. Такие включенные файлы часто содержат определения, макросы и другие элементы, необходимые для разных файлов.
Многие языки ассемблера, в том числе MASM, поддерживают условную компоновку программы. Например, программа
WORDSIZE EQU 16
IF WORDSIZE GT 16 WSIZE: DW32 ELSE
WSIZE: DW 16 ENDIF
выделяет в памяти одно 32-битное слово и вызывает его адрес WSIZE. Этому слову придается одно из значений: либо 32, либо 16 в зависимости от значения WORDSIZE (в данном случае 16). Такая конструкция может использоваться в программах для 16-битных машин (как 8088) или для 32-битных машин (как Pentium II). Если в начале и в конце машинозависимого кода поставить IF и ENDIF, а затем изменить одно определение, WORDSIZE, программу можно автоматически установить на один из двух размеров. Применяя такой подход, можно сохранять одну такую исходную программу для нескольких разных машин. В большинстве случаев все машинозависимые определения, такие как WORDSIZE, сохраняются в одном файле, причем для разных машин должны быть разные файлы. Путем включения файла с нужными определениями программу можно легко перекомпилировать на разные машины.
Директива COMMENT позволяет пользователю изменять символ комментария на что-либо отличное от точки с запятой. Директива PAGE используется для управления листингом программы. Наконец, директива END отмечает конец программы.

Макросы 527
В ассемблере MASM есть еще много директив. Другие ассемблеры для Pentium II содержат другой набор директив, поскольку они определяются не в соответствии с архитектурой машины, а по желанию разработчиков ассемблера.
Макросы
Программистам на языке ассемблера часто приходится повторять одни и те же цепочки команд по несколько раз. Проще всего писать нужные команды всякий раз, когда они требуются. Но если последовательность достаточно длинная или если ее нужно повторять очень много раз, то это становится утомительным.
Альтернативный подход — оформить эту последовательность в процедуру и вызывать ее в случае необходимости. У такой стратегии тоже есть свои недостатки, поскольку в этом случае каждый раз придется выполнять специальную команду вызова процедуры и команду возврата. Если последовательности команд короткие (например, всего две команды), но используются часто, то вызов процедуры может сильно снизить скорость работы программы. Макросы являются простым и эффективным решением этой проблемы.
Макроопределение, макровызов и макрорасширение
Макроопределение — это способ дать имя куску текста. После того как макрос был определен, программист может вместо куска программы писать имя макроса. В сущности, макрос — это обозначение куска текста. В листинге 7.1 приведена программа на языке ассемблера для Pentium II, которая дважды меняет местами содержимое переменных р и q. Эти последовательности команд можно определить как макросы (листинг 7.2). После определения макроса каждое имя SWAP в программе замещается следующими четырьмя строками:
MOV EAX.P
MOV EBX.Q
MOV Q.EAX
MOV P.EBX
Программист определил SWAP как обозначение для этих четырех операторов. Хотя разные языки ассемблера используют немного разные записи для опреде-
ления макросов, все они состоят из одних и тех же базовых частей:
1.Заголовок макроса, в котором дается имя определяемого макроса.
2.Текст, в котором приводится тело макроса.
3.Директива, которая завершает определение (например, ENDM).
Когда ассемблер наталкивается на макроопределение в программе, он сохраняет его в таблице макроопределений для последующего использования. Всякий раз, когда в программе в качестве кода операции появляется макрос (в нашем примере SWAP), ассемблер замещает его телом макроса. Использование имени макроса в качестве кода операции называется макровызовом, а его замещение телом макроса называется макрорасширением.

528 |
Глава7. Уровень языка ассемблера |
Листинг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. Сравнение макровызовов и вызовов процедур
|
Макровызов |
Вызов процедуры |
Когда совершается вызов |
Во время ассемблирования Во время выполнения |
|
программы? |
|
|
Вставляетсялителомакроса |
Да |
Нет |
илипроцедурывобъектнуюпрограмму |
|
|
каждыйраз,когдасовершаетсявызов? |
|
|
Команда вызова процедуры |
Нет |
Да |
вставляется в объектную программу, |
|
|
азатемвыполняется? |
|
|
Нужнолипослевызоваиспользовать |
Нет |
Да |
команду возврата? |
|
|
Сколькокопийтеламакровызова |
Однанамакровызов |
1 |
или процедуры появляется |
|
|
в объектной программе? |
|
|

Макросы 529
Можно считать, что процесс ассемблирования осуществляется в два прохода. На первом проходе сохраняются все макроопределения, а макровызовы расширяются. На втором проходе обрабатывается полученный в результате текст. Иными словами, исходная программа считывается, а затем трансформируется в другую программу, из которой удалены все макроопределения и в которой каждый макровызов замещен телом макроса. Полученная программа без макросов затем поступает в ассемблер.
Важно иметь в виду, что программа представляет собой цепочку символов. Это могут быть буквы, цифры, пробелы, знаки пунктуации и «возврат каретки» (переход на новую строку). Макрорасширение состоит в замене определенных подцепочек из этой цепочки другими цепочками. Макросредства — это способ манипулирования цепочками символов безотносительно их значений.
Макросы с параметрами
Макросредства, описанные ранее, можно использоватьдля сокращения программ, в которых часто повторяется точно одна и та же последовательность команд. Однако очень часто программа содержит несколько похожих, но не идентичных последовательностей команд (листинг 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


Процесс ассемблирования |
531 |
Ассемблер должен сохранять таблицу всех имен макросов, в которой каждое имя сопровождается указателем на определение этого макроса, чтобы его можно было получить в случае необходимости. В одних ассемблерах предусмотрена отдельная таблица для имен макросов, а другие содержат общую таблицу, в которой находятся не только имена макросов, но и все машинные команды и директивы.
Когда встречается макроопределение, создается новый элемент таблицы с именем макроса, числом параметров и указателем на другую таблицу — таблицу макроопределений, где будет храниться тело макроса. Список формальных параметров тоже создается в это время. Затем считывается тело макроса и сохраняется в таблице макроопределений. Формальные параметры, которые встречаются в теле цикла, указываются специальным символом.
Ниже приведен пример внутреннего представления макроса CHANGE. В качестве символа возврата каретки используется точка с запятой, а в качестве символа формального параметра — амперсант.
MOV EAX.&P1;MOV EBX.&P2:M0V &P2EAX;M0V &P1.EBX:
В таблице макроопределений тело макроса представляет собой просто цепочку символов.
Во время первого прохода ассемблирования отыскиваются коды операций, а макросы расширяются. Всякий раз, когда встречается макроопределение, оно сохраняется в таблице макросов. При вызове макроса ассемблер временно приостанавливает чтение входных данных из входного устройства и начинает считывать сохраненное тело макроса. Формальные параметры, извлеченные из тела макроса, замещаются фактическими параметрами, которые предоставляются вызовом. Амперсант перед параметрами позволяет ассемблеру узнавать их.
Процесс ассемблирования
В следующих разделах мы опишем, как работает ассемблер. И хотя на каждой машине есть свой определенный ассемблер, отличный от других, процесс ассемблирования по сути один и тот же.
Двухпроходной ассемблер
Поскольку программа на языке ассемблера состоит из ряда операторов, на первый взгляд может показаться, что ассемблер сначала должен читать оператор, затем транслировать его на машинный язык и, наконец, переносить полученный машинный язык в файл, а соответствующий кусок листинга — в другой файл. Этот процесс будет повторяться до тех пор, пока вся программа не будет оттранслирована. Но, к сожалению, такая стратегия не работает.
Рассмотрим ситуацию, где первый оператор — переход к L. Ассемблер не может ассемблировать это оператор, пока не будет знать адрес L. L может находиться где-нибудь в конце программы, и тогда ассемблер не сможет найти этот адрес, не прочитав всю программу. Эта проблема называется проблемой опережающей

5 3 2 Глава 7. Уровень языка ассемблера
ссылки, поскольку символ L используется еще до того, как он определен (то есть было сделано обращение к символу, определение которого появится позднее).
Опережающие ссылки можно разрешать двумя способами. Во-первых, ассемблер действительно может прочитать программу дважды. Каждое прочтение исходной программы называется проходом, а транслятор, который читает исходную программу дважды, называется двухпроходным транслятором. На первом проходе собираются и сохраняются в таблице все определения символов, в том числе метки. К тому времени как начнется второй проход, значения символов уже известны, поэтому никакой опережающей ссылки не будет, и каждый оператор можно читать и ассемблировать. При этом требуется дополнительный проход по исходной программе, но зато такая стратегия относительно проста.
При втором подходе программа на языке ассемблера читается один раз и преобразуется в промежуточную форму, и эта промежуточная форма сохраняется в таблице в памяти. Затем совершает второй проход, но уже не по исходной программе, а по таблице. Если физической памяти (или виртуальной памяти) достаточно для этого подхода, то будет сэкономлено время, затрачиваемое на процесс ввода-вывода. Если требуется вывести листинг, тогда нужно сохранить полностью исходное выражение, включая комментарии. Если листинг не нужен, то промежуточную форму можно сократить, оставив только голые команды.
Еще одна задача первого прохода — сохранить все макроопределения и расширить вызовы по мере их появления. Следовательно, в одном проходе происходит и определение символов, и расширение макросов.
Первый проход
Главная функция первого прохода — построить таблицу символов, в которой содержатся значения всех имен. Символом может быть либо метка, либо значение, которому с помощью директивы приписывается определенное имя:
BUFSIZE EQU 8192
Приписывая значение символьному имени в поле метки команды, ассемблер должен знать, какой адрес будет иметь эта команда во время выполнения программы. Для этого ассемблер во время процесса ассемблирования сохраняет счетчик адресакоманд (ILC —InstructionLocationCounter) (специальнуюпеременную). Эта переменная устанавливается на 0 в начале первого прохода и увеличивается после каждой обработанной команды на длину этой команды (табл. 7.7.). Пример написан для Pentium П. Мы не будем давать примеры для SPARC и Motorola, поскольку различия между языками ассемблера не очень важны и одного примера будет достаточно. Кроме того, язык ассемблера для SPARC неудобочитаем.
При первом проходе в большинстве ассемблеров используется по крайней мере 3 таблицы: таблица символьных имен, таблица директив и таблица кодов операций. В случае необходимости используется еще литеральная таблица. Таблица символьных имен содержит один элемент для каждого имени, как показано в табл. 7.8. Символьные имена либо используются в качестве меток, либо явным образом определяются (например, с помощью EQU). В каждом элементетаблицы символьных