Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
тип книга1.doc
Скачиваний:
2
Добавлен:
01.03.2025
Размер:
1.52 Mб
Скачать

Знакомство с ассемблером

Трансляторы можно разделить на две группы в зависимости от отношения меж­ду входным и выходным языками. Если входной язык является символическим представлением числового машинного языка, то транслятор называется ассемб­лером, а входной язык — языком ассемблера, или просто ассемблером. Если входной язык является языком высокого уровня (например, Java или С), а вы­ходной язык является либо числовым машинным языком, либо символическим представлением последнего, то транслятор называется компилятором.

Понятие ассемблера

Язык ассемблера — это язык, в котором каждый оператор соответствует ровно одной машинной команде. Иными словами, в программе, написанной на ассемб­лере, существует взаимно однозначное соответствие между машинными коман­дами и операторами. Если каждая строка в ассемблерной программе содержит ровно один оператор, и каждое машинное слово содержит ровно одну команду, то из ассемблерной программы размером в п строк получится программа на ма­шинном языке из п слов.

Мы программируем на языке ассемблера, а не на машинном языке (в шестна­дцатеричной системе счисления), поскольку это гораздо проще. Использовать символические имена и адреса вместо двоичных и восьмеричных намного удоб­нее. Многие могут запомнить, что обозначениями для сложения (add), вычита­ния (subtract), умножения (multiply) и деления (divide) служат команды ADD, SUB, MUL и DIV, но мало кто сможет запомнить соответствующие числовые величины, которые использует для этих команд машина. Программисту, пишущему на язы­ке ассемблера, нужно знать только символические названия, поскольку ассемб­лер транслирует их в машинные команды.

Это утверждение касается и адресов. Программист, пишущий на языке ас­семблера, может давать символические имена ячейкам памяти, и уже ассемблер должен позаботиться о том, чтобы получить из них правильные числовые значе­ния. В то же время программисту, пишущему на машинном языке, всегда прихо­дится работать с числовыми значениями адресов. Сейчас уже нет программи­стов, пишущих программы на машинном языке, хотя несколько десятилетий назад до изобретения ассемблеров программы именно так и писались.

Язык ассемблера имеет несколько особенностей, отличающих его от языков высокого уровня. Во-первых, это взаимно однозначное соответствие между опе­раторами языка ассемблера и машинными командами (об этом мы уже говори­ли). Во-вторых, программист, пишущий на ассемблере, имеет доступ ко всем объектам и командам целевой машины. У программистов, пишущих на языках высокого уровня, такого доступа нет. Например, если целевая машина содержит бит переполнения, ассемблерная программа может проверить его, a Java-npo- грамма — нет. Ассемблерная программа может выполнить любую команду из на­бора команд целевой машины, а программа на языке высокого уровня — нет. Ко­роче говоря, все, что можно сделать на машинном языке, можно сделать и на ассемблере, но в то же время программистам, пишущим программы на языках высокого уровня, недоступны многие команды, регистры и другие объекты. Язы­ки для системного программирования (например, С) часто имеют некое проме­жуточное положение. Они, хотя и обладают синтаксисом, присущим языкам вы­сокого уровня, с точки зрения возможностей доступа ближе к ассемблеру.

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

Назначение ассемблера

Работать с языком ассемблера непросто. Написание одной и той же программы на ассемблере занимает гораздо больше времени, чем на языке высокого уровня. Кроме того, очень много времени занимает отладка.

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

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

Первая причина (достижение высокой производительности) является более важной, поэтому мы рассмотрим ее подробнее. В большинстве программ лишь небольшой процент всего кода сказывается на времени выполнения программы. Обычно 1 % кода отвечает за 50 % времени выполнения, а 10 % кода — за 90 % времени выполнения.

Предположим, что для написания программы на языке высокого уровня требу­ется 10 человеко-лет, а полученной программе нужно 100 секунд, чтобы выполнить некоторую типичную контрольную программу. (Контрольной называют тесто­вую программу, которая используется для сравнения компьютеров, компилято­ров и т. п.) Написание всей программы на ассемблере может занять 50 челове­ко-лет. Полученная в результате контрольная программа будет выполняться примерно 33 секунды, поскольку хороший программист может оказаться в три раза умнее компилятора (хотя об этом можно спорить бесконечно). Ситуацию иллюстрирует табл. 7.1.

