Процесс ассемблирования
имен содержится само имя (или указатель на него), его численное значение и ин некоторая дополнительная информация. Она может включать:
1.Длину поля данных, связанного с символом.
2.Биты перераспределения памяти (которые показывают, изменяется ли чение символа, если программа загружается не в том адресе, в котором п полагал ассемблер).
3.Сведения о том, можно ли получить доступ к символу извне процедур
Таблица 7.7. Счетчик адреса команд используется для слежения за адресами ком В данном примере операторы до MARIA занимают 100 байтов
Метка |
Код операции |
Операнды |
Комментарии |
Длина |
Счетчик адреса ко |
MARIA: |
MOV |
EAX, I |
EAX=I |
5 |
100 |
|
MOV |
EBX,J |
EBX=J |
6 |
105 |
ROBERTA: |
MOV |
ECX,К |
ECX=K |
6 |
111 |
|
IMUL |
EAX, EAX |
EAX=I*I |
2 |
117 |
|
IMUL |
EBX, EBX |
EBX=J*J |
3 |
119 |
|
IMUL |
ECX,ECX |
ECX=K*K |
3 |
122 |
MARILYN: |
ADD |
EAX,EBX |
EAX=I*I+J*J |
2 |
125 |
|
ADD |
EAX,ECX |
EAX=I*I+J*J+K*K |
2 |
127 |
STEPHANY: JMP |
DONE |
Переход к DONE |
5 |
129 |
Таблица 7.8. Таблица символьных имен для программы из табл. 7.7.
Символьное имя |
Значение Прочая информация |
MARIA |
100 |
ROBERTA |
111 |
MARILYN |
125 |
STEPHANY |
129 |
В таблице кодов операций предусмотрен по крайней мере один элемент каждого символического кода операции в языке ассемблера (табл. 7.9). В каж элементе таблицы содержится символический код операции, два операнда, чи вое значение кода операции, длина команды и номер типа, по которому можно о делить, к какой группе относится код операции (коды операций делятся на г пы в зависимости от числа и типа операндов).
Таблица 7.9. Некоторые элементы таблицы кодов операций для ассемблера Penti
Код |
Первый |
Второй |
Шестнадцатеричный |
Длина |
Класс |
операции |
операнд |
операнд |
код |
команды |
команды |
5 3 4 Глава 7. Уровень языка ассемблера
Вкачестве примера рассмотрим код операции ADD. Если команда ADD в первого операнда содержит регистр ЕАХ, а в качестве второго — 32-битную к (immed32), то используется код операции 0x05, а длина команды составля тов. Если используется команда ADD с двумя регистрами в качестве опер длина команды составляет 2 байта, а код операции будет равен 0x01. Все к ции кодов операций и операндов, которые соответствуют данному прави отнесены к классу 19 и будут обрабатываться так же, как команда ADD регистрами в качестве операндов. Класс команд обозначает процедуру, вызывается для обработки всех команд данного типа.
Внекоторых ассемблерах можно писать команды с применением непос ной адресации, даже если соответствующей команды не существует в в языке. Такие команды с «псевдонепосредственными» адресами обрабат следующим образом. Ассемблер назначает участок памяти для непосредс операнда в конце программы и порождает команду, которая обращаетс Например, универсальная вычислительная машина IBM 3090 не имеет ком посредственными адресами. Тем не менее программист может написать
L 14.=F'5'
для загрузки в регистр 14 константы 5 размером в полное слово. Таким программисту не нужно писать директиву, чтобы разместить слово в пам дать ему значение 5, дать ему метку, а затем использовать эту метку в ко Константы, для которых ассемблер автоматически резервирует память, н ся литералами. Литералы упрощают читаемость и понимание программ значение константы очевидным в исходном операторе. При первом пр семблер должен создать таблицу из всех литералов, которые используют грамме. Все три компьютера, которые мы взяли в качестве примеров, команды с непосредственными адресами, поэтому их ассемблеры не обе ют литералы. Команды с непосредственными адресами в настоящее вре ются обычными, но раньше они рассматривались как нечто совершенно ное. Вероятно, широкое распространение литералов внушило разработч непосредственная адресация — это очень хорошая идея. Если нужны лит во время ассемблирования сохраняется таблица литералов, в которой по новый элемент всякий раз, когда встречается литерал. После первого про лица сортируется и продублированные элементы удаляются.
В листинге 7.5 показана процедура, которая лежит в основе первого ассемблера. Названия процедур были выбраны таким образом, чтобы б их суть. Листинг 7.5 представляет собой хорошую отправную точку для и Он достаточно короткий, он легок для понимания, и из него видно, каки быть следующий шаг — это написание процедур, которые используются листинге.
Листинг 7.5. Первый проход простого ассемблера
|
|
|
|
|
Процесс ассемблирования |
53 |
location_counter = 0; |
//ассемблирование первой команды в ячейке 0 |
|
|
|
imtialize_tables(), |
//общая инициализация |
|
|
|
|
|
|
|
while (more_input) { |
|
//more_input получает значение «ложь» с помощью END |
line = read_next_line(); |
//считывание строки |
|
|
|
|
|
|
length =0; |
|
|
//# байт в |
команде |
|
|
|
|
|
|
type =0. |
|
|
//тип команды |
|
|
|
|
|
|
|
if (line_isjiot_coniment(line)) { |
|
|
|
|
|
|
|
|
symbol = check_for_symbol(line), |
//Содержит ли строка метку? |
|
|
|
if (symbol |
!- null) |
|
|
//если да, |
то записывается |
символ и |
enter_new_symbol(symbol. 1ocation_counter), |
|
|
|
|
|
|
literal = check_for_literal(line). |
//Содержит ли строка литерал? |
|
|
if (literal |
!= null) |
|
|
//если да, |
то он |
вводится в |
таблицу |
enter_new_literal(1itera1); |
|
|
|
|
|
|
|
|
|
|
|
|
//Теперь определяем тип кода операции. |
|
|
|
|
|
//-1 значит недопустимый код операции. |
|
opcode = extract_opcode(line). |
//определяем место кода |
операции |
|
|
type =search_opcode_table(opcode). |
//находим формат, |
например. OP REG1.REG2 |
if (type < |
0) |
|
|
|
//Если это не |
код |
операции, |
являет |
|
|
|
|
//ли |
это директивой? |
|
|
|
|
|
type = search_pseudo_table(opcode). |
|
|
|
|
|
|
|
switch(type) { |
|
|
|
//определяем |
длину команды |
|
case l.length=get_length_of_typel (line), break, |
|
|
|
|
|
|
case 2 Iength=get_length_of_type2(line); break. |
|
|
|
|
|
|
|
|
|
|
//другие случаи |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
wnte_temp_file(type, opcode, length, line), |
//информация для второго прохода |
|
location_counter = |
location_counter + length, //обновление счетчика |
адреса команд |
if (type == END_STATEMENT) { |
|
|
//завершился ли |
ввод? |
|
|
morejinput - false. |
|
|
|
//если да. |
то |
выполняем |
служеб |
rewind_temp_for_pass_two(). |
|
|
//перематываем |
файл обратно |
|
sort_literal_table(). |
|
|
//сортируем |
таблицу литералов |
remove_redundant_literals(); |
|
|
//и удаляем из |
нее |
дубликаты |
Одни процедуры будут относительно короткими, например check_jor_symbo которая просто выдает соответствующее обозначение в виде цепочки символо если таковое имеется, и выдает ноль, если его нет. Другие процедуры, наприме get_length_of_type1 и get_length_ofjtype2, могут быть достаточно длинными и м гут сами вызывать другие процедуры. Естественно, на практике типов будет н два, а больше, и это будет зависеть от языка, который ассемблируется, и от тог сколько типов команд предусмотрено в этом языке.
Структурирование программ имеет и другие преимущества помимо простот программирования. Если ассемблер пишется группой людей, разнообразные пр цедуры могут быть разбиты на куски между программистами. Все подробност
получения входных данных спрятаны в процедуре read_next_line. Если эти детал
536 Глава 7. Уровень языка ассемблера
еетип (набор операндов) и вычислить длину команды. Эта информация бится при втором проходе, поэтому ее лучше записать, чтобы не анали строку во второй раз. Однако переписывание входного файла потребуе операций ввода-вывода. Что лучше — увеличить количество операций в вода, чтобы меньше времени тратить на анализ строк, или сократить ко операций ввода-вывода и потратить больше времени на анализ, зависит сти работы центрального процессора и диска, эффективности файловой и некоторых других факторов. В нашем примере мы запишем временн который будет содержать тип, код операции, длину и саму входную цепочк но это цепочка и будет считываться при втором проходе, и читать файл по разу будет не нужно.
После прочтения директивы END первый проход завершается. В это можно сохранить таблицу символьных имен и таблицу литералов, если ходимо. В таблице литералов можно произвести сортировку и удалить п рованные литералы.
Второй проход
Задача второго прохода — произвести объектную программу и напечата кол ассемблирования (если нужно). Кроме того, при втором проходе до водиться информация, необходимая компоновщику для связывания п которые ассемблировались в разное время, в один выполняемый файл. В ге 7.6 показана процедура для второго прохода.
Листинг 7.6. Второй проход простого ассемблера
public static void pass_two() |
{ |
//Эта процедура - второй проход ассемблера |
|
|
|
boolean morejnput = true: |
|
//флаг, |
который останавливает второй п |
String line, opcode; |
|
//поля команды |
int location_counter, length, type: //переменные |
|
final |
int END_STATEMENT = |
-2: |
//сигналы |
конца ввода |
final |
int MAX_CODE =16; |
|
//максимальное количество байтов в ком |
byte code[] = new byte[MAX_CODE]; |
//количество байтов в команде в порожденном |
location_counter = 0;
while (morejnput) { type = readj:ype():
opcode = read_opcode(); length = readJengthO; line = readJineO;
if (type != 0) { switch(type) {
//ассемблирование первой команды в а
//morejnput устанавливается на «ло //считывание поля типа следующей строки //считывание поля кода операции сле //считывание поля длины в следующей //считывание самой входной строки //тип 0 указывает на строки комм
//порождение выходного кода
case l:evalj:ypel(opcode, length, line, code): break;
case 2: eval_type2(opcode, length, line, code); break;
|
Процесс ассемблирования |
537 |
more_input = false; |
// если да, то выполняем служебные операции |
|
finishjjpO; |
// завершение |
|
}
Процедура второго прохода более или менее сходна с процедурой первого прохода: строки считываются по одной и обрабатываются тоже по одной. Поскольку мы записали в начале каждой строки тип, код операции и длину (во временном файле), все они считываются, и таким образом, нам не нужно проводить анализ строк во второй раз. Основная работа по порождению кода выполняется процедурами eval_type1, eval_type2 и т. д. Каждая из них обрабатывает определенную модель (например, код операции и два регистра-операнда). Полученный в результате двоичный код команды сохраняется в переменной code. Затем совершается контрольное считывание. Желательно, чтобы процедура write_code просто сохраняла в буфере накопленный двоичный код и записывала файл на диск большими порциями, чтобы сократить рабочую нагрузку на диск.
Исходный оператор и выходной (объектный) код, полученный из него (в шестнадцатеричной системе), можно напечатать или поместить в буфер, чтобы напечатать потом. После переустановки счетчика адреса команды вызывается следующий оператор.
До настоящего момента предполагалось, что исходная программа не содержит никаких ошибок. Но любой человек, который когда-нибудь писал программы на каком-либо языке, знает, насколько это предположение не соответствует действительности. Наиболее распространенные ошибки приведены ниже:
1.Используемый символ не определен.
2.Символ был определен более одного раза.
3.Имя в поле кода операции не является допустимым кодом операции.
4.Код операции не снабжен достаточным количеством операндов.
5.У кода операции слишком много операндов.
6.Восьмеричное число содержит 8 или 9.
7.Недопустимое применение регистра (например, переход к регистру).
8.Отсутствует оператор END.
Программисты весьма изобретательны по части новых ошибок. Ошибки с неопределенным символом часто возникают из-за опечаток. Хороший ассемблер может вычислить, какой из всех определенных символов в большей степени соответствует неопределенному, и подставить его. Для исправления других ошибок ничего кардинального предложить нельзя. Лучшее, что может сделать ассемблер при обнаружении оператора с ошибкой, — это вывести сообщение об ошибке на экран и попробовать продолжить процесс ассемблирования.
5 38 Глава 7. Уровень языка ассемблера
При применении любого из этих способов мы пытаемся смоделировать тивную память, которая представляет собой набор пар (символьное им ние). По имени ассоциативная память должна выдавать его значение.
Проще всего реализовать таблицу символьных имен в виде массива пар вый элемент является именем (или указателем на имя), а второй — значен указателем на него). Если нужно найти какой-нибудь символ, то таблица ных имен просто последовательно просматривается, пока не будет найдено ствие. Такой метод довольно легко запрограммировать, но он медленно раб скольку в среднем при каждом поиске придется просматривать половину
Другой способ организации — отсортировать таблицу по именам и дл имен использовать алгоритм двоичного поиска. В соответствии с этим мом средний элемент таблицы сравнивается с символьным именем. Есл имя по алфавиту идет раньше среднего элемента, значит, оно находится половине таблицы. Если символьное имя по алфавиту идет после сред мента, значит, оно находится во второй части таблицы. Если нужное имя ет со средним элементом, то поиск на этом завершается.
Предположим, что средний элемент таблицы не равен символу, кот ищем. Мы уже знаем, в какой половине таблицы он находится. Алгоритм д поиска можно применить к соответствующей половине. В результате мы ли чим совпадение, либо определим нужную четверть таблицы. Таким образо лице из п элементов нужный символ можно найти примерно за lo&n попы видно, что такой алгоритм работает быстрее, чем просто последовательный таблицы, но при этом элементы таблицы нужно сохранять в алфавитном
Совершенно другой подход — хэш-кодирование. Для этого подхода т хэш-функция, которая отображает символы (имена) в целые числа в про от 0 до к-1. Такой функцией может быть функция перемножения код всех символов в имени. Можно перемножить все коды ASCII символов с рованием переполнения, а затем взять значение по модулю к или раздели ченное значение на простое число. Фактически подойдет любая входная ф которая дает равномерное распределение значений.
Символьные имена можно хранить в таблице, состоящей из к участко к-1. Все пары (символьное имя, значение), в которых имя соответствует няются в связном списке, на который указывает слот i в хэш-таблице. Есл таблице содержится п символьных имен и к слотов, то в среднем длин будет n/k. Если мы выберем к, приблизительно равное п, то на нахождени го символьного имени в среднем потребуется всего один поиск. Путем к ровки к мы можем сократить размер таблицы, но при этом скорость пои зится. Хэш-код показан на рис. 7.1.
слированные процедуры. Если виртуальной памятинет, связанная программадолж на загружаться в основную память. Программы, которые выполняют эти функ ции, называются по-разному: компоновщиками, связывающими загрузчикам
и редакторами связей. Для полной трансляции исходной программы требуетс два шага, как показано на рис. 7.2:
1.Компиляция или ассемблирование исходных процедур.
2.Связывание объектных модулей.
|
|
|
|
|
Andy |
14025 |
0 |
|
|
|
|
|
|
|
Anton |
31253 |
4 |
|
|
|
|
|
|
|
Cathy |
65254 |
5 |
|
|
|
|
|
|
|
Dick |
54185 |
0 |
|
|
|
|
|
|
|
Erik |
47357 |
6 |
|
|
|
|
|
|
|
Frances |
56445 |
3 |
|
|
|
|
|
|
|
Frank |
14332 |
3 |
|
|
|
|
|
|
|
Gerrit |
32334 |
4 |
|
|
|
|
|
|
|
Hans |
44546 |
4 |
|
|
|
|
|
|
|
Henri |
75544 |
2 |
|
|
|
|
|
|
|
Jan |
17097 |
5 |
|
|
|
|
|
|
|
Jaco |
64533 |
6 |
|
|
|
|
|
|
|
Maarten |
23267 |
0 |
|
|
|
|
|
|
|
Reind |
63453 |
1 |
|
|
|
|
|
|
|
Roel |
76764 |
7 |
|
|
|
|
|
|
|
Willem |
34544 |
6 |
|
|
|
|
|
|
|
Wiebern |
34344 |
1 |
|
|
Хэш- |
|
|
|
|
|
|
|
|
|
|
|
таблица |
Связная таблица |
|
|
|
|
|
|
|
0 |
Andy |
| |
14025 |
| |
-Ы |
Maarten |
| |
23267 |
| |
4+-| Dick |
| 54185~ |
1 |
Reind |
| |
63453 |
|
|
Wiebern | |
34344 |
|
|
|
2 |
Henri |
|
75544 |
|
|
|
|
|
|
|
|
3 |
Frances |
| |
56445 |
| |
Ц->\ |
Frank |
| |
14332 |
|
|
|
4 |
Hans |
|
44546 |
|
|
Gerrit |
|
32334 |
|
Anton |
31253 |
5 |
Jan |
|
17097 |
|
* ^ H |
Cathy |
| |
65254 |
|
|
|
6 |
Jaco |
|
64533 |
|
|
Willem |
|
34544 |
|
Erjk |
"r7357 |
7 |
Roel |
|
76764 |
|
|
|
|
|
|
|
|
Рис. 7 . 1 . Хэш-кодирование: символьные имена, значения и хэш-коды, образованные
5 40 Глава 7. Уровень языка ассемблера
Трансляция исходной процедуры в объектном модуле — это переход уровень, поскольку исходный язык и выходной язык имеют разные кома пись. Однако при связывании перехода надругой уровень не происходит, программы на входе и на выходе компоновщика предназначены для од же виртуальной машины. Задача компоновщика — собрать все процеду рые транслировались раздельно, и связать их вместе, чтобы в результате исполняемый двоичный код. В системах MS-DOS, Windows 95/98 и N ные модули имеют расширение .obj, а исполняемые двоичные программ ширение .ехе. В системе UNIX объектные модули имеют расширение .о няемые двоичные программы не имеют расширения.
|
Исходная |
|
Объектный |
|
|
|
процедура1 |
|
модуль 1 |
|
|
|
Исходная |
|
Объектный |
|
Испо |
|
Транслятор |
Компоновщик |
дв |
|
процедура2 |
модуль 2 |
|
|
|
|
|
Исходная |
|
Объектный |
|
|
|
процедура3 |
|
модуль 3 |
|
|
Рис.7.2.Дляполученияисполняемойдвоичнойпрограммыизсовокупнос оттранслированных независимо друг от друга процедур используется компон
Компиляторы и ассемблеры транслируют каждую исходную процеду дельную единицу. На это есть веская причина. Если компилятор или считывал бы целый ряд исходных процедур и сразу переводил бы их программу на машинном языке, то при изменении одного оператора в процедуре потребовалось бы заново транслировать все исходные проце
Если каждая процедура транслируется по отдельности, как показано н то транслировать заново нужно будет только одну измененную проце понадобится заново связать все объектные модули. Однако связывание дит гораздо быстрее, чем трансляция, поэтому выполнение этих двух шаг ляции и связывания) сэкономит время при доработке программы. Это важно для программ, которые содержат сотни или тысячи модулей.
Задачи компоновщика
В начале первого прохода ассемблирования счетчик адреса команды уст ется на 0. Этот шаг эквивалентен предположению, что объектный модуль выполнения будет находиться в ячейке с адресом 0. На рис. 7.3 показан
Связываниеизагрузка 54
полняемой программы внутри компоновщика и разместить все объектные модул в соответствующих адресах. Если физической или виртуальной памяти не доста точно для формирования отображения, то можно использовать файл на диск Обычно небольшой раздел памяти, начинающийся с нулевого адреса, использует ся для векторов прерывания, взаимодействия с операционной системой, обнару жения неинициализированных указателей и других целей, поэтому программ обычно начинаются не с нулевого адреса, а выше. В нашем примере программ начинаются с адреса 100.
400
300
200
100
0
500
400
300
200
100
|
|
Объектный модуль В |
|
еии |
|
|
500 |
CALL С |
Объектный модуль А |
|
|
|
400 |
|
CALL В |
300 |
MOVE Q ТО X |
|
MOVE P ТО X |
200 |
|
|
100 |
|
BRANCH TO 200 |
0 |
BRANCH TO 300 |
Объектный модуль С |
|
|
CALLD |
|
|
|
|
Объектный модуль D |
|
300 |
|
MOVE R ТО X |
200 |
MOVE S ТО X |
|
100 |
|
BRANCH TO 200 |
|
BRANCH TO 200 |
Рис. 7.3. Каждый модуль имеет свое собственное адресное пространство, начинающееся с нуля
Посмотрите на рис. 7.4, а. Хотя программа уже загружена в отображение ис
полняемого двоичного файла, она еще не готова для выполнения. Посмотрим, чт