ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 24.12.2021
Просмотров: 12240
Скачиваний: 10

Процесс ассемблирования
5 3 3
имен содержится само имя (или указатель на него), его численное значение и иногда
некоторая дополнительная информация. Она может включать:
1. Длину поля данных, связанного с символом.
2. Биты перераспределения памяти (которые показывают, изменяется ли зна-
чение символа, если программа загружается не в том адресе, в котором пред-
полагал ассемблер).
3. Сведения о том, можно ли получить доступ к символу извне процедуры.
Таблица 7.7.
Счетчик адреса команд используется для слежения за адресами команд.
В данном примере операторы до MARIA занимают 100 байтов
Метка Код операции Операнды Комментарии Длина Счетчик адреса команд
MARIA:
ROBERTA:
MARILYN:
STEPHANY:
MOV
MOV
MOV
IMUL
IMUL
IMUL
ADD
ADD
JMP
EAX, I
EBX, J
ECX, К
EAX, EAX
EBX, EBX
ECX, ECX
EAX, EBX
EAX, ECX
DONE
EAX=I
EBX=J
ECX=K
EAX=I*I
EBX=J*J
ECX=K*K
EAX=I*I+J*J
EAX=I*I+J*J+K*K
Переход к DONE
5
6
6
2
3
3
2
2
5
100
105
111
117
119
122
125
127
129
Таблица 7.8.
Таблица символьных имен для программы из табл. 7.7.
Символьное имя Значение Прочая информация
MARIA 100
ROBERTA 111
MARILYN 125
STEPHANY 129
В таблице кодов операций предусмотрен по крайней мере один элемент для
каждого символического кода операции в языке ассемблера (табл. 7.9). В каждом
элементе таблицы содержится символический код операции, два операнда, число-
вое значение кода операции, длина команды и номер типа, по которому можно опре-
делить, к какой группе относится код операции (коды операций делятся на груп-
пы в зависимости от числа и типа операндов).
Таблица 7.9.
Некоторые элементы таблицы кодов операций для ассемблера Pentium II
Код
операции
ААА
ADD
ADD
AND
AND
Первый
операнд
—
EAX
reg
EAX
reg
Второй
операнд
—
immed32
reg
immed32
reg
Шестнадцатеричный
код
37
05
01
25
21
Длина
команды
1
5
2
5
2
Класс
команды
6
4
19
4
19

