Реклама:

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

BUFSIZE EQU 8192

Приписывая значение символическому имени в поле метки команды, ассемблер должен знать, какой адрес будет иметь эта команда во время выполнения программы. Для этого ассемблер во время ассемблирования сохраняет специальную переменную, называемую счетчиком адресов команд (Instruction Location Counter, ILC). В начале первого прохода эта переменная устанавливается в О и увеличивается после каждой обработанной команды на длину этой команды. В листинге 7.8 дан соответствующий пример для Pentium 4 (в предпоследней колонке поля комментариев показана длина каждой команды, а в последней - накопленное значение счетчика). В данном примере операторы, расположенные до метки MARIA, занимают 100 байт. Мы не будем давать примеры для SPARC и Motorola, поскольку различия между языками ассемблера не очень важны, и одного примера вполне достаточно. Кроме того, как вы уже успели убедиться, ассемблер SPARC совершенно неудобочитаем.

Листинг 7,8. Счетчик адресов команд позволяет отслеживать адреса команд

MARIA: MOV ЕАХ, I ; ЕАХ = I 5 100

MOV ЕВХ. J ; ЕВХ = J б 105

ROBERTA: MOV ЕСХ, К : ЕСХ = К б 111

IMUL ЕАХ. ЕАХ ; ЕАХ =1*1 2 117

IMUL ЕВХ, ЕВХ ; ЕВХ = J * J 3 119

IMUL ЕСХ, ЕСХ ; ЕСХ = К * К 3 122

MARILYN: ADD ЕАХ, ЕВХ ; ЕАХ = I * I + J * J 2 125

ADD ЕАХ. ЕСХ : ЕАХ =I*I+J*J+K*K 2 127

STEPHANY: JMP DONE ; переход к DONE 5 129

При первом проходе в большинстве ассемблеров используются, по крайней мере, 3 таблицы: таблица символических имен, таблица директив и таблица кодов операций. В случае необходимости используется еще литеральная таблица. Таблица символических имен содержит по одной записи для каждого имени, как показано в табл. 7.4. Символические имена либо являются метками, либо явным образом определяются (например, с помощью директивы EQU). В каждом элементе таблицы символических имен содержится само имя (или указатель на него), его численное значение и иногда некоторая дополнительная информация. Она может включать:

♦ длину поля данных, связанного с символом;

♦ биты перераспределения памяти (которые показывают, изменится ли значение символа, если программа будет загружена не по тому адресу, по которому ее предполагал загрузить ассемблер);

♦ сведения о том, можно ли получить доступ к символу извне процедуры.

Таблица 7.4, Таблица символических имен для программы из листинга 7.8

Символическое имя

Значение

Прочая информация

MARIA

 

ROBERTA

 

MARILYN

 

STEPHANY

 

В таблице кодов операций предусмотрена, по крайней мере, одна запись для каждого символьного кода операции ассемблера (табл. 7.5). В каждой записи содержится символьный код операции, два операнда, числовое значение кода операции, длина команды и номер типа, по которому можно определить, к какой группе относится код операции (коды операций делятся на группы в зависимости от числа и типа операндов).

Таблица 7,5. Несколько элементов таблицы кодов операций ассемблера Pentium 4

Код

Первый

Второй

Шестнадца-

Длина

Класс

операции

операнд

операнд

теричный код

команды

команды

AAA

-

-

ADD

EAX

immed32

ADD

reg

reg

AND

EAX

immed32

AND

reg

reg

В качестве примера рассмотрим код операции ADD. Если первым операндом команды ADD является регистр ЕАХ, вторым - 32-разрядная константа (immed32), то используется код операции 0x05, а длина команды составляет 5 байт. Если оба операнда команды ADD являются регистрами, длина команды составляет 2 байта, а код операции равен 0x01. Все комбинации кодов операций и операндов, которые соответствуют данному правилу, относятся к классу 19 и обрабатываются так же, как команда ADD с двумя регистрами в качестве операндов. Класс команд идентифицирует процедуру, которая вызывается для обработки всех команд данного типа.

В некоторых ассемблерах можно писать команды с применением непосредственной адресации, даже если соответствующей команды нет в выходном языке. Такие команды с "псевдонепосредственными" адресами обрабатываются следующим образом. Ассемблер назначает область памяти для непосредственного операнда в конце программы и порождает команду, которая к нему обращается. Например, универсальная вычислительная машина IBM 3090 не имеет команд с непосредственными адресами. Тем не менее для загрузки в регистр 14 константы 5 размером в полное слово программист может написать команду:

L l^F'S'

Таким образом, программисту не нужно писать директиву, чтобы разместить слово в памяти, присвоить ему значение 5, дать ему метку, а затем использовать эту метку в команде L. Константы, для которых ассемблер автоматически резервирует память, называются литералами. Литералы упрощают чтение и понимание программы, делая значение константы очевидным в исходном операторе. При первом проходе ассемблер должен создать таблицу всех литералов, которые используются в программе. Все три компьютера, которые мы взяли в качестве примеров, имеют команды с непосредственными адресами, поэтому их ассемблеры не поддерживают литералов. Команды с непосредственными адресами в настоящее время считаются вполне обычными, хотя раньше они рассматривались как нечто совершенно экзотическое. Вероятно, популярность литералов внушила разработчикам, что непосредственная адресация - очень хорошая идея. Если литералы нужны, то во время ассемблирования сохраняется таблица литералов, в которой появляется новый элемент всякий раз, когда встречается литерал. После первого прохода таблица сортируются, и повторяющиеся элементы удаляются.

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

Листинг 7.9. Первый проход простого ассемблера

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; // сигналы окончания ввода

location_counter =0; // ассемблирование первой команды в ячейке О

initialize_tables(); // общая инициализация

while (morejinput) { // more_input с помощью директивы END

// получает значение "ложь"

line = read_next_line(); // считывание строки length =0; // # байт в команде

type =0; // тип команды

if (line_is_not_comment(line)) { symbol = check_for_symbol(line); // содержит ли строка метку? if (symbol != null) // если да, то записываются

// символ и значение enter_new_symbol(symbol, location_counter); literal = check_for_litérai(1 ine); // содержит ли строка литерал? if (literal != null) // если да, то он

// сохраняется в таблице

enter_new_litérai(literal); // Теперь определяем тип кода операции. -1 значит недопустимый код операции opcode = extract_opcode(line); // определяем место кода операции type =search_opcode_table(opcode); // находим формат,

// например, OP REG1.REG2 if (type < 0) // если это не код операции,

// является ли он директивой? type = search_pseudo_table(opcode); switch(type) { // определяем длину команды

case 1:1 ength=get_length_of_typel(1ine); break; case 2:1 ength=get_length_of_type2(line); break; // другие варианты

}

}

write_temp_file(type, opcode, length, line); // информация для

// второго прохода location_counter = location_counter + length; // обновление счетчика

// адресов команд if (type == END_STATEMENT) { // завершился ли ввод?

more_input = false; // если да, то выполняем

// служебные действия: rewind_temp_for_pass_two(); // перематываем файл обратно

sort_literal_table(); // сортируем таблицу литералов

remove_redundant_litérais(); // и удаляем из нее дубликаты

Некоторые процедуры будут относительно короткими, например, check_for_symbol, которая просто возвращает в виде символьной строки имя, если таковое имеется, или ноль, если его нет. Другие процедуры, например get_length_of_typel и getl ength_of_type2, могут быть достаточно длинными и сами вызывать другие процедуры. Естественно, на практике типов будет не два, а больше, - это зависит от ассемблируемого языка и от того, сколько типов команд предусмотрено в этом языке.

Структурирование программ имеет и другие преимущества помимо простоты программирования. Если программа пишется группой людей, разнообразные процедуры могут быть поделены на фрагменты и распределены между программистами. Все подробности получения входных данных скрыты в процедуре read nextline. Если эти детали нужно изменить (например, из-за изменений в операционной системе), то это повлияет только на одну подчиненную процедуру, и никаких изменений в самой процедуре pass_one делать не нужно.

По мере чтения программы во время первого прохода ассемблер должен анализировать каждую строку, чтобы найти код операции (например, ADD), определить ее тип (набор операндов) и вычислить длину команды. Эта информация понадобится при втором проходе, поэтому ее лучше записать, чтобы не анализировать строку второй раз. Однако переписывание входного файла потребует больше операций ввода-вывода. Что лучше - увеличить количество операций ввода-вывода, чтобы меньше времени тратить на анализ строк, или сократить количество операций ввода-вывода и потратить больше времени на анализ, зависит от быстродействия центрального процессора и дисковой памяти, эффективности файловой системы и некоторых других факторов. В нашем примере мы запишем временный файл, в который будут записываться тип, код и длина операции, а также сама входная строка. Именно эта строка и будет считываться при втором проходе, и читать файл по второму разу не потребуется.

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

Ассемблирование за два прохода || Оглавление || Второй проход