Таблица 7.1. Сравнение программирования на ассемблере

и языке высокого уровня

Время программирования, человеко-лет

Время выполнения программы, с

Язык ассемблера 50

33

Язык высокого уровня 10

100

Смешанный подход до подстройки

Критические 10 % 1

90

Остальные 90 % 9

10

Всего 10

100

Смешанный подход после подстройки

Критические 10 % 6

30

Остальные 90 % 9

10

Всего 15

40

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

Предположим, что 10 % программы отвечает за 90 % времени ее выполнения. Это значит, что из 100 секунд работы 90 секунд выполняется десятая часть про­граммы, а 10 секунд — оставшиеся 90 %. Эти 10 % программы можно усовершен­ствовать, переписав на ассемблере. Этот процесс называется подстройкой (tuning). На подстройку основных процедур потребуется еще 5 человеко-лет, но время выполнения программы сократится с 90 до 30 секунд.

Сравним этот смешанный подход, когда используются и ассемблер, и язык высокого уровня, с подходом, в котором применяется только язык ассемблера (см. табл. 7.1). При втором подходе программа работает примерно на 20 % быст­рее (33 секунды против 40), но более чем за тройную цену (50 человеко-лет про­тив 15). Более того, у смешанного подхода есть еще одно преимущество: гораздо проще переписать на ассемблер уже отлаженную процедуру, написанную на язы­ке высокого уровня, чем писать эту процедуру на ассемблере «с нуля». Отметим, что, если бы написание программы занимало ровно 1 год, соотношение между смешанным подходом и подходом, при котором используется только язык ас­семблера, составляло бы 4:1 в пользу смешанного подхода.

В то же время программисту, пишущему на языке высокого уровня, не нужно задумываться о перемещении отдельных битов, поэтому он может осмысливать задачу в целом, и иногда ему так удается построить программу, что он добивает­ся реального повышения производительности. Такая ситуация обычно не харак­терна для программистов, пишущих на ассемблере, — как правило, они возятся с отдельными командами, пытаясь сэкономить несколько циклов.

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

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

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

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

Формат оператора в ассемблере

Хотя структура ассемблерного оператора отражает структуру соответствующей машинной команды, языки ассемблера для разных машин и разных уровней во многом похожи, что позволяет говорить о языке ассемблера в целом. В листин­гах 7.1-7.3 показаны фрагменты программ на ассемблерах Pentium 4, Motorola 680x0 и (Ultra)SPARC. Все эти программы выполняют вычисление формулы N = I + J. Во всех трех примерах операторы над пустой строкой выполняют вы­числение, а операторы под пустой строкой резервируют память для переменных I, J и N. То есть последние операторы не являются символьными представления­ми машинных команд.

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

Pentium 4, все сказанное применимо и к процессорам 386, 486, Pentium и Pen­tium Pro. Для процессора SPARC мы будем использовать ассемблер Sun, а все сказанное применимо и к более ранним 32-разрядным версиям. В книге коды операций и регистры всегда обозначаются прописными буквами, причем не только для ассемблера Pentium 4, как это обычно принято, но и для ассемблера Sun, где по соглашению буквы строчные.


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

В каждом из трех примеров есть метки: FORMULA, I, J и N. Отметим, что в ассемблере SPARC после каждой метки нужно ставить двоеточие, а в ассемб­лере Motorola — нет. В ассемблере компьютеров Intel двоеточие ставится только после меток команд, но не после меток данных. Данное различие вовсе не явля­ется чем-то фундаментальным, просто у разработчиков разных ассемблеров раз­ные вкусы. Архитектура машины никак не влияет на тот или иной выбор. Един­ственное достоинство двоеточия состоит в том, что метку можно написать на отдельной строке, а код операции — на следующей строке с тем же отступом, что и метка. Без двоеточия компилятору невозможно было бы отличить метку от ко­да операции при их размещении в отдельных строках.