5 3 4
Глава 7. Уровень языка ассемблера
В качестве примера рассмотрим код операции ADD. Если команда
ADD
в качестве
первого операнда содержит регистр Е АХ, а в качестве второго — 32-битную константу
(immed32), то используется код операции 0x05, а длина команды составляет 5 бай-
тов. Если используется команда ADD с двумя регистрами в качестве операндов, то
длина команды составляет 2 байта, а код операции будет равен 0x01. Все комбина-
ции кодов операций и операндов, которые соответствуют данному правилу, будут
отнесены к классу 19 и будут обрабатываться так же, как команда
ADD
с двумя
регистрами в качестве операндов. Класс команд обозначает процедуру, которая
вызывается для обработки всех команд данного типа.
В некоторых ассемблерах можно писать команды с применением непосредствен-
ной адресации, даже если соответствующей команды не существует в выходном
языке. Такие команды с «псевдонепосредственными» адресами обрабатываются
следующим образом. Ассемблер назначает участок памяти для непосредственного
операнда в конце программы и порождает команду, которая обращается к нему.
Например, универсальная вычислительная машина IBM 3090 не имеет команд с не-
посредственными адресами. Тем не менее программист может написать команду
L 14.=F'5'
для загрузки в регистр 14 константы 5 размером в полное слово. Таким образом,
программисту не нужно писать директиву, чтобы разместить слово в памяти, при-
дать ему значение 5, дать ему метку, а затем использовать эту метку в команде L.
Константы, для которых ассемблер автоматически резервирует память, называют-
ся
литералами.
Литералы упрощают читаемость и понимание программы, делая
значение константы очевидным в исходном операторе. При первом проходе ас-
семблер должен создать таблицу из всех литералов, которые используются в про-
грамме. Все три компьютера, которые мы взяли в качестве примеров, содержат
команды с непосредственными адресами, поэтому их ассемблеры не обеспечива-
ют литералы. Команды с непосредственными адресами в настоящее время счита-
ются обычными, но раньше они рассматривались как нечто совершенно необыч-
ное. Вероятно, широкое распространение литералов внушило разработчикам, что
непосредственная адресация — это очень хорошая идея. Если нужны литералы, то
во время ассемблирования сохраняется таблица литералов, в которой появляется
новый элемент всякий раз, когда встречается литерал. После первого прохода таб-
лица сортируется и продублированные элементы удаляются.
В листинге 7.5 показана процедура, которая лежит в основе первого прохода
ассемблера. Названия процедур были выбраны таким образом, чтобы была ясна
их суть. Листинг 7.5 представляет собой хорошую отправную точку для изучения.
Он достаточно короткий, он легок для понимания, и из него видно, каким должен
быть следующий шаг — это написание процедур, которые используются в данном
листинге.
Листинг 7.5.
Первый проход простого ассемблера
public static void pass_one() {
// Эта процедура - первый проход ассемблера
boolean more_input=true; //флаг, который останавливает первый проход
String line, symbol, literal, opcode; //поля команды
int location_counter, length, value, type; //переменные
final int END STATEMENT = -2; //сигналы конца ввода

Процесс ассемблирования 535
location_counter = 0; //ассемблирование первой команды в ячейке 0
i m t i a l i z e _ t a b l e s ( ) , //общая инициализация
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_symbol,
которая просто выдает соответствующее обозначение в виде цепочки символов,
если таковое имеется, и выдает ноль, если его нет. Другие процедуры, например
get_length_of_type1
и
get_length_ofjtype2,
могут быть достаточно длинными и мо-
гут сами вызывать другие процедуры. Естественно, на практике типов будет не
два, а больше, и это будет зависеть от языка, который ассемблируется, и от того,
сколько типов команд предусмотрено в этом языке.
Структурирование программ имеет и другие преимущества помимо простоты
программирования. Если ассемблер пишется группой людей, разнообразные про-
цедуры могут быть разбиты на куски между программистами. Все подробности
получения входных данных спрятаны в процедуре
read_next_line.
Если эти детали
нужно изменить (например, из-за изменений в операционной системе), то это по-
влияет только на одну подчиненную процедуру, и никаких изменений в самой про-
цедуре
passjone
делать не нужно.
По мере чтения программы во время первого прохода ассемблер должен анали-
зировать каждую строку, чтобы найти код операции (например, ADD), определить

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; //ассемблирование первой команды в адресе 0
while (morejnput) { //morejnput устанавливается на «ложь» с помощью END
type = readj:ype(): //считывание поля типа следующей строки
opcode = read_opcode(); //считывание поля кода операции следующей строки
length = readJengthO; //считывание поля длины в следующей строке
line = readJineO; //считывание самой входной строки
if (type != 0) { //тип 0 указывает на строки комментария
switch(type) { //порождение выходного кода
case l:evalj:ypel(opcode, length, line, code): break;
case 2: eval_type2(opcode, length, line, code); break;
//Другие случаи
}
}
write_output(code): // запись двоичного кода
writejisting(code. line); // вывод на печать одной строки
location_counter = location_counter + length; //обновление счетчика адреса команд
if (type == END_STATEMENT) { // завершен ли ввод?

Процесс ассемблирования 537
more_input = false; // если да, то выполняем служебные операции
f i n i s h j j p O ; // завершение
}
Процедура второго прохода более или менее сходна с процедурой первого про-
хода: строки считываются по одной и обрабатываются тоже по одной. Поскольку
мы записали в начале каждой строки тип, код операции и длину (во временном
файле), все они считываются, и таким образом, нам не нужно проводить анализ
строк во второй раз. Основная работа по порождению кода выполняется процеду-
рами
eval_type1, eval_type2
и т. д. Каждая из них обрабатывает определенную мо-
дель (например, код операции и два регистра-операнда). Полученный в результа-
те двоичный код команды сохраняется в переменной
code.
Затем совершается
контрольное считывание. Желательно, чтобы процедура
write_code
просто сохра-
няла в буфере накопленный двоичный код и записывала файл на диск большими
порциями, чтобы сократить рабочую нагрузку на диск.
Исходный оператор и выходной (объектный) код, полученный из него (в шес-
тнадцатеричной системе), можно напечатать или поместить в буфер, чтобы напе-
чатать потом. После переустановки счетчика адреса команды вызывается следую-
щий оператор.
До настоящего момента предполагалось, что исходная программа не содержит
никаких ошибок. Но любой человек, который когда-нибудь писал программы на
каком-либо языке, знает, насколько это предположение не соответствует действи-
тельности. Наиболее распространенные ошибки приведены ниже:
1. Используемый символ не определен.
2. Символ был определен более одного раза.
3. Имя в поле кода операции не является допустимым кодом операции.
4. Код операции не снабжен достаточным количеством операндов.
5. У кода операции слишком много операндов.
6. Восьмеричное число содержит 8 или 9.
7. Недопустимое применение регистра (например, переход к регистру).
8. Отсутствует оператор END.
Программисты весьма изобретательны по части новых ошибок. Ошибки с не-
определенным символом часто возникают из-за опечаток. Хороший ассемблер
может вычислить, какой из всех определенных символов в большей степени соот-
ветствует неопределенному, и подставить его. Для исправления других ошибок
ничего кардинального предложить нельзя. Лучшее, что может сделать ассемблер
при обнаружении оператора с ошибкой, — это вывести сообщение об ошибке на
экран и попробовать продолжить процесс ассемблирования.
Таблица символов
Во время первого прохода ассемблер аккумулирует всю информацию о символах
и их значениях. Эту информацию он должен сохранить в таблице символьных имен,
к которой будет обращаться при втором проходе. Таблицу символьных имен мож-
но организовать несколькими способами. Некоторые из них мы опишем ниже.