В некоторых ассемблерах длина метки ограничена значением 6 или 8 симво­лов. В то же время в большинстве языков высокого уровня длина имен произ­вольна. Длинные и хорошо подобранные имена упрощают чтение и понимание программы.

В каждой машине есть несколько регистров, но названия у них совершенно разные. Регистры Pentium 4 называются ЕАХ, ЕВХ, ЕСХ и т. д., регистры Moto­rola — DO, DI, D2, регистры SPARC имеют несколько названий. В этой книге для их обозначения мы будем использовать символы Ш и %R2.

В поле кода операции содержится либо символическая аббревиатура этого кода (если оператор является символическим представлением машинной команды), либо команда самого ассемблера. Выбор имени — дело вкуса, и поэтому разные разработчики называют их по-разному. Разработчики ассемблера Intel решили использовать обозначение MOV и для загрузки регистра из памяти и сохранения регистра в память, разработчики ассемблера Motorola выбрали для обеих опера­ций обозначение MOVE, а разработчики ассемблера SPARC решили использовать символы LD для первой операции и ST для второй. Очевидно, что выбор названий в данном случае никак не связан с архитектурой машины.

Напротив, необходимость указывать две машинные команды для доступа к памяти объясняется архитектурой SPARC, поскольку виртуальные адреса мо­гут быть 32-разрядными (как в SPARC версии 8) и 44-разрядными (как в SPARC версии 9), а команды могут содержать максимум 22 бита данных. Следо­вательно, чтобы передать все биты полного виртуального адреса, всегда требуют­ся две команды. Например:

SETHI %HI(I).%R1

Эта команда обнуляет старшие 32 бита и младшие 10 бит 64-разрядного реги­стра R1, а затем помещает старшие 22 бита 32-разрядного адреса переменной I в битовые позиции с 10 по 31 регистра R1. Далее:

LD[%R1+%L0(I)].R1

Эта команда складывает R1 и младшие 10 бит адреса I (в результате получа­ется полный адрес I), вызывает данное слово из памяти и помещает его в ре­гистр R1. Указанные команды, прямо скажем, не слишком элегантны, однако разработчики SPARC не гнались за красотой. Перед ними была поставлена зада­ча обеспечить высокую скорость исполнения, и они успешно ее решили.

Процессоры семейства Pentium, 680x0 и SPARC позволяют работать с опе­рандами разной длины (размером с байт, слово и длинное слово). Каким обра­зом ассемблер определяет, какова длина операндов? И опять разработчики раз­ных ассемблеров приняли разные решения. В Pentium 4 регистры разной длины имеют разные названия. Так, для перемещения 32-разрядных значений исполь­зуется название ЕАХ, для 16-разрядных — АХ, а для 8-разрядных — AL и АН. Разработчики ассемблера Motorola решили прибавлять к каждому коду опера­ции суффикс .L для типа long, .W — для типа word и .В — для типа byte. В SPARC для операндов разной длины используются разные коды операций, например, для загрузки байта, полуслова и слова в 64-разрядный регистр указываются ко­ды операций LDSB, LDSH и LDSW соответственно. Как видите, природа языков совер­шенно произвольна.

Три ассемблера, которые мы рассматриваем, различаются способом резерви­рования пространства для данных. Разработчики ассемблера Intel выбрали для этой операции название DD (Define Double — определить двойное слово), посколь­ку слово процессора 8088 имело длину 8 бит. В Motorola используется аббревиа­тура DC (Define Constant — определить константу). Разработчики SPARC с само­го начала предпочли название .WORD. И снова различия абсолютно случайны.

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

В поле комментариев поясняются действия программы. Эти пояснения могут пригодиться программистам, которым потом придется использовать и дорабаты­вать чужую программу, а также самому автору программы, когда он через год вернется к работе над ней. Ассемблерная программа без таких комментариев — нечто совершенно невразумительное (даже для ее автора). Комментарии могут быть полезны только людям и никак не влияют на работу программы.

Директивы

Ассемблерная программа определяет не только машинные команды, которые нужно выполнять процессору, но и команды, которые нужно выполнять самому ассемблеру (например, выделить немного памяти или выдать новую страницу листинга). Команды для ассемблера называются псевдокомандами, или ассемб­лерными директивами. В листинге 7.1 мы уже встречали типичную псевдоко­манду DD. В табл. 7.2 перечислены некоторые другие псевдокоманды (директи­вы). Они взяты из ассемблера MASM семейства ассемблеров Intel.

Таблица 7.2. Некоторые директивы ассемблера MASM

Директива

Описание

SEGMENT

Начало нового сегмента (текста, данных и т. п.) с определенными атрибутами

ENDS

Завершение текущего сегмента

ALIGN

Управление выравниванием следующей команды или данных

EQU

Определение нового символа, равного данному выражению

DB

Выделение памяти для одного или нескольких байтов

DW

Выделение памяти для одного или нескольких 16-разрядных полуслов

DD

Выделение памяти для одного или нескольких 32-разрядных слов

Директива

Описание

DQ

Выделение памяти для одного или нескольких 64-разрядных двойных слов

PROC

Начало процедуры

ENDP

Завершение процедуры

MACRO

Начало макроса

ENDM

Завершение макроса

PUBLIC

Экспорт имени, определенного в данном модуле

EXTERN

Импорт имени из другого модуля

INCLUDE

Вызов другого файла и включение его в текущий файл

IF

Начало условного ассемблирования программы на основе данного выражения

ELSE

Начало условного ассемблирования программы, если условие для директивы IF не выполнено

ENDIF

Завершение условного ассемблирования программы

COMMENT

Определение нового символа начала поля комментариев

PAGE

Принудительный разрыв страницы в листинге

END

Завершение ассемблерной программы

Директива SEGMENT начинает новый сегмент, а директива ENDS завершает его. Разрешается начинать текстовый сегмент, затем начинать сегмент данных, затем переходить обратно к текстовому сегменту и т. д.

Директива ALIGN передает следующую строку (обычно данные) по адресу, за­данному аргументом директивы. Например, если текущий сегмент уже содержит 61 байт данных, тогда после выполнения директивы ALIGN 4 следующим выделяе­мым адресом будет адрес 64.

Директива EQU дает символическое название некоторому выражению. Напри­мер, после следующей директивы символ BASE можно использовать в программе вместо значения 1000:

BASE EQU 1000

Выражение, которое следует за директивой EQU, может содержать несколько символов, соединенных знаками арифметических и других операций, напри­мер:

LIMIT EQU 4 * BASE + 2000

Большинство ассемблеров, в том числе MASM, требуют, чтобы символ был определен в программе до его появления в таком выражении, как это.

Директивы DB, DD, DW и DQ выделяют память для одной или нескольких пере­менных размером 1, 2, 4 и 8 байт соответственно. Например:

TABLE DB 11, 23, 49

Эта директива выделяет место для 3 байт и присваивает им начальные значе­ния И, 23 и 49 соответственно, кроме того, она определяет символ TABLE, равный тому адресу, по которому хранится значение И.

Директивы PR0C и 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 4). Если в начале и в конце машинно-зависимого кода вставить директивы IF и ENDIF, а затем изменить единственное определение, WORDSIZE, про­грамму можно автоматически настроить на один из двух размеров. Применяя та­кой подход, можно задействовать одну такую исходную программу для несколь­ких разных машин. В большинстве случаев все машинно-зависимые определения, такие как WORDSIZE, сохраняются в одном файле, причем для разных машин долж­ны быть разные файлы. Путем включения файла с нужными определениями про­грамму можно легко перекомпилировать для разных машин.

Директива COMMENT позволяет пользователю заменить символ начала коммен­тария (точку с запятой) чем-либо другим. Директива PAGE используется для управления листингом программы. Наконец, директивой END помечается конец программы.

В ассемблере MASM есть еще много директив. Другие ассемблеры Pentium 4 содержат другой набор директив, поскольку эти директивы определяются не ар­хитектурой машины, а вкусами разработчиков ассемблера.