Алгоритм обработки макровызова следующий
Обработка макровызова. На входе этого модуля есть номер элемента в Таблице имен макроопределений и разобранный текст оператора макровызова. Создание пустых: Таблицы локальных переменных, Таблицы меток. Чтение первой строки из Таблицы макроопределений по адресу, записанному в элементе Таблице имен макроопределений. (Здесь и далее мы подразумеваем, что после чтения очередной строки макроопределения указатель для следующего чтения устанавливается на адрес следующей строки, если он не изменен явным образом.) Проверка параметров: сопоставление фактических параметров вызова с формальными параметрами, описанными в заголовке макроопределения (Заголовок находится в строке, только что считанной из Таблицы макроопределений). При несоответствии фактических параметров формальным выдается сообщение об ошибке... ... и обработка макровызова завершается При правильном задании фактических параметров параметры и их значения заносятся в Таблицу локальных переменных. Создается и заполняется Таблица меток макроопределения. При этом текст макроопределения просматривается до оператора MEND, выявляются метки и заносятся в таблицу. Проверяется уникальность меток. После заполнения таблицы меток указатель чтения из Таблицы макроопределений устанавливается на вторую (следующую за заголовком строку) текста макроопределения. Читается следующая строка текста макроопределения. Если строка является комментарием Ассемблера, строка выводится в макрорасширение. Если строка является комментарием Макроязыка, управление передается на чтение следующей строки макроопределения. Выполняется разбор строки. Алгоритм ветвится в зависимости от мнемоники оператора. При обработке оператора LOCL имя локальной переменной ищется сначала в Таблице локальных переменных... ... а затем - в Таблице глобальных переменных. Если имя найдено в одной из таблиц, формируется сообщение о неуникальном имени. В противном случае заносится новая строка в таблицу локальных имен. В любом случае управление передается на чтение следующей строки макроопределения. Обработка оператора GLBL отличается от оператора LOCL только тем, что новая строка создается в Таблице глобальных переменных. При обработке оператора LOCL вычисляется выражение - операнд команды. Вычисление включает в себя подстановку значений входящих в выражение переменных. Возможны ошибки - из-за использования неопределенных имен и ошибок в синтаксисе выражения. Имя переменной ищется сначала в Таблице локальных переменных. Если имя найдено, изменяется его значение в Таблице локальных переменных. Если имя переменной не найдено, оно ищется в Таблице глобальных переменных. Если имя найдено в Таблице глобальных переменных, изменяется его значение в этой таблице. Если имя не найдено ни в одной из таблиц, формируется сообщение о неопределенном имени. При обработке оператора MIF вычисляется условное выражение - 1-й операнд команды (возможны ошибки). Проверяется значение вычисленного условного выражения. Если значение выражения "истина", имя метки - 2-го операнда команды ищется в Таблице меток макроопределения. Если метка найдена в таблице, указатель для следующего чтения из Таблице макроопределений устанавливается на адрес соответствующий метке Если метка найдена в таблице, выдается сообщение о неопределенной метке. При обработке оператора MGO имя метки - операнда команды ищется в Таблице меток макроопределения. Если метка найдена в таблице, указатель для следующего чтения из Таблице макроопределений устанавливается на адрес соответствующий метке. Если метка найдена в таблице, выдается сообщение о неопределенной метке. При обработке оператора MNOTE выводится сообщение, определяемое операндом. Устанавливается и анализируется код серьезности. Код серьезности является общим для всей работы Макропроцессора, его значение изменяется только, если новое значение больше текущего (более серьезная ошибка) Если код серьезности не допускает продолжения работы Макропроцессора, устанавливается признак завершения работы. При обработке оператора MEXIT устанавливается и анализируется код серьезности. Если код серьезности не допускает продолжения работы Макропроцессора, устанавливается признак завершения работы. Освобождаются структуры данных, созданные для обработки макровызова... ...и обработка макровызова завершается. При обработке оператора MEND освобождаются структуры данных, созданные для обработки макровызова... ...и обработка макровызова завершается. Любая другая мнемоника операции означает, что оператор является не оператором Макроязыка, а оператором языка Ассемблера. В этом случае прежде всего проверяется, не имеет ли оператор метки, которая должна быть уникальной. Если оператор имеет такую метку, формируется имя уникальной метки и индекс уникальных меток увеличивается на 1. Выполняются подстановки в операторе языка Ассемблера (значение имен ищутся в Таблицах локальных и глобальных переменных, возможны ошибки). Оператор языка Ассемблера записывается в макрорасширение. |
Алгоритм работы Макропроцессора
Очевидно, что когда Макропроцессор обрабатывает макровызов, он уже должен "знать" макроопределение данной макрокоманды. Для обеспечения этого таблицы макроопределений и имен макроопределений должны быть созданы до начала обработки макровызовов. Поэтому Макропроцессор должен состоять из двух проходов, на первом проходе строятся таблицы макроопределений и имен макроопределений, а на втором осуществляется обработка макровызовов. Если макроопределения сосредоточены в начале исходного модуля, то Макропроцессор может быть и однопроходным.
Ниже мы приводим алгоритм работы 2-проходного Макропроцессора, при этом мы исходим из следующих предпосылок:
наш Макропроцессор является независимым от Ассемблера; таблица параметров объединяется с таблицей локальных переменных, в дальнейшем мы называем объединенную таблицу таблицей локальных переменных; операторы Макроязыка включают в себя: MACRO, MEND, MEXIT, MNOTE, LOCL, GLBL, SET, MGO, MIF; обеспечиваются локальные и глобальные переменные макроопределений, уникальные метки.
Алгоритм работы Непосредственно Связывающего Загрузчика
Наиболее простой алгоритм работы Загрузчика - двухпроходный.
На вход Загрузчика обязательно подается список объектных модулей, из которых составляется программа. Этот список может быть параметром программы-Загрузчика или находиться в отдельном файле. На первом проходе Загрузчик просматривает все объектные модули по списку и решает 2 задачи:
определяет общий объем области памяти, необходимый для программы и размещение объектных модулей в этой области; составляет Глобальную таблицу внешних имен программы.
Структура элемента Глобальной таблицы - такая же, как и Таблицы внешних символов каждого модуля. В нее заносятся только входные точки всех модулей. Поскольку Загрузчик уже знает в каком месте области памяти, выделяемой для программы, будет размещаться тот или иной модуль, он заносит в Глобальную таблицу адреса входных точек относительно начала всей программы.
В конце 1-го прохода Загрузчик выделяет память и, уже зная фактический начальный адрес программы в памяти корректирует все адреса в Глобальной таблице внешних символов.
На 2-ом проходе Загрузчик снова читает все объектные модули по списку. При этом он уже размещает коды модуля в памяти и формирует для каждого модуля Таблицу внешних символов (локальную для модуля) и Таблицу перемещений. Адресные поля в этих таблицах он заполняет или корректирует с учетом фактического адреса модуля в памяти и содержимого Глобальной таблицы внешних символов. Затем он выполняет обработку Таблицы перемещений, используя для коррекции адресных кодов в программе значения из Локальной Таблицы внешних символов.
Алгоритм выполнения 1-го прохода следующий:
1-й проход Макропроцессора Инициализация: открытие исходного файла, создание пустых таблиц, признак "обработка макроопределения" устанавливается в FALSE. Чтение следующей строки исходного файла с проверкой конца файла. Если при чтении строки найден конец файла, выводится сообщение об ошибке, закрываются файлы, освобождается память... ... и Макропроцессор завершается с признаком ошибки. Если конец файла не достигнут, выполняется лексический разбор прочитанной строки с выделением имени и мнемоники операции. Алгоритм Макропроцессора разветвляется в зависимости от мнемоники операции Если мнемоника операции MACRO - заголовок макроопределения, то в таблицу имен макроопределений заносится имя, находящееся в этом операторе и начальный адрес свободной области в таблице макроопределений. (При занесении имени в таблицу имен макроопределений проверяется, нет ли уже в таблице такого имени, если есть - ошибка) Оператор MACRO записывается в таблицу макроопределений. Признак "обработка макроопределения" устанавливается в TRUE. Если мнемоника операции MEND - конец макроопределения, то оператор записывается в таблицу макроопределений... ... и признак "обработка макроопределения" устанавливается в FALSE. Если мнемоника операции END - конец программы, то проверяется установка признака "обработка макроопределения". Если этот признак установлен в TRUE, т.е., конец программы встретился до окончания макроопределения, то выводится сообщение об ошибке, закрываются файлы, освобождается память... ... и Макропроцессор завершается с признаком ошибки. Если этот признак установлен в FALSE, то выполняются завершающие операции ... ... и заканчивается 1-й проход Макропроцессора. При любой другой мнемонике оператора проверяется установка признака "обработка макроопределения". Если этот признак установлен в TRUE, то оператор записывается в таблицу макроопределений, если признак установлен в FALSE, то оператор игнорируется Макропроцессором. |
1-й проход Загрузчика. Начальные установки. Создание пустой Глобальной таблицы. Стартовый адрес=пусто. Относительный адрес 1-го сегмента - 0. Размер программы - 0. Выборка следующего имени из списка объектных модулей. Если весь список объектных модулей обработан - переход на окончание 1-го прохода. Чтение заголовка очередной записи объектного модуля, если объектный модуль обработан полностью - переход к следующему модулю. Чтение остальной части записи (размер записи содержится в ее заголовке). Разветвление в зависимости от типа записи. При обработке записи окончания проверяется, имеется ли в записи стартовый адрес. Если стартового адреса нет - никакая другая обработка записи не производится. Если в записи есть стартовый адрес, проверяется, не был ли он уже установлен. Если стартовый адрес не был установлен, он устанавливается. Если стартовый адрес был установлен, выдается сообщение об ошибке. (Ни эта, ни последующие рассмотренные ошибки не приводят к немедленному завершению 1-го прохода, однако, если на 1-ом проходе были ошибки, 2-й проход не выполняется). При обработке записи связывания выполняется перебор элементов Таблицы внешних символов... ... и разветвление - в зависимости от типа элемента. Для элемента - сегмента вычисляется начальный адрес следующего сегмента и длина сегмента прибавляется к общему размеру программы. Для элемента - входной точки ищется имя точки в Глобальной таблице . Если имя не найдено в Глобальной таблице, в таблицу добавляется новый элемент. Если имя найдено в Глобальной таблице, - ошибка, неуникальное внешнее имя. При окончании 1-го прохода проверяется, установился ли адрес стартовой точки программы. Если этот адрес не установлен - ошибка. Если этот адрес установлен и в ходе выполнения 1-го прохода не было других ошибок, Загрузчик продолжает работу. Выделяется память для программы в соответствии с ее размером. В Глобальную таблицу внешних символов записываются фактические адреса. Выполняется 2-й проход. Освобождается Глобальная таблица Если не было ошибок на 2-ом проходе ... управление передается на стартовый адрес программы Загрузчик завершает работу. |
Алгоритм выполнения 2-го прохода следующий:
2-й проход макропроцессора Начальные установки: открытие файлов, создание пустых таблиц, etc. Признак режима обработки устанавливается в значение "обработка программы". Признак конца обработки установлен ? Если признак конца обработки установлен, выполняются завершающие операции... .. и работа Макропроцессора заканчивается. Выполняется разбор строки. Проверяется признак режима обработки. Если признак режима установлен в значение "обработка макроопределения", то проверяется мнемоника оператора. Если в режиме обработки макроопределения встречается мнемоника MEND, то режим переключается в "обработка программы", все прочие операторы в режиме обработки макроопределения игнорируются. Если признак режима работы установлен в значение "обработка программы", происходит ветвление алгоритма в зависимости от мнемоники оператора. Обработка оператора MACRO заключается в установке режима обработки в значение "обработка программы". Обработка директивы Ассемблера END заключается в установке признака окончания работы и выводе оператора в выходной файл. Любая другая мнемоника ищется в Таблице машинных команд и в Таблице директив Ассемблера. Если мнемоника найдена в одной из этих таблиц, то... ... оператор просто выводится в выходной файл. Если оператор не является оператором языка Ассемблера, то предполагается, что это макровызов и соответствующее мнемонике имя ищется в Таблице имен макроопределений. Если имя не найдено в Таблице имен макроопределений, то оно ищется в библиотеках макроопределений (см. ниже). Если имя не найдено и в библиотеках макроопределений, вырабатывается сообщение об ошибке и управление передается на чтение следующего оператора программы. Если имя не найдено в библиотеках макроопределений, соответствующие элементы включаются в Таблицу имен макроопределений и в Таблицу макроопределений. Если имя есть в Таблице макроопределений, выполняется обработка макровызова (см. ниже), после чего управление передается на чтение следующего оператора программы. |
2-й проход Загрузчика Установка на начало списка имен объектных модулей. Выборка следующего имени из списка объектных модулей. Если весь список объектных модулей обработан - переход на окончание 2-го прохода. Создание для модуля Локальной таблицы внешних символов и Таблицы перемещений - пустых. Чтение заголовка очередной записи объектного модуля, если все записи модуля прочитаны - переход к обработке перемещений в модуле. Чтение остальной части записи (размер записи содержится в ее заголовке). Разветвление в зависимости от типа записи. Для кодовой записи считывается относительный адрес записи и переводится в фактический. Тело кодовой записи считывается и размещается в памяти по фактическому адресу. Для записи связывания перебираются находящиеся в ней элементы Таблицы имен Обработка разветвляется в зависимости от типа имени. Для имен сегментов или входных точек относительный адрес переводится в фактический. Имя внешней точки ищется в Глобальной таблице внешних имен. Если имя не найдено в Глобальной таблице, выдается сообщение об ошибке. Если имя найдено в Глобальной таблице, в значение адреса из Глобальной таблицы становится значением этого имени. Элемент с откорректированным адресом заносится в Локальную таблицу имен. Для записи перемещения перебираются находящиеся в ней элементы Таблицы перемещения. Относительный адрес в элементе заменяется на фактический... ... и элемент добавляется в Таблицу перемещений. После того, как весь модуль прочитан, выполняется перебор Таблицы перемещений модуля. Для каждого элемента Таблицы перемещений имя, записанное в его поле имени ищется в Локальной таблице имен и из Локальной таблицы имен выбирается связанный с этим именем адрес. Из кода программы выбирается код, адрес и длина которого записаны в элементе Таблицы перемещений. Над выбранным кодом и адресом, выбранным из Таблицы имен выполняется операция сложения или вычитания, результат записывается на место кода. После перебора всей Таблицы перемещений, освобождаются Таблица перемещений и Локальная таблица имен модуля, и управление передается на обработку следующего модуля. После обработки всех объектных модулей 2-й проход заканчивается. |
Алгоритмы работы Ассемблеров
Ассемблер просматривает исходный программный модуль один или несколько раз. Наиболее распространенными являются двухпроходные Ассемблеры, выполняющие два просмотра исходного модуля. На первом проходе Ассемблер формирует таблицу символов модуля, а на втором - генерирует код программы
Библиотеки макроопределений
Макровызовы к макроопределение, приведенному в исходном модуле, могут применяться только в этом же исходном модуле. Для того, чтобы можно было использовать макроопределение в разных исходных модулях, макроопределения помещаются в библиотеку макроопределений. Список библиотек макроопределений, которые используются для данного исходного модуля является параметром Макропроцессора.
Мы в нашей схеме алгоритма показали, что обращение к библиотекам макроопределений происходит на 2-ом проходе Макропроцессора - если мнемоника оператора не распознана ни как оператор языка Ассемблера, ни как макрокоманда, определенная в данном исходном модуле. Возможны, однако, и другие алгоритмы использования библиотек.
Один из таких алгоритмов следующий:
Анализ мнемоники производится на 1-ом проходе Ассемблера, все операторы, не распознанные как операторы языка Ассемблера, считаются макрокомандами и для них создаются строки в Таблице имен макроопределений. Если для такой макрокоманды макроопределение еще не найдено, поле ссылки на Таблицу макроопределений остается пустым. Если в исходном модуле встречается макроопределение, то его текст заносится в Таблицу макроопределений. Если в Таблице имен макроопределений уже есть это имя с пустой ссылкой на Таблицу макроопределений, ссылке присваивается значение. Если такого имени в Таблице имен макроопределений нет, в таблице создается новая строка. В конце 1-го прохода просматривается Таблица имен макроопределений. Если в таблице находятся имена с пустыми ссылками на Таблицу макроопределений, соответствующее макроопределение ищется в библиотеках. Если макроопределение найдено в библиотеке, его текст переписывается в Таблицу макроопределений и присваивается значение ссылке в соответствующей строке Таблицы имен макроопределений. Если после этого в Таблице имен макроопределений остаются имена с пустыми ссылками, это свидетельствует об ошибках в программе.
Директивы
Директивы являются указаниями Ассемблеру о том, как проводить ассемблирование. Директив м.б. великое множество. В 1-ом приближении мы рассмотрим лишь несколько практически обязательных директивы (мнемоники директив везде - условные, в конкретных Ассемблерах те же по смыслу директивы могут иметь другие мнемоники).
EQU | Определение имени. Перед этой директивой обязательно стоит имя. Операнд этой директивы определяет значение имени. Операндом может быть и выражение, вычисляемое при ассемблировании. Имя может определяться и через другое имя, определенное выше. Как правило, не допускается определение имени со ссылкой вперед. |
DD | Определение данных. Выделяются ячейки памяти и в них записываются значения, определяемые операндом директивы. Перед директивой может стоять метка/имя. Как правило, одной директивой могут определяться несколько объектов данных. В конкретных Ассемблерах может существовать либо одна общая директива DD, тогда тип данных, размещаемых в памяти определяется формой записи операндов, либо несколько подобных директив - для разных типов данных. В отличие от других,, эта директива приводит непосредственной к генерации некоторого выходного кода - значений данных. |
BSS | Резервирование памяти. Выделяются ячейки памяти, но значения в них не записываются. Объем выделяемой памяти определяется операндом директивы. Перед директивой может стоять метка/имя. |
END | Конец программного модуля. Указание Ассемблеру на прекращение трансляции. Обычно в модуле, являющемся главным (main) операндом этой директивы является имя точки, на которую передается управление при начале выполнения программы. Во всех других модулях эта директива употребляется без операндов. |
Двухпроходный Ассемблер - 1-й проход
Алгоритм работы 1-го прохода двухпроходного Ассемблера показан на рисунке.
Начало 1-го прохода ассемблирования. Начальные установки: установка в 0 счетчика адреса PC; создание пустой таблицы символов; создание пустой таблицы литералов; открытие файла исходного модуля; установка в FASLE признака окончания. Признак окончания TRUE? Считывание следующей строки исходного модуля. Добавка к счетчику адреса устанавливается равной 0. При считывании был обнаружен конец файла? Если конец файла обнаружен до того, как обработана директива END, - ошибка (преждевременный конец файла), при этом также устанавливается признак окончания обработки.. Лексический разбор оператора программы. При этом: выделяется метка/имя, если она есть; выделяется мнемоника операции; выделяется поле операндов; удаляются комментарии в операторе; распознается строка, содержащая только комментарий. Строка содержит только комментарий? В этом случае обработка оператора не производится. Мнемоника операции ищется в таблице директив. Завершился ли поиск в таблице директив успешно? Если мнемоника была найдена в таблице директив, происходит ветвление, в зависимости от того, какая директива была опознана. Обработка директив типа DD (определения данных) включает в себя: выделение элементов списка операндов (одной директивой DD может определяться несколько объектов данных); определение типа и, следовательно, размера объекта данных, заданного операндом; обработка для каждого операнда возможного коэффициента повторения. Добавка к счетчику адреса устанавливается равной суммарному размеру объектов данных, определяемых директивой. Обработка директив типа BSS подобна обработке директив типа DD. Добавка к счетчику адреса устанавливается равной суммарному объему памяти, резервируемому директивой. Обработка директивы END состоит в установке в TRUE признака окончания обработки. Обработка директивы включает в себя вычисление значения имени и занесение его в таблицу символов. Обработка прочих директив ведется по индивидуальным для каждой директивы алгоритмам. Существенно, что никакие директивы, кроме DD и BSS, не изменяют нулевого значения добавки к счетчику адреса. Если мнемоника операции не найдена в таблице директив, она ищется в таблице команд. Завершился ли поиск в таблице команд успешно? Если мнемоника не была найдена в таблице команд, - ошибка (неправильная мнемоника). Если мнемоника найдена в таблице команд - определение длины команды, она же будет добавкой к счетчику адреса. Есть ли в операторе литерал? Занесение литерала в таблицу литералов (если его еще нет в таблице). Была ли в операторе метка? Поиск имени в таблице символов. Имя в таблице символов найдено? Если имя найдено в таблице символов - ошибка (повторяющееся имя).Если имя не найдено в таблице символов - занесение имени в таблицу символов. Формирование и печать строки листинга. Модификация счетчика адреса вычисленной добавкой к счетчику Печать строки листинга и переход к чтению следующего оператора. При окончании обработки - закрытие файла исходного модуля. Были ли ошибки на 1-ом проходе ассемблирования? Формирование литерального пула Выполнение 2-го прохода ассемблирования. Конец работы Ассемблера. |
Примечания
Определение длины команды (п.21). Эта задача может решаться существенно разным образом для разных языков. В языках некоторых Ассемблеров мнемоника команды однозначно определяет ее формат и длину (S/390, все RISC-процессоры). В этом случае длина команды просто выбирается из таблицы команд. В других языках длина и формат зависит от того, с какими операндами употреблена команда (Intel). В этом случае длина вычисляется по некоторому специфическому алгоритму, в который входит выделение отдельных операндов и определение их типов. В последнем случае должна производиться также необходимая проверка правильности кодирования операндов (количество операндов, допустимость типов). Обнаружение литералов (п.22). Требует, как минимум, выделения операндов команды. (Подробнее об обработке литералов см. ниже). Листинг. Строка листинга печатается в конце каждой итерации обработки команды. Строка листинга 1-го прохода содержит: номер оператора, значение счетчика адреса (только для команд и директив, приводящих к выделению памяти), текст оператора. Листинг 1-го прохода не является окончательным, фактически он используется только для поиска ошибок, поэтому печатать его необязательно. Режим печати листинга 1-го прохода может определяться параметром Ассемблера или специальной директивой. После листинга программы может (опционно) печататься таблица символов. /i>Ошибки. На первом проходе выявляются не все ошибки, а только те, которые связаны с выполнением задачи 1-го прохода. Сообщение об ошибке включает в себя: код ошибки, диагностический текст, номер и текст оператора программы, в котором обнаружена ошибка.
Двухпроходный Ассемблер - 2-й проход
Обычно 2-й проход Ассемблера читает исходный модуль с самого начала и отчасти повторяет действия 1-го прохода (лексический разбор, распознавание команд и директив, подсчет адресов). Это, однако, скорее дань традиции - с тех времен, когда в вычислительных системах ощущалась нехватка (или даже полное отсутствие) внешней памяти. В те далекие времена колода перфокарт или рулон перфоленты, содержащие текст модуля, вставлялись в устройство ввода повторно. В системах с эволюционным развитием сохраняются перфокарты и перфоленты (виртуальные), так что схема работы Ассемблера - та же. В новых системах Ассемблер может создавать промежуточный файл - результат 1-го прохода, который является входом для 2-го прохода. Каждому оператору исходного модуля соответствует одна запись промежуточного файла, и формат этой записи приблизительно такой:
Текст исходного оператора нужен только для печати листинга, Ассемблер на 2-ом проходе использует только первые 4 поля записи. Первое поле позволяет исключить строки, целиком состоящие из комментария. Второе поле позволяет избежать подсчета адресов, третье - поиска мнемоники в таблицах. Основная работа 2-го прохода состоит в разборе поля операндов и генерации объектного кода.
Некоторые общие соображения, касающиеся этой работы. Объектный код команды состоит из поля кода операции и одного или нескольких полей операндов. Код операции, как правило, имеет размер 1 байт, количество, формат и семантика полей операндом определяется для каждого типа команд данной аппаратной платформы. В общем случае операндом команды может быть:
регистр; непосредственный операнд; адресное выражение.
Виды адресных выражений зависят от способов адресации вычислительной системы, некоторые (возможно, наиболее типовые) способы адресации:
абсолютный адрес; [базовый регистр]+смещение (здесь и далее квадратные скобки означают) "содержимое того, что взято в скобки); [базовый регистр]+[индексный регистр]+смещение; имя+смещение; литерал.
Адресное выражение моет содержать арифметические операции, соображения, касающиеся арифметики в этом случае - те же, что и в адресной арифметике языка C.
Имена в адресных выражениях должны заменяться на значения. Замена абсолютных имен (определенных в директиве EQU) очень проста - значение имени из таблицы символов просто подставляется вместо имени. Перемещаемые имена (метки и имена переменных) превращаются Ассемблером в адресное выражение вида [базовый регистр]+смещение. В таблице символов значения этих имен определены как смещение соответствующих ячеек памяти относительно начала программы. При трансляции имен необходимо, чтобы:
Ассемблер "знал", какой регистр он должен использовать в качестве базового; Ассемблер "знал", какое значение содержится в базовом регистре; в базовом регистре действительно содержалось это значение.
Первые два требования обеспечиваются директивами, третье - машинными командами. Ответственность за то, чтобы при обеспечении этих требований директивы согласовывались с командами, лежит на программисте. Эти требования по-разному реализуются в разных вычислительных системах. Приведем два примера.
В Intel Ассемблер использует в качестве базовых сегментные регистры (DS при трансляции имен переменных, CS при трансляции меток). Для простой программы, состоящей из одной секции, Загрузчик перед выполнением заносит во все сегментные регистры сегментный адрес начала программы и Ассемблер считает все смещения относительно него. Сложная программа может состоять из нескольких секций, и в сегментном регистре может содержаться адрес той или иной секции, причем содержимое сегментного регистра может меняться в ходе выполнения программы. Загрузка в сегментный регистр адреса секции выполняется машинными командами:
MOV AX,секция
MOV сегментный_регистр,AX
Для того, чтобы Ассемблер знал, что адрес секции находится в сегментном_регистре, применяется директива:
ASSUME сегментный_регистр:секция
Далее при трансляции имен Ассемблер превращает имена в адресные выражения вида
[сегментный_регистр]+смещение в секции
Отмена использования сегментного регистра задается директивой:
ASSUME сегментный_регистр:NOTHING
Обратим внимание на то, что при трансляции команды
MOV AX,секция
в поле операнда заносится относительный адрес секции, при выполнении же здесь должен быть ее абсолютный адрес. Поэтому поля операндов такого типа должны быть модифицированы Загрузчиком после размещения программы в оперативной памяти.
Более гибкая система базовой адресации применяется в S/360, S/370, S/390. В качестве базового может быть использован любой регистр общего назначения. Директива:
USING относительный_адрес,регистр
сообщает Ассемблеру, что он может использовать регистр в качестве базового, и в регистре содержится адрес - 1-й операнд. Чаще всего относительный_адрес кодируется как * (обозначение текущего значения счетчика адреса), это означает, что в регистре содержится адрес первой команды, следующей за директивой USING. Занесение адреса в базовый регистр выполняется машинной командой BALR. Обычный контекст определения базового регистра:
BALR регистр,0 USING *,регистр
С такими операндами команда BALR заносит в регистр адрес следующей за собой команды.
Зная смещение именованной ячейки относительно начала программы и смещение относительно начала же программы содержимого базового регистра, Ассемблер легко может вычислить смещение именованной ячейки относительно содержимого базового регистра.
В отличие от предыдущего примера, в этом случае не требуется модификации при загрузке, так как команда BALR заносит в регистр абсолютный адрес.
Директива
DROP регистр
отменяет использование регистра в качестве базового.
В качестве базовых могут быть назначены несколько регистров, Ассемблер сам выбирает, какой из них использовать в каждом случае.
Выше мы говорили, что Ассемблер "знает" базовый регистр и его содержимое. Это "знание хранится в таблице базовых регистров. Обычно таблица содержит строки для всех регистров, которые могут быть базовыми и признак, используется ли регистр в таком качестве. Формат строки таблицы:
Алгоритм выполнения 2-го прохода представлен на рисунке.
Мы исходили из того, что 2-й проход использует промежуточный файл, сформированный 1-ым проходом. Если же 2-й проход использует исходный модуль, то алгоритм должен быть расширен лексическим разбором оператора, распознаванием мнемоники и подсчетом адресов - так же, как и в 1-ом проходе.
Начало 2-го прохода ассемблирования. Начальные установки: создание пустой таблицы базовых регистров; открытие промежуточного файла исходного модуля; установка в FASLE признака окончания Признак окончания TRUE? Считывание следующей записи промежуточного файла. Если запись промежуточного файла описывает комментарий, переход на печать строки листинга Выясняется, содержит оператор команду или директиву Если оператор содержит команду, формируется байт кода операции (код выбирается из таблицы команд) в объектном коде. Выделение следующего элемента из списка операндов с удалением его из списка и с проверкой, не обнаружен ли при выделении конец списка операндов? Если конец не обнаружен, обрабатывается выделенный операнд. Проверяется, не превысило ли число операндов требуемого для данного типа команды (выбирается из таблицы команд) Если число операндов превышает требуемое - формирование сообщения об ошибке Если число операндов правильное, распознается и проверяется тип операнда. Если тип операнда не распознан или недопустим для данной команды - формирование сообщения об ошибке. Есть ли в команде имя? Если в команде есть имя, оно ищется в таблице символов. Если имя в таблице символов не найдено - формирование сообщения об ошибке. Если найдено имя в таблице символов, оно переводится в "база-смещение" Если имени в команде нет, выполняется разбор и интерпретация операнда с проверкой правильности его кодирования. Если обнаружены ошибки в кодировании операнда - формирование сообщения об ошибке. Формируется код поля операнда и заносится в объектный код команды и обрабатывается следующий элемент списка операндов. Если обнаружен конец списка операндов, проверяется, не меньше ли число операндов требуемого для данного типа команды. Если число операндов соответствует требуемого, управление переходит на вывод объектного кода. Если число операндов меньше требуемого - формирование сообщения об ошибке Если обрабатываемый оператор является директивой, алгоритм разветвляется, в зависимости от того, какая это директива. При обработке любой директивы производится разбор и анализ ее операндов и (не показано на схеме алгоритма) возможно формирование сообщения об ошибке. Обработка директивы типа DD включает в себя: выделение элементов списка операндов; для каждого элемента - распознавание типа и значения константы; генерация объектного кода константы; обработка возможных коэффициентов повторения. Обработка директивы типа BSS может вестись точно так же, как и DD за исключением того, что вместо кода константы генерируются некоторые "пустые" коды. Однако, эти коды не нужны в объектном модуле, они могут не генерироваться, в этом случае должны предприниматься некоторые действия, формирующие "разрыв" в объектных кодах. Обработка директивы типа USING (ASSUME) включает в себя занесение в соответствующую строку таблицы базовых регистров значения операнда-адреса и установку для данного регистра признака использования. Обработка директивы типа USING (ASSUME) включает в себя занесение в соответствующую строку таблицы базовых регистров значения операнда-адреса и установку для данного регистра признака использования. Обработка директивы END устанавливает признак окончания в TRUE. При обработке этой директивы в объектный модуль также может заносится стартовый адрес программы - параметр директивы. Обработка прочих директив ведется по своим алгоритмам. После окончания обработки команды или директивы сформированный объектный код выводится в файл объектного модуля. Печать строки листинга. На эту точку также управление передается при выявлении ошибок. При наличии ошибки сообщение об ошибке печатается после строки листинга. Управление затем передается на считывание следующей записи промежуточного файла. После того, как установлен признак окончания работы формируются и выводятся в объектный модуль коды литерального пула, таблицы связываний и перемещений. Закрываются файлы, освобождается выделенная память. Работа Ассемблера завершается |
Формат объектного модуля
Объектный модуль, поступающий на вход Загрузчика должен в той или иной форме содержать:
размер модуля; машинные коды; входные точки (те адреса в модуле, к которым возможны обращения извне); внешние точки (те имена во внешних модулях, к которым есть обращения в данном модуле); информация о размещении в модуле перемещаемых данных.
Один из вариантов организации объектного модуля описывается ниже.
Объектный модуль состоит из записей четырех типов. В каждой записи первый байт содержит идентификатор типа записи, следующие 2 байта - размер записи. Количество и формат следующих байтов записи определяется ее типом.
Кодовая запись содержит адрес относительно начала модулей и коды программы. Кодовые записи строятся Ассемблером при генерации объектного кода - кодов команд и директив типа DC. Каждая кодовая запись начинается с адреса (относительно начала модуля), с которого начинается размещение ее содержимого. Разрывы в линейном пространстве адресов могут быть обусловлены директивами выделения памяти без записи в нее значений (директивы типа BSS) или разрывами, управляемыми программистом (директивы типа ORG).
Запись связываний содержит один или несколько элементов Таблицы внешних символов. Элемент Таблицы внешних символов имеет следующий формат:
Ассемблер формирует новые элементы Таблицы перемещений при обработке директив типа SEGMENT, ENT, EXT.
Запись перемещений содержит один или несколько элементов Таблицы перемещений. Каждый элемент этой таблицы описывает один элемент кода программы, требующий настройки, зависящей от адреса загрузки программы в память и имеет следующий формат:
Относительный адрес и длина - адрес и длина того кода, который должен быть модифицирован. Имя символа - имя из Таблицы внешних символов, которое прибавляется к значению кода или вычитается из него. Бит операции - признак операции сложения или вычитания. Ассемблер генерирует элемент Таблицы перемещений, когда обрабатывает адресные выражения. Адресное выражение может быть абсолютным (независящими от адреса загрузки) или перемещаемым (зависящими от адреса загрузки) .
Элементы Таблицы перемещений генерируются только для перемещаемых выражений. Рассмотрим адресное выражение:
ADDR1 - ADDR2
Если ADDR1 и ADDR2 являются именами, перемещаемыми внутри одного и того же сегмента SEGM (их адресные значения определяются относительно начала сегмента), то адресное выражение является абсолютным, так как его значение: SEGM+ADDR1 - (SEGM+ADDR2) не зависит от адреса сегмента. Для такого выражения элемент Таблицы перемещений не строится. Если ADDR1 является именем, перемещаемым внутри сегмента SEGM, а ADDR2 - абсолютный адрес, то выражение является простым перемещаемым. В кодовое представление этого выражения записывается разность между относительным адресом в сегменте ADDR1 и абсолютным значением ADDR2. Для такого выражения строится элемент Таблицы перемещений:
адрес SEGN + длина
Загрузчик прибавит к содержимому кода адрес сегмента SEGM. Если ADDR1 является внешним именем, а ADDR2 - абсолютный адрес, то выражение также является простым перемещаемым. В кодовое представление этого выражения абсолютное значение ADDR2. Элемент Таблицы перемещений для такого выражения содержит:
адрес ADDR1 + длина
Если ADDR1 и ADDR2 являются внешним именем, то выражение является сложным перемещаемым. В кодовое представление этого выражения записывается 0. Для такого выражения строятся два элемента Таблицы перемещений:
адрес ADDR1 + длина адрес ADDR2 - длина
При загрузке к нулевому значению записанному по адресу адрес будет прибавлен адрес внешней точки ADDR1, а затем вычтен адрес внешней точки ADDR2.
Запись окончания формируется Ассемблером при обработке директивы END, она содержит стартовый адрес программы. Естественно, эта запись должна быть заполнена только в одном из объектных модулей, составляющих программу.
Глобальные переменные макроопределения
Значения локальных переменных макроопределения сохраняются только при обработке данного конкретного макровызова. В некоторых случаях, однако, возникает необходимость, чтобы значение переменной макроопределения было запомнено Макропроцессором и использовано при следующей появлении той же макрокоманды в данном модуле. Для этого могут быть введены глобальные переменные макроопределения (в сильносвязанных Макропроцессорах в них опять-таки нет необходимости).
Объявление глобальной переменной макроопределения может иметь, например, вид:
имя_переменной GLBL начальное_значение (последнее необязательно)
Присваивание значений глобальным переменным макроопределения выполняется так же, как и локальным.
Этапы подготовки программы
При разработке программ, а тем более - сложных, используется принцип модульности, разбиения сложной программы на составные части, каждая из которых может подготавливаться отдельно. Модульность является основным инструментом структурирования программного изделия, облегчающим его разработку, отладку и сопровождение.
Определение (ГОСТ) | Программный модуль - программа или функционально завершенный фрагмент программы, предназначенный для хранения, трансляции, объединения с другими программными модулями и загрузки в оперативную память. |
При выборе модульной структуры должны учитываться следующие основные соображения:
Функциональность - модуль должен выполнять законченную функцию Несвязность - модуль должен иметь минимум связей с другими модулями, связь через глобальные переменные и области памяти нежелательна Специфицируемость - входные и выходные параметры модуля должны четко формулироваться
На рисунке показаны этапы, которые проходит программа от своего написания до выполнения
Программа пишется в виде исходного модуля, на рисунке - файл ИМ.
Определение (ГОСТ) | Исходный модуль - программный модуль на исходном языке, обрабатываемый транслятором и представляемый для него как целое, достаточное для проведения трансляции. |
Первым (не для всех языков программирования обязательным) этапом подготовки программы является обработка ее Макропроцессором (или Препроцессором). Макропроцессор обрабатывает текст программы и на выходе его получается новая редакция текста (на рис. - ИМ'). В большинстве систем программирования Макропроцессор совмещен с транслятором, и для программиста его работа и промежуточный ИМ' "не видны". Следует иметь в виду, что Макропроцессор выполняет обработку текста, это означает, с одной стороны, что он "не понимает" операторов языка программирования и "не знает" переменных программы, с другой, что все операторы и переменные Макроязыка (тех выражений в программе, которые адресованы Макропроцессору) в промежуточном ИМ' уже отсутствуют и для дальнейших этапов обработки "не видны".
Так, если Макропроцессор заменил в программе некоторый текст A на текст B, то транслятор уже видит только текст B, и не знает, был этот текст написан программистом "своей рукой" или подставлен Макропроцессором.
Следующим этапом является трансляция.
Определение (ГОСТ) | Трансляция - преобразование программы, представленной на одном языке программирования, в программу на другом языке программирования, в определенном смысле равносильную первой. |
Определение (ГОСТ) | Машинный язык - язык программирования, предназначенный для представления программы в форме, позволяющей выполнять ее непосредственно техническими средствами обработки информации. |
Определение (ГОСТ) | Автокод - символьный язык программирования, предложения которого по своей структуре в основном подобны командам и обрабатываемым данным конкретного машинного языка. |
Определение (ГОСТ) | Язык Ассемблера - язык программирования, который представляет собой символьную форму машинного языка с рядом возможностей, характерных для языка высокого уровня (обычно включает в себя макросредства). |
Определение (ГОСТ) | Язык высокого уровня - язык программирования, понятия и структура которого удобны для восприятия человеком. |
Определение (ГОСТ) | Объектный модуль - программный модуль, получаемый в результате трансляции исходного модуля. |
Специфика исходного языка, однако, может сказываться на физическом представлении базовых типов данных, способах обращения к процедурам/функциям и т.п. Для совместимости разноязыковых модулей должны выдерживаться общие соглашения. Большая часть объектного модуля - команды и данные машинного языка именно в той форме, в какой они будут существовать во время выполнения программы. Однако, программа в общем случае состоит из многих модулей. Поскольку транслятор обрабатывает только один конкретный модуль, он не может должным образом обработать те части этого модуля, в которых запрограммированы обращения к данным или процедурам, определенным в другом модуле. Такие обращения называются внешними ссылками. Те места в объектном модуле, где содержатся внешние ссылки, транслируются в некоторую промежуточную форму, подлежащую дальнейшей обработке. Говорят, что объектный модуль представляет собой программу на машинном языке с неразрешенными внешними ссылками.
Разрешение внешних ссылок выполняется на следующем этапе подготовки, который обеспечивается Редактором Связей (Компоновщиком). Редактор Связей соединяет вместе все объектные модули, входящие в программу. Поскольку Редактор Связей "видит" уже все компоненты программы, он имеет возможность обработать те места в объектных модулях, которые содержат внешние ссылки. Результатом работы Редактора Связей является загрузочный модуль.
Определение (ГОСТ) | Загрузочный модуль - программный модуль, представленный в форме, пригодной для загрузки в оперативную память для выполнения. |
Возможен также вариант, в котором редактирование связей выполняется при каждом запуске программы на выполнение и совмещается с загрузкой.
Это делает Связывающий Загрузчик. Вариант связывания при запуске более расходный, т.к. затраты на связывание тиражируются при каждом запуске. Но он обеспечивает:
большую гибкость в сопровождении, так как позволяет менять отдельные объектные модули программы, не меняя остальных модулей; экономию внешней памяти, т.к. объектные модули, используемые во многих программах не копируются в каждый загрузочный модуль, а хранятся в одном экземпляре.
Вариант интерпретации подразумевает прямое исполнение исходного модуля.
Определение (ГОСТ) | Интерпретация - реализация смысла некоторого синтаксически законченного текста, представленного на конкретном языке. |
Не обязательно подготовка программы должна вестись на той же вычислительной системе и в той же операционной среде, в которых программа будет выполняться. Системы, обеспечивающие подготовку программ в среде, отличной от целевой называются кросс-системами. В кросс-системе может выполняться вся подготовка или ее отдельные этапы:
Макрообработка и трансляция Редактирование связей Отладка
Типовое применение кросс-систем - для тех случаев, когда целевая вычислительная среда просто не имеет ресурсов, необходимых для подготовки программ, например, встроенные системы. Программные средства, обеспечивающие отладку программы на целевой системе можно также рассматривать как частный случай кросс-системы.
Дальнейшая тематика курса соответствует плану намеченному в первой теме: мы будем последовательно рассматривать системные обрабатывающие программы, обеспечивающие подготовку программ.
Качественное расширение возможностей.
Активное и грамотное применение макросредств может сделать работу программиста весьма продуктивной. Так, затратив определенные усилия на создание библиотеки макроопределений, программист может превратить язык Ассемблера в качественно новый язык, который будет обладать некоторыми свойствами языка высокого уровня. Программист может сделать этот язык в известной степени проблемно-ориентированным, то есть в максимальной степени приспособленным для тех задач, которые решает его разработчик. Вкратце опишем те основные направления, по которым может идти расширение возможностей Ассемблера за счет макросредств.
Комментарии макроопределения
Если в тексте макроопределения имеются комментарии, то они переходят в макрорасширение так же, как и операторы машинных команд и директив Ассемблера. Однако, должна быть обеспечена и возможность употребления таких комментариев, которые не переходят в макрорасширение - комментарии, которые относятся не к самой программе, а к макроопределению и порядку его обработки. Такие комментарии должны обладать некоторым отличительным признаком. Возможны специальные директивы Ассемблера, определяющие режим печати комментариев макроопределения.
Локальные переменные макроопределения
Поскольку генерация макрорасширения ведется по некоторому алгоритму, описанному в макроопределении, реализация этого алгоритма может потребовать собственных переменных. Эти переменные имеют силу только внутри данного макроопределения, в макрорасширении не остается никаких "следов" переменных макроопределения.
Переменные макроопределения могут использоваться двумя способами:
их значения могут подставляться вместо их имен в тех операторах макроопределения, которые переходят в макрорасширение; их значения могут проверяться в условных операторах макроязыка и влиять на последовательность обработки.
При подстановке значений переменных макроопределения в макрорасширение работают те же правила, что и при подстановки значений параметров.
Для сильносвязанных Макропроцессоров необходимости в локальных переменных макроопределения, вместо них могут использоваться имена программы (определяемые директивой EQU). Для сильносвязанных и независимых процессоров переменный макроопределения и имена программы должны различаться, для этого может применяться тот же признак, что и для параметров макроопределения. Объявление локальной переменной макроопределения может иметь, например, вид:
имя_переменной LOCL начальное_значение (последнее необязательно)
Макрокоманды внутри макроопределений
В отличие от предыдущего, это средство может быть весьма полезным. Прежде всего - для часто употребляемых макрокоманд, могут быть включены в библиотеки макроопределений - системные или пользовательские. Это может весьма упростить создание новых макроопределений.
Для обеспечения такой возможности достаточно сделать рекурсивным только 2-й проход Макропроцессора. В нем несколько усложняется анализ операторов макроопределения. В ветви "Другой" (на нашей схеме алгоритма она начинается с блока ) 2-й проход Макропроцессора должен распознавать макрокоманду и, если оператор - макрокоманда, вызывать сам себя. Распознавание макрокоманды - методом исключения: если оператор - не оператор Макроязыка, не директива Ассемблера и не машинная команда, то он считается макрокомандой и ищется в Таблице имен макроопределений. Для рекурсивного вызова создается новая Таблица локальных переменны (и параметров). Таблица глобальных переменных и индекс уникальных меток используются общие.
Некоторая сложность возникает в том случае если вложенные марокоманды - библиотечные. В нашем алгоритме 1-го прохода содержимое макроопределения (то, что лежит между операторами MACRO и MEND) не анализировалось, следовательно, определения вложенных макрокоманд не заносились в Таблицы макроопределений и имен макропредолений. Есть два варианта решения этой проблемы:
На 1-ом проходе все же распознавать вложенные макровызовы и включать макроопределения их в таблицы. Выполнять это на 2-ом проходе: при появлении оператора, не распознанного ни как оператор Макроязыка, ни как директива Ассемблера, ни как машинная команда и ни как макрокоманда, определение которой уже есть в наших таблицах, считать его библиотечной макрокомандой и искать ее макроопределение в библиотеках. Если макроопределение найдено, оно добавляется в наши таблицы. Нет необходимости удалять из таблиц определение вложенной библиотечной макрокоманды при завершении обработки внешнего макровызова: оно может потребоваться при обработке и последующих макровызовов.
Макроопределения внутри макроопределений
Честно говоря, необходимость в таких средствах сомнительна. Она может возникнуть при создании большого макроопределения, в котором есть повторяющиеся фрагменты. Вложенное макроопределение действительно только внутри того макроопределения, в которое оно вложено.
Против такого средства можно привести 2 соображения:
макроопределение не бывает слишком большим - иначе не срабатывают его преимущества над подпрограммой (следует однако признать, что могут существовать довольно большие макроопределения, которые генерируют разнообразные варианты небольших макрорасширений); в языке Pascal допускаются вложенные процедуры, а в языке C - нет; и C прекрасно обходится без них, да и современная практика программирования на Pascal их практически не использует.
Тем не менее, если вложенные макроопределения все же необходимы, можно предложить следующий вариант их реализации:
1-й проход Макропроцессора работает почти по тому же алгоритму, который приведен нами. Принципиально важно, однако, что Таблица макроопределений и Таблица имен макроопределений имеют последовательную структуру, элементы в них записываются в порядке их поступления. В Макропроцессоре есть некоторая целая переменная - глубина вложенности. Ее исходное значение - 0, при каждом появлении оператора MACRO это значение увеличивается на 1, при каждом появлении оператора MEND - уменьшается на 1. Если при глубине вложенности 0 появляется оператор MACRO, в Таблицу имен макроопределений заносится новый элемент, и текст макроопределения записывается в Таблицу макроопределений - до тех пор, пока глубина вложенности не станет равной 0. Появление оператора MACRO при глубине вложенности, большей 0 не приводит к созданию нового элемента в Таблице имен макроопределений. Таким образом, в Таблице имен макроопределений имеется строка только для самого внешнего макроопределения, а все вложенные пока "не видны" и находятся внутри текста внешнего в Таблице макроопределений. 2-й проход Макропроцессора при обработке макровызова считывает текст макроопределения в некоторый буфер и прежде всего рекурсивно вызывает для его обработки Макропроцессор. Для вложенного вызова Макропроцессора доступны Таблица макроопределений и Таблица имен макроопределений, новые макроопределения, обнаруженные рекурсивным вызовом заносятся в конец этих таблиц. При возврате из рекурсивного вызова макроопределения, дописанные им, удаляются из таблиц.
Макрорасширения в листинге
Как уже неоднократно говорилось, макрорасширения для Ассемблера неотличимы от программного текста, написанного программистом "своей рукой". Но программист, анализируя листинг программы, конечно, должен видеть макрорасширения и отличать их от основного текста. Как правило, директивы Ассемблера, управляющие печатью листинга предусматривают режим, при котором макрорасширение не печатается в листинге, а печатается только макрокоманда и режим, при котором в листинге печатается и макрокоманда, и ее макрорасширение, но операторы макрорасширения помечаются каким-либо специальным символом.
Модель оперативной памяти
Объем адресного пространства памяти, к которому теоретически могут выполняться обращения к программе определяется разрядностью представления адреса. Однако, реально в целевой ВС может быть значительно меньший объем памяти. Во встроенных ВС адресное пространство может покрываться реальной памятью несмежными фрагментами, причем фрагменты реальной памяти могут быть как ОЗУ, так и ПЗУ.
Интерпретатор должен "знать" конфигурацию реальной памяти в целевой ВС. Возможные варианты задания такой конфигурации:
Потребовать, чтобы любая ячейка памяти, к которой обращается программа, была описана в программе (директивой DD или BSS). Описать конфигурацию памяти в отдельном файле, являющимся входным для Интерпретатора.
Представляется, что второй подход более универсальный, так как:
обращение в программе по неописанному в ней адресу памяти возможно (особенно это касается программ для встроенных ВС с абсолютными программами и жестки распределением памяти); определение памяти в программе также является объектом проверки/отладки может содержать ошибки; в Ассемблере нет средств описания ОЗУ/ПЗУ.
Внешнее описание памяти считывается Интерпретатором в начале работы и превращается в таблицу фрагментов вида:
Оперативная память целевой ВС представляется памятью (не обязательно оперативной) исходной ВС. Однако, в модели памяти на исходной ВС мы имеем возможность помимо собственно данных, хранящихся в целевой памяти, представлять также и описание этих данных. Каждый байт целевой памяти представляется двумя байтами исходной памяти. В первом байте представления хранятся собственно данные, а во втором - ряд признаков, характеризующих ячейку целевой памяти. Среди этих признаков могут быть такие:
a | признак 1-го байта команды (управление можно передавать только на 1-й байт команды); |
b | признак команды/данных |
c | признак инициализированных/неинициализированных данных |
d | признак изменяемых/неизменяемых данных |
e | признак останова при передаче управления |
f | признак останова при передаче записи |
g | признак останова при передаче чтении |
h | и т.д. |
Все названные признаки - однобитные. Признаки a, b устанавливаются Кросс-ассемблером при трансляции программы и не изменяются при выполнении. Признак с устанавливается Кросс-ассемблером, но может изменяться Интерпретатором в процессе выполнения. Признак d устанавливается Интерпретатором перед началом выполнения на основе таблицы фрагментов и, возможно, дополнительной информации, вводимой программистом (отдельно от программы) и может изменяться программистом в ходе интерактивной отладки. Признаки e-f устанавливаются перед началом выполнения на основе дополнительной информации и может изменяться программистом в ходе интерактивной отладки.
Дополнительная информация о памяти, таким образом, состоит из таблицы фрагментов, списка переменных в ОЗУ, которые не разрешается изменять, списка переменных, при обращении к которым должен происходить останов, и меток, при передаче управления на которые должен происходить останов.
Каждое обращение к памяти в программе характеризуется типом: R (чтение), W(запись) или X(передача управления). При любом типе обращения проверяется попадание в реально существующий фрагмент памяти. При обращении типа X проверяется бит a признака, управление может быть передано только на байт с установленным признаком a. При обращениях типа R и W проверяется бит b признака, обращения этого типа могут происходить только к данным При обращениях типа R проверяется бит c признака, читаться могут только инициализированные данные При обращениях типа W проверяется бит d признака, данные должны быть изменяемые, бит с признака при этом устанавливается, т.е. данные становятся инициализированными.
Модель процессора
Работа процессора моделируется алгоритмом работы Интерпретатора. Основной алгоритм работы модели состоит из цикла, в каждой итерации которого моделируется выполнение одной команды целевой программы. Итерация этого цикла начинается с выборки байта, записанному в модели памяти по адресу, содержащемуся в модели регистра-счетчика адреса. В подавляющем большинстве ВС первый байт команды содержит код операции, позволяющий однозначно идентифицировать команду. Интерпретатор выполняет поиск по коду операции в таблице команд. При этом может использоваться либо таблица команд Ассемблера, либо ее модификация с расширениями и с возможностью быстрого поиска по коду операции. Распознав команду, Интерпретатор выбирает ее остальные байты (их количество определено в таблице команд) и выделяет из них операнды команды (их количество и кодировка определяется типом команды). Далее алгоритм Интерпретатора разветвляется, в общем случае число ветвей равно числу возможных кодов операции. В каждой ветви вычисляется значение, являющееся результатом выполнения той или иной команды. Вычисленное значение заносится в объект, являющийся для данной команды приемником результата. Кроме того для тех команд, для которых это требуется устанавливаются значения признаков в регистре состояния (перечень признаков, устанавливаемых командой, может содержаться в таблице команд Интерпретатора). Вычисляется новое значение регистра-счетчика адреса. В большинстве случаев это значение получается добавлением к текущему его значению длины команды, но в командах перехода (типа JMP, CALL) это значение вычисляется.
При реализации алгоритмов выполнения отдельных команд возможны два подхода, которые мы называем RISC и CISC-моделями, по аналогии с архитектурами процессоров (однако выбор программной RISC или CISC-модели необязательно должен совпадать с реальной архитектурой процессора).
Смысл RISC-модели состоит в том, что разветвление алгоритма выполняется сразу же после распознавания команды и выполнение каждой команды полностью реализуется кодами соответствующей ветви.
Смысл CISC-модели состоит в том, что выполнение каждой команды представляется в виде последовательности выполнения простых процедур ("микрокоманд"). Список микрокоманд, составляющих выполнение каждой команды, может быть "зашит" в программу в виде последовательности вызовов или представлен в виде данных, например, в виде списка номеров процедур. В последнем случае алгоритм не требует ветвления, а сводится к циклу, в каждой итерации которого выбирается номер очередной процедуры и вызывается процедура с данным номером. В предельном случае выполнение каждой команды может быть представлено в виде исходного текста на языке макрокоманд, который интерпретируется Интерпретатором.
Пример
Пусть в языке микрокоманд имеются следующие (показаны не все) микрокоманды:
Микрокоманда | Выполнение |
GETR n,rx | Выборка номера регистра, заданного в n-ом операнде в промежуточную переменную rx |
GETA n,ax | Выборка адреса, заданного в n-ом операнде в промежуточную переменную ax |
LDR dx,rx | Выборка данных из регистра, номер которого находится в промежуточной переменной rx в промежуточную переменную dx |
LDM dx,ax | Выборка данных из памяти по адресу, находящемуся в промежуточной переменной ax промежуточную переменную dx |
SDR rx,dx | Запись данных из промежуточной переменной dx в регистр, номер которого находится в промежуточной переменной rx |
SDM dx,ax | Запись данных из промежуточной переменной dx в память по адресу, находящемуся в промежуточной переменной ax промежуточную переменную dx |
ADD dx1,dx2 | Сложение данных из промежуточной переменной dx1 с данными из dx2; результат - в dx1 |
SIG dx | Инверсия знака данных, содержащихся в промежуточной переменной dx |
CC1 dx | Установка признаков "больше", "меньше", "равно" по значению, содержащемуся в промежуточной переменной dx |
CC2 dx | Установка признака переполнения по значению, содержащемуся в промежуточной переменной dx |
PC1 | Увеличение регистра-счетчика адреса на длину команды |
PC2 dx | Запись данных из промежуточной переменной dx в регистр-счетчика адреса |
END | Окончание микропрограммы |
Тогда реализация некоторых машинных команд может быть "замикропрограммирована" следующим образом:
Команда языка Ассемблера | Выполнение | Микропрограммная реализация |
LR регистр2,регистр1 | Пересылка данных из регистра1 в регистр2 | GETR 2, r1; LDR d1,r1; GETR 1,r2; SDR r2,d1, PC1; END; |
L регистр,память | Пересылка данных из памяти по адресу память в регистр | GETA 2,a1; LDM d1,a1; GETR 1,r1; SDR r1,d1; PC1; END; |
AR регистр2,регистр1 | Сложение данных из регистра1 с данными в регистре2; результат - в регистре1 | GETR 2, r1; LDR d1,r1; GETR 1,r2; LDR d2, r2; ADD d1,d2; SDR r1; PC1; END; |
CMP регистр,память | Сравнение данных, содержащихся в регистре с данными по адресу память | GETR 1,r1; LDR d1,r1; GETA 2, a1; LDM d2,a1; SIG d2; ADD d1,d2; CC1 d1; CC2 d1; PC1; END; |
JMP память | Переход по адресу память | GETA 2,a2; PC2 a2; END; |
Модель регистров
Модель регистров включает в себя, как минимум:
регистры общего назначения регистр-счетчик адреса регистр состояния
Регистры моделируются переменными интерпретатора.
РОН во время выполнения программы содержат обрабатываемые данные. РОН могут моделироваться как отдельными переменными, так и массивами - в зависимости от их количества и свойств. В тех ВС, где РОН немного и некоторые из них обладают собственными индивидуальными свойствами (напр., Intel) удобно представлять каждый РОН в виде отдельной переменной. Для тех ВС, где РОН много и/или они одинаковы во всем (напр., S/390, все RISC) , их целесообразно представлять в виде массива. Характерно, что в ВС первого типа РОН обычно имеют собственные имена, а в ВС второго типа РОН идентифицируются номерами.
Счетчик адреса содержит адрес текущий выполняемой команды и представляется в виде отдельной переменной.
Регистр состояния содержит признаки результата выполнения предыдущей команды - больше, меньше, равен нулю (не все команды устанавливают эти признаки) и, возможно, признак привилегированного/непривилегированного режима. Эти признаки могут "упаковываться" в одну переменную или представляться отдельной переменной каждый.
Настраивающие Загрузчики
Настраивающий Загрузчик является первым шагом в сторону усложнения от Абсолютного Загрузчика. Функции связывания и перемещения решаются в нем не самым эффективным, но простейшим способом.
Некоторые дополнительные директивы
OGR | Установка адреса. Операндом директивы является числовая константа или выражение, вычисляемое при ассемблировании. Как правило, Ассемблер считает, что первая ячейка обрабатываемой им программы располагается по адресу 0. Директива ORG устанавливает счетчик адресов программы в значение, определяемое операндом. Так, при создании COM-программ для MS DOS программа начинается с директивы ORG 100H. Этим оператором резервируется 256 байт в начале программы для размещения префикса программного сегмента. В абсолютных программах директива применяется для размещения программы по абсолютным адресам памяти. |
START/ SECT | Начало модуля или программной секции. Операндом директивы является имя секции. Этой директивой устанавливается в 0 счетчик адресов программы. Программа может состоять из нескольких программных секций, в каждой секции счет адресов ведется от 0. При обработке этой директивы на 1-ом проходе Ассемблер создает таблицу программных секций вида:
На 1-ом проходе Ассемблер составляет список секций, и только в конце 1-го прохода определяет их длины и относительные адреса в программе. На 2-ом проходе Ассемблер использует таблицу секций при трансляции адресов. |
Директивы связывания | |
ENT | Входная точка. Операндом этой директивы является список имен входных точек программного модуля - тех точек, на которые может передаваться управление извне модуля или тех данных, к которым могут быть обращения извне. |
EXT | Внешняя точка. Операндом этой директивы является список имен, к которым есть обращение в модуле, но сами эти имена определены в других модулях. |
Эти директивы обрабатываются на 2-ом проходе, и на их основе строятся таблицы связываний и перемещений (см. Тему 4). |
Некоторые структуры данных 1-го прохода
Таблица команд содержит одну строку для каждой мнемоники машинной команды. Ниже приведен пример структуры такой таблицы для простого случая, когда мнемоника однозначно определяет формат и длину команды. Обработка команд происходит по всего нескольким алгоритмам, зависящим от формата команды, поэтому в данном случае все параметры обработки могут быть представлены в виде данных, содержащихся в таблице.
Таблица директив содержит одну строку для каждой директивы Обработка каждой директивы происходит по индивидуальному алгоритму, поэтому параметры обработки нельзя представить в виде данных единого для всех директив формата. Для каждой директивы в таблице хранится только идентификация (имя или адрес, или номер) процедуры Ассемблера, выполняющей эту обработку. Некоторые директивы обрабатываются только на 1-ом проходе, некоторые - только на 2-ом, для некоторых обработка распределяется между двумя проходами.
Таблица символов является основным результатом 1-го прохода Ассемблера. Каждое имя, определенное в программе, должно быть записано в таблице символов. Для каждого имени в таблице хранится его значение. , размер объекта, связанного с этим именем и признак перемещаемости/неперемещаемости. Значением имени является число, в большинстве случаев интерпретируемое как адрес, поэтому разрядность значения равна разрядности адреса. Перемещаемость рассматривается в разделе, посвященном Загрузчикам, здесь укажем только, что значение перемещаемого имени должно изменяться при загрузке программы в память. Имена, относящиеся к командам или к памяти, выделяемой директивами DD, BSS, как правило, являются перемещаемыми (относительными), имена, значения которых определяются директивой EQU (см. ниже), являются неперемещаемыми (абсолютными).
Таблица литералов содержит запись для каждого употребленного в модуле литерала. Для каждого литерала в таблице содержится его символьное обозначение, длина и ссылка на значение. Литерал представляет собой константу, записанную в памяти. Обращение к литералам производится так же, как и к именам ячеек программы. По мере обнаружения в программе литералов Ассемблер заносит их данные в т.наз. литеральный пул. Значение, записываемое в таблицу литералов является смещением литерала в литеральном пуле. После окончания 1-го прохода Ассемблер размещает литеральный пул в конце программы (т.е., назначает ему адрес, соответствующий последнему значению счетчик адреса) и корректирует значения в таблице литералов, заменяя их смещениями относительно начала программы. После выполнения этой корректировки таблица литералов может быть совмещена с таблицей символов.
Некоторые возможности Макроязыка
Ниже мы описываем некоторые возможности макроязыка, в той или иной форме реализованные во всех Макропроцессорах. Мы, однако, ориентируемся прежде всего на Макропроцессор, независимый от Ассемблера, потому что в этой категории функции Макропроцессора легче определить.
Непосредственно Связывающие Загрузчики
Эти Загрузчики называются непосредственно связывающими потому, что они обеспечивают обращение к внешней точке непосредственно, а не через косвенную адресацию. Эти Загрузчики обеспечивают более высокую эффективность кода и более гибкие возможности связывания. Такие возможности достигаются за счет того, что в объектном модуле содержится вся необходимая для Загрузчика информация.
О структуре таблиц Ассемблера
Структура таблиц Ассемблера выбирается таким образом, чтобы обеспечить максимальную скорость поиска в них.
Таблицы команд и директив являются постоянной базой данных. Они заполняются один раз - при разработке Ассемблера, а затем остаются неизменными. Эти таблицы целесоообразно строить как таблицы прямого доступа с функцией хеширования, осуществляющей преобразование мнемоники в адрес записи в таблице. Имеет смысл постараться и подобрать функцию хеширования такой, чтобы в таблице не было коллизий. Поскольку заполнение таблицы происходит один раз, а доступ к ней производится многократно, эти затраты окупаются.
Таблица символов формируется динамически - в процессе работы 1-го прохода. Поиск же в этой таблице осуществляется как в 1-ом проходе (перед занесением в таблицу нового имени, проверяется, нет ли его уже в таблице). Построение этой таблицы как таблицы прямого доступа не очень целесообразно, так как неизбежно возникновение коллизий. Поэтому поиск в таблице символов может быть дихотомическим, но для этого таблица должна быть упорядочена. Поскольку новые имена добавляются в таблицу "по одному", и после каждого добавления упорядоченность таблицы должна восстанавливаться, целесообразно применять алгоритму сортировки, чувствительные к исходной упорядоченности данных.
Эти же соображения относятся и к другим таблицам, формируемым Ассемблером в процессе работы. При больших размерах таблиц и размещении их на внешней памяти могут применяться и более сложные (но и более эффективные) методы их организации, например - B+-деревья.
Объектно-ориентированный Ассемблер
Макросредства могут обеспечить и реализацию свойств объектно-ориентированного программирования - в большей или меньшей степени.
Простейшее расширение Ассемблера ОО свойствами предполагает введение макрокоманды определения объекта (или резервирования памяти для объекта). В макрокоманде указывается тип объекта и она употребляется вместо директив DC/BSS. Для типа могут быть созданы макрокоманды-операции. В этом варианте может быть воплощен принцип полиморфизма, так как одна и та же операция может быть допустимой для разных типов. (Например, одна команда сложения для всех типов - чисел, независимо от из разрядности и формы представления). Принцип инкапсуляции реализуется здесь в том смысле, что программист, использующий макрокоманды не должен знать внутренней структуры объекта и подробности выполнения операций над ним, защиту же внутренней структуры организовать гораздо сложнее.
Имеются примеры разработок, в которых на уровне Макроязыка созданы и средства описания классов, включающие в себя наследование классов со всеми вытекающими из него возможностями.
Одно- и многопроходный Ассемблер
Мы показали, что в двухпроходном Ассемблере на 1-ом проходе осуществляется определение имен, а на втором - генерация кода.
Можно ли построить однопроходный Ассемблер? Трудность состоит в том, что в программе имя может появиться в поле операнда команды прежде, чем это имя появится в поле метки/имени, и Ассемблер не может преобразовать это имя в адресное выражение, т.к. еще на знает его значение. Как решить эту проблему?
Запретить ссылки вперед. Имя должно появляться в поле операнда только после того, как оно было определено как метка при команде или данных или через директиву EQU. В этом случае построить Ассемблер легко, но накладываются ограничения, стесняющие действия программиста. Если объектный модуль сохраняется в объектной памяти, то Ассемблер может отложить формирование кода для операнда - неопределенного имени и вернуться к нему, когда имя будет определено. При появлении в поле операнда команды неопределенного имени поле операнда не формируется (заполняется нулями). Таблица символов расширяется полями: признаком определенного/неопределенного имени, и указателем на список адресов в объектном модуле, по которым требуется модификация поля операнда.
При появлении имени в поле операнда Ассемблер ищет имя в таблице символов. Если имя найдено и помечено как определенное, Ассемблер транслирует его в адресное выражение, как и при 2-проходном режиме. Если имя не найдено, Ассемблер заносит имя в таблицу символов, помечает его неопределенным и создает первый элемент связанного с именем списка, в который заносит адрес в объектном модуле операнда данной команды. Если имя найдено, но помечено как определенное, Ассемблер добавляет в список, связанный с данным именем, адрес в объектном модуле операнда данной команды.
При появлении имени в поле метки команды или директивы Ассемблер определяет значение имени и ищет его в таблице символов. Если имя не найдено, Ассемблер добавляет имя в таблицу и помечает его как определенное. Если имя найдено и помечено как определенное, Ассемблер выдает сообщение об ошибке (неуникальное имя). Если имя найдено, но помечено как определенное, Ассемблер обрабатывает связанный с данным именем список: для каждого элемента списка по хранящемуся в нем адресу в объектном модуле записывается значение имени. После обработки список уничтожается, значение имени сохраняется в таблице символов и имя помечается как определенное.
После окончания прохода Ассемблер проверяет таблицу символов: если в ней остались неопределенные имена, выдается сообщение об ошибке. Если объектный модуль выводится в файл по мере его формирования, алгоритм работы Ассемблера очень похож на предыдущий случай. Ассемблер не может при определении имени исправить уже сформированные и выведенные в файл операнды, но вместо этого он формирует новую запись объектного модуля, исправляющую старую. При загрузке будет сначала загружена в память запись с пустым полем операнда, но затем на ее место - запись с правильным полем операнда.
Для чего может понадобиться многопроходный Ассемблер? Единственная необходимость во многих проходах может возникнуть, если в директиве EQU разрешены ссылки вперед. Мы упоминали выше, что имя в директиве EQU может определяться через другое имя. В одно- или двухпроходном Ассемблере это другое имя должно быть обязательно определено в программе выше. В многопроходном Ассемблере оно может быть определено и ниже. На первом проходе происходит определение имен и составление таблицы символов, но некоторые имена остаются неопределенными. На втором проходе определяются имена, не определившиеся во время предыдущего прохода. Второй проход повторяется до тех пор, пока не будут определены все имена (или не выяснится, что какие-то имена определить невозможно). На последнем проходе генерируется объектный код.
Окончание макроопределения
Если у макроопределения есть начало (оператор MACRO), то у него, естественно, должен быть и конец. Конец макроопределения определяется оператором MEND. Этот оператор не требует параметров. Макроопределение, взятое в "скобки" MACRO - MEND может располагаться в любом месте исходного модуля, но обычно все макроопределения размещают в начале или в конце модуля.
Операнды команд.
Константы - могут представлять непосредственные операнды или абсолютные адреса памяти. Применяются 10-ные, 8-ные, 16-ные, 2-ные, символьные константы.
Непосредственные операнды - записываются в сам код команды.
Имена - адреса ячеек памяти. При трансляции Ассемблер преобразует имена в адреса. Способ преобразования имени в значение зависит от принятых способов адресации. Как правило, в основным способом адресации в машинных языках является адресация относительная: адрес в команде задается в виде смещения относительно какого-то базового адреса, значение которого содержится в некотором базовом регистре. В качестве базового могут применяться либо специальные регистры (DS, CS в Intel) или регистры общего назначения (S/390).
Литералы - записанные в особой форме константы. Концептуально литералы - те же имена. При появлении в программе литерала Ассемблер выделяет ячейку памяти и записывает в нее заданную в литерале константу. Далее все появления этого литерала Ассемблер заменяет на обращения по адресу этой ячейки. Таким образом, литеральные константы, хранятся в памяти в одном экземпляре, независимо от числа обращений к ним.
Специальный синтаксис - явное описание способа адресации (например, указание базового регистра и смещения и т.п.).
Оператор безусловного перехода и метки макроопределения
Возможный формат оператора:
MGO макрометка
Концептуально важным понятием является макрометка. Макрометка может стоять перед оператором Макроязыка или перед оператором языка Ассемблера. Макрометки не имеют ничего общего с метками в программе. Передача управления на макрометку означает то, что при обработке макроопределения следующим будет обрабатываться оператор, помеченный макрометкой. Макрометки должны иметь какой-то признак, по которому их имена отличались бы от имен программы и переменных макроопределения. Например, если имена переменных макроопределения начинаются с символа &, то имя макрометки может начинаться с &&.
Оператор условного перехода
Возможный формат оператора:
MIF условное_выражение макрометка
Если условное_выражение имеет значение "истина", обработка переходит на оператор, помеченный макрометкой, иначе обрабатывается следующий оператор макроопределения. Условные выражения формируются по обычным правилам языков программирования. В них могут употребляться параметры и переменные (локальные и глобальные) макроопределения, константы, строковые, арифметические и логические операции и, конечно же, операции сравнения. Кроме того, в составе Макроязыка обычно имеются специальные функции, позволяющие распознавать тип своих операндов, например: является ли операнд строковым представлением числа, является ли операнд именем, является ли операнд именем регистра и т.п.
Операторы повторений
Операторы повторений Макроязыка (или директивы повторений языка Ассемблера) заставляют повторить блок операторов исходного текста, возможно, с модификациями в каждом повторении. Операторы повторений играют роль операторов цикла в языках программирования, они не являются обязательными для макроязыка, так как цикл можно обеспечить и условным переходом.
Как и в языках программирования, в Макроязыке может быть несколько форм операторов повторения, приведем некоторые (не все) из возможных форм:
MDO выражение блок_операторов_макроопределения
ENDMDO
выражение должно иметь числовой результат, обработка блока операторов повторяется столько раз, каков результат вычисления выражения.
MDOLIST переменная_макроопределения,список_выражений блок_операторов_макроопределения
ENDMDO
обработка блока операторов повторяется столько раз, сколько элементов имеется в списке_выражений, при этом в каждой итерации переменной_макроопределения присваивается значение очередного элемента из списка_выражений.
MDOWHILE условное_выражение блок_операторов_макроопределения
ENDMDO
обработка блока операторов повторяется до тех пор, пока значение условного_выражения - "истина".
Основные понятия
Определение (не по ГОСТ) | Макропроцессор - модуль системного ПО, позволяющий расширить возможности языка Ассемблера за счет предварительной обработки исходного текста программы. |
Определение, которое дает ГОСТ не представляется удачным, так как оно говорит только о сокращении объема записи, а это лишь одна из возможностей обеспечиваемых Макропроцессором. Хотя Макропроцессоры являются обязательным элементом всех современных языков Ассемблеров, аналогичные модули (Препроцессоры) могут быть и для других языков, в том числе и для языков высокого уровня. Для одних языков (Pascal, PL/1) применение средств препроцессора является опционным, для других (C, C++) - обязательным.
Важно понимать, что Макропроцессор осуществляет обработку исходного текста. Он "не вникает" в синтаксис и семантику операторов и переменных языка Ассемблера, не знает (как правило) имен, употребляемых в программе, а выполняет только текстовые подстановки. В свою очередь, Ассемблер обрабатывает исходный текст, не зная, написан тот или иной оператор программистом "своей рукой" или сгенерирован Макропроцессором. По тому, насколько Препроцессор (Макропроцессор) и Транслятор (Ассемблер) "знают" о существовании друг друга, их можно разделить на три категории:
Независимые. Препроцессор составляет отдельный программный модуль (независимую программу), выполняющую просмотр (один или несколько) исходного модуля и формирующую новый файл исходного модуля, поступающий на вход Транслятора (пример - язык C). Слабосвязанные. Препроцессор составляет с Транслятором одну программу, но разные секции этой программы. Если в предыдущем случае Препроцессор обрабатывает весь файл, а затем передает его Транслятору, то в этом случае единицей обработки является каждый оператор исходного текста: он обрабатывается секцией Препроцессора, а затем передается секции Транслятора. (Пример - HLASM для S/390). Сильносвязанные. То же распределение работы, что и в предыдущем случае, но Препроцессор использует некоторые общие с Транслятором структуры данных.
Например, Макропроцессор может распознавать имена, определенные в программе директивой EQU и т.п. (Пример - MASM, TASM).
Основные термины, связанные с данными, обрабатываемыми Макропроцессором: макровызов (или макрокоманда), макроопределение, макрорасширение.
Макровызов или макрокоманда или макрос - оператор программы, который подлежит обработке Макропроцессором (как мы дальше увидим, Макропроцессор обрабатывает не все операторы, а только ему адресованные).
Макроопределение - описание того, как должна обрабатываться макрокоманда, макроопределение может находиться в том же исходном модуле, что и макрокоманда или в библиотеке макроопределений.
Макрорасширение - результат выполнения макровызова, представляющий собой один или несколько операторов языка Ассемблера, подставляемых в исходный модуль вместо оператора макровызова. Пример обработки макровызова показан на рисунке.
Оператор макровызова в исходной программе имеет тот же формат, что и другие операторы языка Ассемблера: В нем есть метка (необязательно), мнемоника и операнды. При обработке исходного текста если мнемоника оператора не распознается как машинная команда или директива, она считается макрокомандой и передается для обработки Макропроцессору.
Макроопределение описывает, как должна обрабатываться макрокоманда. Средства такого описания составляют некоторый Макроязык. Для Макропроцессоров 1-й и 2-й категорий средства Макроязыка могут быть достаточно развитыми. Для Макропроцессоров 3-й категории средства Макроязыка могут быть довольно бедными, но в составе языка Ассемблера может быть много директив, применяемых в макроопределениях (возможно, - только в макроопределениях). В теле макроопределения могут употребляться операторы двух типов:
операторы Макроязыка, которые не приводят к непосредственной генерации операторов макрорасширения, а только управляют ходом обработки макроопределения; операторы языка Ассемблера (машинные команды и директивы), которые переходят в макрорасширение, возможно, с выполнением некоторых текстовых подстановок.
Поскольку макроопределение, обрабатывается перед трансляцией или вместе с ней, макрокоманда, определенная в исходном модуле, может употребляться только в этом исходном модуле и "не видна" из других исходных модулей. Для повторно используемых макроопределений обычно создаются библиотеки макроопределений. В некоторых системах (например, z/OS) макрокоманды обеспечивают системные вызовы и существуют богатейшие библиотеки системных макроопределений.
Самое очевидное применение макрокоманд - для сокращения записи исходной программы, когда один оператор макровызова заменяется на макрорасширение из двух и более операторов программы. В некоторых случаях макрорасширение может даже содержать и единственный оператор, но просто давать действию, выполняемому этим оператором более понятную мнемонику. Но возможности Макропроцессора гораздо шире. Так, одна и та же макрокоманда с разными параметрами может приводить к генерации совершенно различных макрорасширений - и по объему, и по содержанию.
Перемещенне в Настраивающем Загрузчике.
Принятые в Настраивающих Загрузчиках методы позволяют легко реализовать настройку реальных адресов, заданных относительно начала программы. Сущность метода перемещения состоит в том, что с каждым словом кода программы (размер слова обычно равен размеру реального адреса) связывается "бит перемещения". Значение этого бита 0/1 является признаком неперемещаемого/перемещаемого слова. Если слово является неперемещаемым, оно оставляется Загрузчиком без изменений. Если слово является перемещаемым, то к значению в слове прибавляется стартовый адрес модуля в оперативной памяти. Биты перемещения могут упаковываться - например, описание 8 слов в одном байте.
Переносимый машинный язык
Макросредствами может быть обеспечен полнофункциональный набор команд некоторой виртуальной машины. Программа пишется на языке этой виртуальной машины. Для разных платформ создаются библиотеки макроопределений, обеспечивающие расширение макровызовов в команды данной целевой платформы. Программа, таким образом, становится переносимой на уровне исходного текста. Поскольку макроопределение может быть построено так, чтобы генерировать неизбыточный код для каждого конкретного вызова, программа на языке виртуальной машины не будет уступать в эффективности программе, сразу написанной на языке целевого Ассемблера.
Системное программирование" для нашей специальности,
Начиная читать 2-ю часть курса " Системное программирование" для нашей специальности, я обнаружил, что в Сети нет электронной литературы по первым темам курса. Поэтому я спешно перевел в электронную форму и привел в читабельный вид свой старый рукописный конспект по нескольким темам. Для следующих тем курса, посвященных языкам высокого уровня и компиляторам, я надеюсь найти электронные источники на стороне. Основные источники, которые использовались при подготовке конспекта:
Донован Дж. Системное программирование. - М.: "Мир", 1975. Бек А. Введение в системное программирование. - М.: "Мир", 1988. Вишняков В.А., Петровский А.А. Системное обеспечение микроЭВМ. - Минск: "Вышэйшая школа", 1990.
А.Д.
Предложения языка Ассемблера
Предложения языка Ассемблера описывают команды или псевдокоманды (директивы). Предложения-команды задают машинные команды вычислительной системы; обработка Ассемблером команды приводит к генерации машинного кода. Обработка псевдокоманды не приводит к непосредственной генерации кода, псевдокоманда управляет работой самого Ассемблера. Для одной и той же аппаратной архитектуры м.б. построены разные Ассемблеры, в которых команды будут обязательно одинаковые, но псевдокоманды м.б. разные.
Во всех языках Ассемблеров каждое новое предложение языка начинается с новой строки. Каждое предложение, как правило, занимает одну строку, хотя обычно допускается продолжение на следующей строке/строках. Формат записи предложений языка м.б. жесткий или свободный. При записи в жестком формате составляющие предложения должны располагаться в фиксированных позициях строки. (Например: метка должна располагаться в позициях 1-8, позиция 9 - пустая, позиции 10-12 - мнемоника команды, позиция 13 - пустая, начиная с позиции 14 - операнды, позиция 72 - признак продолжения). Обычно для записи программ при жестком формате создаются бланки. Жесткий формат удобен для обработки Ассемблером (удобен и для чтения). Свободный формат допускает любое количество пробелов между составляющими предложения.
В общих случаях предложения языка Ассемблера состоят из следующих компонент:
метка или имя; мнемоника; операнды; комментарии.
Метка или имя является необязательным компонентом. Не во всех языках Ассемблеров эти понятия различаются. Если они различаются (например, MASM), то метка - точка программы, на которую передается управление, следовательно, метка стоит в предложении, содержащем команду; имя - имя переменной программы, ячейки памяти, следовательно, имя стоит в предложении, содержащем псевдокоманду резервирования памяти или определения константы. В некоторых случаях метка и имя могут отличаться даже синтаксически, так, в MASM/TASM после метки ставится двоеточие, а после имени - нет.
Однако, физический смысл и метки, и имени - одинаков, это - адрес памяти.
Во всех случаях, когда Ассемблер встречает в программе имя или метку, он заменяет ее на адрес той ячейки памяти, к которую имя/метка именует. Правила формирования имен/меток совпадают с таковыми для языков программирования. В некоторых Ассемблерах (HLAM S/390) не делается различия между меткой и именем.
В языке должны предусматриваться некоторые специальные правила, позволяющие Ассемблеру распознать и выделить метку/имя, например:
метка/имя должна начинаться в 1-й позиции строки, если метки/имени нет, то в 1-й позиции д.б. пробел, или за меткой/именем должно следовать двоеточие, и т.п.
Мнемоника - символическое обозначение команды/псевдокоманды.
Операнды - один или несколько операндов, обычно разделяемые запятыми. Операндами команд являются имена регистров, непосредственные операнды, адреса памяти (задаваемые в виде констант, литералов, символических имен или сложных выражений, включающих специальный синтаксис). Операнды псевдокоманд м.б. сложнее и разнообразнее.
Комментарии - любой текст, который игнорируется Ассемблером. Комментарии располагаются в конце предложения и отделяются от текста предложения, обрабатываемого Ассемблером, каким-либо специальным символом (в некоторых языках - пробелом). Всегда предусматривается возможность строк, содержащих только комментарий, обычно такие строки содержат специальный символ в 1-й позиции.
Присваивание значений переменным макроопределения
Присваивание может производиться оператором вида:
имя_переменной SET выражение
или
имя_переменной = выражение
Выражения, допустимые при присваивании, могут включать в себя имена переменных и параметров макроопределения, константы, строковые, арифметические и логические операции, функции. Основной тип операций - строковые (выделение подстроки, поиск вхождения, конкатенация. etc.), так как обработка макроопределения состоит в текстовых подстановках. Строковые операции обычно реализуются в функциях. Однако, в некоторых случаях может потребоваться выполнение над переменными макроопределения операций нестрокового типа. Как обеспечить выполнение таких операций? Можно предложить два варианта решения этой проблемы:
Ввести в оператор объявления переменной макроопределения определение ее типа. При выполнении операций должно проверяться соответствие типов. Все переменные макроопределения имеют строковый тип, но при вычислении выражений автоматически преобразуются к типу, требуемому для данной операции (при таком преобразовании может возникать ошибка). Результат выражения автоматически преобразуется в строку.
Как правило, операции присваивания могут применяться к параметрам макроопределения точно так же, как и к переменным макроопределения.
Программы и программное обеспечение
Определение (ГОСТ) | Программа - это данные, предназначенные для управления конкретными компонентами системы обработки информации (СОИ) в целях реализации определенного алгоритма. |
Определения даются по: ГОСТ 19781-90. Обеспечение систем обработки информации программное. Термины и определения. - М.:Изд-во стандартов, 1990.
Обратить внимание: программа - это данные. Один из основных принципов машины фон Неймана - то, что и программы, и данные хранятся в одной и той же памяти. Сохраняемая в памяти программа представляет собой некоторые коды, которые могут рассматриваться как данные. Возможно, с точки зрения программиста программа - активный компонент, она выполняет некоторые действия. Но с точки зрения процессора команды программы - это данные, которые процессор читает и интерпретирует. С другой стороны программа - это данные с точки зрения обслуживающих программ, например, с точки зрения компилятора, который на входе получает одни данные - программу на языке высокого уровня (ЯВУ), а на выходе выдает другие данные - программу в машинных кодах.
Определение (ГОСТ) | Программное обеспечение (ПО) - совокупность программ СОИ и программных документов, необходимых для их эксплуатации |
Существенно, что ПО - это программы, предназначенные для многократного использования и применения разными пользователями. В связи с этим следует обратить внимание на ряд необходимых свойств ПО. Необходимость документирования. По определению программы становятся ПО только при наличии документации. Конечный пользователь не может работать, не имея документации. Документация делает возможным тиражирование ПО и продажу его без его разработчика. По Бруксу ошибкой в ПО является ситуация, когда программное изделие функционирует не в соответствии со своим описанием, следовательно, ошибка в документации также является ошибкой в программном изделии. Эффективность. ПО, рассчитанное на многократное использование (например, ОС, текстовый редактор и т.п.) пишется и отлаживается один раз, а выполняется многократно.
Таким образом, выгодно переносить затраты на этап производства ПО и освобождать от затрат этап выполнения, чтобы избежать тиражирования затрат. Надежность. В том числе:
Тестирование программы при всех допустимых спецификациях входных данных Защита от неправильных действий пользователя Защита от взлома - пользователи должны иметь возможность взаимодействия с ПО только через легальные интерфейсы.
Готье: "Ошибки в системе возможны из-за сбоев аппаратуры, ошибок ПО, неправильных действий пользователя. Первые - неизбежны, вторые - вероятны, третьи - гарантированы".
Появление ошибок любого уровня не должно приводить к краху системы. Ошибки должны вылавливаться диагностироваться и (если их невозможно исправить) превращаться в корректные отказы.
Системные структуры данных должны сохраняться безусловно.
Сохранение целостности пользовательских данных желательно. Возможность сопровождения. Возможные цели сопровождения - адаптация ПО к конкретным условиям применения, устранение ошибок, модификация.
Во всех случаях требуется тщательное структурирование ПО и носителем информации о структуре ПО должна быть программная документация.
Адаптация во многих случаях м.б. передоверена пользователю - при тщательной отработке и описании сценариев инсталляции и настройки.
Исправление ошибок требует развитой сервисной службы, собирающей информацию об ошибках и формирующей исправляющие пакеты.
Модификация предполагает изменение спецификаций на ПО. При этом, как правило, должны поддерживаться и старые спецификации. Эволюционное развитие ПО экономит вложения пользователей.
Система прерываний
Является самым сложным для моделирования компонентом. Трудность состоит в том, что прерывания поступают асинхронно, без привязки к выполнению программы. Следовательно, прерывания должны "зарождаться" где-то вне собственно выполняемой программы. При выполнении Интерпретатора в пошаговом режиме прерывания могут задаваться командами, вводимыми человеком-оператором. Более универсальным является прием, предполагающий создание в отдельном файле "программы поступления прерываний". Каждый "оператор" этой "программы" содержит идентификатор типа прерывания и время (модельное) поступления прерывания. Эти "операторы" должны быть упорядочены по возрастанию времен поступления. Поскольку ВС обладают свойством непрерываемости команд, условие поступления прерывания может проверяться только после окончания обработки очередной команды. Действия по прерыванию определяются характеристиками конкретной ВС. Как правило, они включают в себя запоминание текущего значения регистров состояния и счетчика адреса и занесение в счетчик адреса адреса программной секции обработки прерывания данного типа. Отладке программ, предусматривающих обработку внешних прерываний, усложняется многократно, так как при этом должно быть предусмотрено поступление внешних прерываний во все возможные (и невозможные!) моменты выполнения.
Системное программирование
Определение (ГОСТ) | Системная программа - программа, предназначенная для поддержания работоспособности СОИ или повышения эффективности ее использования. |
Определение (ГОСТ) | Прикладная программа - программа, предназначенная для решения задачи или класса задач в определенной области применения СОИ. |
В соответствии с терминологией, системное программирование - это процесс разработки системных программ (в т.ч., управляющих и обслуживающих).
С другой стороны, по определению Гегеля система - единое целое, состоящее из множества компонентов и множества связей между ними. Тогда системное программирование - это разработка программ сложной структуры.
Эти два определения не противоречат друг другу, так как разработка программ сложной структуры ведется именно для обеспечения работоспособности или повышения эффективности СОИ.
Зафиксированное в ГОСТ подразделение ПО на системное и прикладное является до некоторой степени устаревшим. Сегодняшнее деление предусматривает по меньшей мере три градации ПО:
Системное Промежуточное Прикладное
Промежуточное ПО (middleware) мы определяем как совокупность программ, осуществляющих управление вторичными (конструируемыми самим ПО) ресурсами, ориентированными на решение определенного (широкого) класса задач. К такому ПО относятся менеджеры транзакций, серверы БД, серверы коммуникаций и другие программные серверы. С точки зрения инструментальных средств разработки промежуточное ПО ближе к прикладному, так как не работает на прямую с первичными ресурсами, а использует для этого сервисы, предоставляемые системным ПО. С точки зрения алгоритмов и технологий разработки промежуточное ПО ближе к системному, так как всегда является сложным программным изделием многократного и многоцелевого использования и в нем применяются те же или сходные алгоритмы, что и в системном ПО.
Современные тенденции развития ПО состоит в снижении объема как системного, так и прикладного программирования. Основная часть работы программистов выполняется в промежуточном ПО.
Снижение объема системного программирования определено современными концепциями ОС, объектно-ориентированной архитектурой и архитектурой микроядра, в соответствии с которыми большая часть функций системы выносится в утилиты, которые можно отнести и к промежуточному ПО. Снижение объема прикладного программирования обусловлено тем, что современные продукты промежуточного ПО предлагают все больший набор инструментальных средств и шаблонов для решения задач своего класса.
Значительная часть системного и практически все прикладное ПО пишется на языках высокого уровня, что обеспечивает сокращение расходов на их разработку/модификацию и переносимость.
Системное ПО подразделяется на системные управляющие программы и системные обслуживающие программы.
Определение (ГОСТ) | Управляющая программа - системная программа, реализующая набор функций управления, который включает в себя управление ресурсами и взаимодействие с внешней средой СОИ, восстановление работы системы после проявления неисправностей в технических средствах. |
Определение (ГОСТ) | Программа обслуживания (утилита) - программа, предназначенная для оказания услуг общего характера пользователям и обслуживающему персоналу СОИ. |
Кроме входящих в состав ОС утилит могут существовать и другие утилиты (того же или стороннего производителя), выполняющие дополнительное (опционное) обслуживание. Как правило, это утилиты, обеспечивающие разработку программного обеспечения для операционной системы.
Определение (ГОСТ) | Система программирования - система, образуемая языком программирования, компилятором или интерпретатором программ, представленных на этом языке, соответствующей документацией, а также вспомогательными средствами для подготовки программ к форме, пригодной для выполнения. |
Сравнение макросредств и подпрограмм
Использование макросредств во многом подобно использованию подпрограмм: в обоих случаях мы сокращаем запись исходного текста и создаем повторно используемые фрагменты кода. (Например, в C/C++ вызов псевдофункции неотличим от вызова функции.) Принципиальные различия между подпрограммами и макросредствами:
Команды, реализующие подпрограмму, содержатся в кода загрузочного модуля один раз, а команды, реализующие макровызов, включаются в программу для каждого применения макровызова (макросредства требуют больше памяти). Выполнение подпрограммы требует передачи управления с возвратом - команды типа CALL и RET, а команды макрорасширения включаются в общую последовательность команд программы (макровызовы выполняются быстрее). Если в многофункциональной подпрограмме имеется разветвление в зависимости от значений параметров, то в загрузочный модуль включается код подпрограммы в полном объеме, даже если в конкретной программе реально используется только одна из ветвей алгоритма; в макровызове в каждое макрорасширение включаются только операторы, определяемые фактическими значениями параметров макровызова (экономия и времени и объема в макровызовах).
Общий итог сравнения: макросредства обеспечивают несколько большее быстродействие при несколько больших затратах памяти. Поэтому обычно макросредства применяются для оформления сравнительно небольших фрагментов повторяющегося кода.
Структурный Ассемблер
В виде макрокоманд могут быть реализованы операторы, близкие к операторам управления потоком вычисления в языках высокого уровня (условные операторы, ветвления, различные виды циклов). Известным примером такого расширения является язык Макроассемблера BCPL - предшественник языка C.
Структуры данных Макропроцессора
Таблица макроопределений, строго говоря, не таблица, а просто массив строк, в который записываются тексты всех макроопределений (от оператора MACRO до оператора MEND), найденных в обрабатываемом модуле.
Таблица имен макроопределений содержит имена макроопределений и указатель на размещение текста макроопределения в таблице макроопределений, как показано на рисунке.
Таблица глобальных переменных имеет такую структуру:
Все таблицы имеют переменный размер и заполняются в процессе работы.
Индекс уникальных меток - число, используемое для формирования уникальной части имен меток, встречающихся в макроопределениях
Для обработки каждого макровызова создаются:
Таблица параметров, содержащая информацию о параметрах макроопределения.
Таблица локальных переменных, содержащая информацию о локальных переменных макроопределения.
Структура этих таблиц - такая же, как и таблицы глобальных переменных, эти две таблицы могут быть объединены в одну таблицу параметров и локальных переменных.
Таблица меток макроопределения, структура которой:
Связывание в Настраивающем Загрузчике.
Проблема связывания в Настраивающем Загрузчике решается при помощи Вектора Переходов. Вектор Переходов включается в состав объектного модуля и содержит список всех внешних имен, к которым есть обращение в модуле с полем адреса для каждого имени. Вектор Переходов заполняется при обработке директив типа EXT (перечисления внешних имен). В команды программы, обращающиеся к внешним именам вставляется обращение к адресному полю соответствующего элемента Вектора Переходов с признаком косвенного обращение. (Косвенное обращение означает, что обращение идет не по адресу, который задан в команде, а по адресу, который записан в ячейке, адрес которой задан в команде.)
При загрузке в оперативную память Вектор Переходов загружается вместе с кодами программы и остается в памяти все время выполнения программы.
Когда Загрузчик компонует программу из нескольких объектных модулей, он "узнает" все фактические адреса входных точек и в Вектора Переходов тех модулей, которые обращаются к данной входной точке вставляет эти адреса. Обращение к внешней точке, таким образом, производится косвенное через Вектор Переходов.
Тема 2. Ассемблеры
Определение (не по ГОСТ) | Язык Ассемблера - система записи программы с детализацией до отдельной машинной команды, позволяющая использовать мнемоническое обозначение команд и символическое задание адресов. |
Поскольку в разных аппаратных архитектурах разные программно-доступные компоненты (система команд, регистры, способы адресации), язык Ассемблера аппаратно-зависимый. Программы, написанные на языке Ассемблера м.б. перенесены только на вычислительную систему той же архитектуры.
Программирование на языке Ассемблера позволяет в максимальной степени использовать особенности архитектуры вычислительной системы. До недавнего времени воспринималась как аксиома, что Ассемблерная программа всегда является более эффективной и в смысле быстродействия, и в смысле требований к памяти. Для Intel-архитектуры это и сейчас так. Но это уже не так для RISK-архитектур. Для того, чтобы программа могла эффективно выполняться в вычислительной среде с распараллеливанием на уровне команд, она д.б. определенным образом оптимизирована, т.е., команды д.б. расположены в определенном порядке, допускающим их параллельное выполнение. Программист просто не сможет покомандно оптимизировать всю свою программу. С задачей такой оптимизации более эффективно справляются компиляторы.
Доля программ, которые пишутся на языках Ассемблеров в мире, неуклонно уменьшается, прикладное программирование на языках Ассемблеров применяется только по недомыслию. Язык Ассемблера "в чистом виде" применяется только для написания отдельных небольших частей системного ПО: микроядра ОС, самых нижних уровней драйверов - тех частей, которые непосредственно взаимодействуют с реальными аппаратными компонентами. Этим занимается узкий круг программистов, работающих в фирмах, производящих аппаратуру и ОС. Зачем же нам тогда изучать построение Ассемблера?
Хотя разработка программ, взаимодействующих с реальными аппаратными компонентами, - редкая задача, в современном программировании при разработке прикладного, а еще более - промежуточного ПО довольно часто применяется технологии виртуальных машин. Для выполнения того или иного класса задач программно моделируется некоторое виртуальное вычислительное устройство, функции которого соответствуют нуждам этого класса задач. Для управления таким устройством для него м.б. создан соответствующий язык команд. (Широко известные примеры: MI AS/400, JVM.) Говоря шире, любую программу можно представить себе как виртуальное "железо", решающее конкретную задачу. (Конечный пользователь обычно не видит разницы между программой и аппаратурой и часто говорит не "мне программа выдала то-то", а "мне компьютер выдал то-то"). В некоторых случаях интерфейс программы м.б. удобно представить в виде системы команд, а следовательно, нужен соответствующий Ассемблер. (Это, конечно, относится не к программам "для чайников", а к инструментальным средствам программистов, системам моделирования и т.п.).
Тема 5. Кросс-системы
Исходная вычислительная система (ВС) - та ВС, на которой программа готовится к выполнению.
Целевая ВС - та ВС, на которой программа выполняется.
Эти две ВС не обязательно совпадают. Макропроцессор, Ассемблер, Редактор Связей - программы, обрабатывающие данные. Ассемблер, например, получает на входе одни код (текст) и производит на выходе другой код (объектный модуль). При этом Ассемблер не рассматривает свой выходной код как команды именно своей ВС, это просто некоторые данные. Ничто не мешает нам сделать Ассемблер, на выходе которого будут генерироваться коды не той ВС, в которой работает Ассемблер, а некоторой другой ВС.
Системы подготовки программ, в которых исходная ВС отличается от целевой, называются кросс-системами.
Для чего может понадобиться кросс-система?
Часто кросс-системы применяются для разработки программного обеспечения встроенных вычислительных систем. Для встроенных ВС характерен малый объем ресурсов: ограничение оперативной памяти, возможно, отсутствие внешней памяти (программы и постоянные данные размещаются в ПЗУ), отсутствие должного набора внешних устройств. Иногда ресурсов целевой ВС просто недостаточно для выполнения на ней системного программного обеспечения подготовки программ, тем более - для выполнения интерактивных систем программирования с развитым интерфейсом пользователя. При разработке новых ВС создание программного обеспечения для них ведется параллельно с разработкой аппаратной части. Подготовка и отладка программ для проектируемой ВС должна вестись, когда целевой ВС еще не существует физически.
В простейшем случае процесс подготовки программы на кросс-системе аналогичен процессу их подготовки в однородной системе.
Загрузка и выполнение должна обязательно происходить на целевой системе, а предшествующие этапы (все или некоторые) могут быть перенесены на исходную систему. Любой из файлов-результатов выполнения очередного этапа подготовки может быть перенесен из исходной системы в целевую.
Однако, такая схема не в полной мере использует возможности кросс-системы.
Важным этапом подготовки программ является их отладка. Отладка также может проводиться на кросс-системе (частично или полностью)
Для отладки программ на исходной ВС применяется программа-Интерпретатор. Интерпретатор является программной моделью целевой ВС, которая обеспечивает выполнение программ в кодах целевой ВС. Интерпретатор строится по принципу имитационной программной модели, это означает, что отдельные компоненты целевой ВС моделируются соответствующими компонентами программной модели, которые имитируют поведение реальных компонентов.
Если исходная ВС обладает большими вычислительными ресурсами, чем целевая ВС, то отладка на исходной ВС может быть более удобной и функционально более полной, чем на целевой ВС. Это, впрочем, относится и к тому случаю, когда исходная ВС не превосходит целевую по объему ресурсов. В таком случае для отладки программы все равно может быть выделено больше ресурсов (возможно, виртуальных), чем при ее выполнении.
Модель, на которой производится отладка, всегда является избыточной по ресурсам по сравнению с целевой средой выполнения. Избыточность модели позволяет выявить при отладке на ней такие ошибочные ситуации, которые трудно или вообще невозможно выявить при отладке в реальной среде. Примеры таких ситуаций:
обращение по адресу несуществующей памяти попытка записи в защищенную от записи память модификация программой команд и констант передача управления на данные выборка неинициализированных данных и т.п.
Модель целевой вычислительной системы состоит из компонентов, моделирующих программно-доступные компоненты целевой ВС (т.е. такие, с которыми работают команды отлаживаемой программы) и включает в себя следующие составляющие:
модель регистров модель оперативной памяти модель процессора модель системы прерывания модель системы ввода-вывода.
Уникальные метки
В некоторых случаях операторы машинных команд, имеющихся в макроопределении, должны быть помечены, например, для того, чтобы передавать на них управление. Если применить для этих целей обычную метку, то может возникнуть ошибочная ситуация. Если метка в макроопределении имеет обычное имя, и в модуле данная макрокоманда вызывается два раза, то будет сгенерировано два макрорасширения, и в обоих будет метка с этим именем. Чтобы избежать ситуации неуникальности меток, в макроязыке создается возможность определять метки, для которых формируются уникальные имена. Обычно имя такой метки имеет тот же отличительный признак, который имеют параметры и переменные макроопределения. Каждую такую метку Макропроцессор заменяет меткой с уникальными именем.
Уникальное имя метки может формироваться формате, подобном следующему:
&имя.nnnnnn
где - nnnnnn - число, увеличивающееся на 1 для каждой следующей уникальной метки.
Другой возможный способ формирования, например:
имя&SYSNDX
где SYSNDX - предустановленное имя, имеющее числовое значение, начинающееся с 00001 и увеличивающееся на 1 для каждой следующей уникальной метки.
Следующие операторы Макроязыка влияют на последовательность обработки операторов макроопределения. В тех или иных Макропроцессорах имеется тот или иной набор таких операторов.
Условные блоки
Возможный формат оператора:
IF условное_выражение операторы_макроопределения_блок1
ENDIF ELSE операторы_макроопределения_блок2
ENDIF
Если условное_выражение имеет значение "истина", обрабатываются операторы макроопределения от оператора IF до оператора ENDIF, иначе обрабатываются операторы макроопределения от оператора ESLE до оператора ENDIF. Как и в языках программирования блок ELSE - ENDIF не является обязательным.
Условные выражения описаны выше. Обычно предусматриваются специальные формы:
IFDEF имя
IFNDEF имя
проверяющие просто определено или не определено данное имя.
Операторы условных блоков довольно часто являются не операторами Макроязыка, а директивами самого языка Ассемблера.
Вложенные макровызовы. Вложенные макроопределения.
Можно ли употреблять макроопределения внутри макроопределений? Можно ли употреблять макровызовы вызовы внутри макроопределений? Представленные выше алгоритмы делать этого не позволяют. Тем не менее, можно построить такие алгоритмы Макропроцессора, которые это позволять будут. Эти алгоритмы в любом случае будут довольно "затратными", то есть, требующими много ресурсов - процессорного времени и памяти. "Классические" алгоритмы, создававшиеся в условиях хронического дефицита памяти, были очень ограничены.
Время
Для значительного класса встроенных ВС время выполнение программы является принципиально важной ее характеристикой (например, бортовые системы управления должны работать в реальном времени). Важно понимать, что время выполнения программы на Интерпретаторе ни в коей мере не соответствует времени ее выполнения на реальной ВС. Более того, временные соотношения между выполнением различных частей программы на модели также не соответствуют соотношениям выполнения частой программы на реальном оборудовании. Поэтому время также является моделируемым компонентом. Моделью времени является целая переменная большой разрядности. В этой переменной на каждом шаге выполнения содержится число машинных тактов, выполненных с начала выполнения программы. Исходное значение этой переменной - 0, после выполнения каждой команды ее значение увеличивается на время выполнения данной команды (время выполнения может быть столбцом в таблице команд).
Ввод-вывод
Операции ввода-вывода целевой ВС моделируются файловым вводом-выводом исходной ВС. Данные, которые целевая ВС вводит с внешнее устройство, читаются моделью из файла. Данные, которые целевая ВС выводит на внешнее устройство, записываются моделью в файл. Для каждого внешнего устройства удобно назначать свой файл. В частном случае это может быть файл клавиатуры или файл экрана. Данные в файл, имитирующий устройство ввода, должны быть занесены заранее. На вход Интерпретатора должна подаваться таблица соответствия файлов устройствам.
Ввод-вывод может быть синхронным или асинхронным. При синхронном вводе-выводе (например, через порты) операция ввода-вывода завершается вместе с завершением команды ввода-вывода. Моделирование такого ввода-вывода сложностей не представляет. При асинхронном вводе-выводе (КПДП, каналы ввода-вывода) команда ввода-вывода только запускает операцию ввода-вывода и заканчивается. Выполнение операции ввода-вывода далее происходит параллельно с выполнением команд программы, а об окончании ввода-вывода устройство сигнализирует прерыванием. И здесь "срабатывают" трудности, присущие моделированию системы прерываний. Одно из возможных решений - при инициализации операции ввода-вывода добавлять в программу поступления прерываний новый элемент, соответствующий прерыванию, которое поступит через какой-то интервал времени после текущего момента.
Особую сложность представляет собой моделирование ошибочных ситуаций ввода-вывода, эта проблема должна решаться для каждого прикладного случая, поэтому здесь не рассматривается.
Выдача сообщения
При возникновении ошибок или ситуаций, требующих предупреждения программисту в листинг должно выводиться сообщение. Если в результате ошибки программиста, написавшего макроопределение или макровызов будет сгенерирован неправильный код программы на языке Ассемблера, то эта ошибка будет выявлена только Ассемблером на этапе трансляции программы. Однако выгоднее выявлять ошибки не как можно более ранних этапах подготовки программы, в Макроязыке ошибочные ситуации (ошибки в параметрах и т.п.) могут быть выявлены при помощи условных операторов или блоков, а для выдачи сообщения об ошибке должен существовать специальный оператор Макроязыка. Формат такого оператора примерно следующий:
MOTE код_серьезности,код_ошибки,сообщение_об_ошибке
код_серьезности - числовой код, определяющий возможность продолжения работы при наличии ситуации, вызвавшей сообщения. Должны индицироваться, как минимум, следующие ситуации:
работа Макропроцессора может быть продолжена, по окончании ее может выполняться ассемблирование; работа Макропроцессора может быть продолжена, но ассемблирование выполняться не может; работа Макропроцессора не может продолжаться.
код_ошибки - числовой код, служащий, например, для поиска развернутого описания сообщений и действий при его возникновении в документе "Сообщения программы"
сообщение_об_ошибке - текст, печатаемый в листинге
Взаимодействие с человеком-оператором
Интерпретатор может выполняться в автоматическом или пошаговом режиме. В автоматическом режиме Интерпретатор моделирует выполнение команд программы без остановок до команды типа HALT или до точки останова. В точке останова оператор может вводить команды, управляющие действиями Интерпретатора и выбрать режим продолжения выполнения. В пошаговом режиме Интерпретатор после выполнения каждой команды программы останавливается и предоставляет оператору возможность вводить команды управления. Командами управления работой Интерпретатора могут быть:
команды на отображение/изменение состояния/содержимого компонентов модели; команды задания точек останова; команды моделирования прерываний; команды установки режима выполнения; команда окончания работы.
Отображаться должны состояния и значения всех составляющих программной модели ВС: регистров (РОН, счетчика адреса, состояния), заданных участков памяти и их признаков, счетчика модельного времени, программы поступления прерываний и т.д. Отображаемые значения также должны быть доступны для изменений. Отметим, что для интерактивного отображения/изменения должны быть доступны также байты признаков памяти. Изменение содержимого регистра-счетчика адреса равносильно передачи управления в программе.
Точки останова могут задаваться в исходном для Интерпретатора файле и вводиться/изменяться в ходе интерактивной отладки. Могут быть предусмотрены остановы при:
передаче управления по заданному адресу; чтении данных по заданному адресу; записи данных по заданному адресу.
Связь отладки с исходным текстом. Такая связь безусловно удобно и может быть осуществлена относительно несложно, если выход 1-го прохода Кросс-Ассемблера передается на вход Интерпретатора. Выход 1-го прохода связывает операторы исходного текста с адресами памяти. Таким образом, по значению счетчика адреса в каждый момент выполнения программы можно найти в выходе 1-го прохода соответствующий оператор исходного текста. Если на вход Интерпретатора подается также сформированная 1-ым проходом таблица символов, то есть возможность обращаться к переменным программы и к точкам передачи управления по символьным именам.
Можно ли обеспечить изменение прямо в ходе отладки исходного текста? Схема решения сводится к представленной на рисунке.
В схеме остается только 1-й проход Кросс-Ассемблера. Выход его - исходный текст с разметкой адресов и таблица символов является основным входом Интерпретатора. Необходимость во 2-ом проходе Кросс-Ассемблера отпадает. В начале выполнения Интерпретатор должен построить модель памяти, в которой он размещает, однако, только данные программы, но не команды. При работе Интерпретатор повторяет многие действия 2-го прохода Кросс-Ассемблера, читает не коды, а исходные тексты и распознает команду не по коду операции, а по мнемонике, и интерпретирует операнды не по кодам, а по исходным текстам. Изменения в исходном тексте оператора программы должны автоматически реплицироваться в соответствующем операторе (только в одном операторе!) результата 1-го прохода, и тогда при следующем выполнении этого оператора будет моделироваться уже выполнение новой команды. Однако, поскольку в результате 1-го прохода каждый оператор уже привязывается к определенному адресу, возможность изменения должна ограничиваться тем, что длина новой команды обязательно равна длине старой команды. Более сложные изменения потребуют повторного выполнения 1-го прохода Кросс-Ассемблера.
Итоговая схема алгоритма функционирования Интерпретатора сводится к следующей:
|
Запуск Интерпретатора. Открытие исходных файлов - результатов работы Кросс-Ассемблера и файлов с управляющей информацией (описание файлов - внешних устройств, программа поступления прерываний, описание фрагментов памяти и отдельных ячеек и т.п.). Считывание управляющей информации. Установка начальных значений для компонентов модели (содержимое памяти, регистры, счетчик модельного времени). Интерактивное задание/корректировка управляющей информации (режим выполнения, точки останова и т.п.). Автоматический режим? Если установлен пошаговый (не автоматический) режим выполнения, выполняется ввод и обработка команд оператора в интерактивном режиме. Эта обработка может заканчиваться либо продолжением выполнения интерпретатора в пошаговом или автоматическом режиме, либо завершением его работы по команде оператора. Если установлен автоматический режим выполнения, но текущее значение регистра - счетчика адреса совпадает с одной из заданных точек останова, также выполняется ввод и обработка команд оператора в интерактивном режиме. Проверяется счетчик модельного времени сравнивается с временем поступления первого прерывания в списке прерываний. Если счетчик модельного времени больше или равен времени поступления первого прерывания в списке, выполняется сохранение текущего состояния и занесение в регистр-счетчик адреса секции обработки прерывания данного типа. Первый элемент удаляется их списка прерываний и происходит возврат на начало итерации обработки команды. Если прерывание не поступило, выбирается первый байт команды (при отладке по объектному модулю) или ее мнемоника (при отладке по исходному тексту). Код операции или мнемоника команды ищется в таблице команд. При неуспешном поиске Интерпретатор заканчивается с сообщением об ошибке. Выбор операндов из кода команды или из текста оператора. Проверка правильности кодирования операндов, проверка корректности обращения к памяти. При ошибках в операндах или в обращении к памяти Интерпретатор заканчивается с сообщением об ошибке. Задан ли для адреса операнда останов при обращении? Если да - возврат на выполнение команд в интерактивном режиме. Интерпретация команды и запись результата Вычисление и занесение в регистр-счетчик адреса следующей команды. Проверка, является ли адрес в регистре-счетчике адреса адресом 1-го байта команды Если это не так, Интерпретатор заканчивается с сообщением об ошибке. Модификация счетчика модельного времени и переход на выполнение следующей команды. |
при обнаружении ошибки в программе; при вводе оператором интерактивной команды завершения работы; при обработке команды останова (HALT) в программе.
Заголовок макроопределения
Макроопределение должно как-то выделяться в программе, поэтому оно всегда начинается с заголовка.
Заголовок имеет формат, подобный следующему:
имя_макрокоманды MACRO список формальных параметров
имя_макрокоманды является обязательным компонентом. При макровызове это имя употребляется в поле мнемоники оператора. Имена макроопределений, имеющихся в программе, должны быть уникальны. Обычно при распознавании макровызова поиск по имени макрокоманды ведется сначала среди макроопределений имеющихся в программе, а затем (если в программе такое макроопределение не найдено) - в библиотеках макроопределений. Таким образом, имя макрокоманды, определенной в программе, может совпадать с именем макрокоманды, определенной в библиотеке, в этом случае макрокоманда, определенная в программе, заменяет собой библиотечную.
Формальные параметры играют ту же роль, что и формальные параметры процедур/функций. При обработке макровызова вместо имен формальных параметров в теле макроопределения подставляются значения фактических параметров макровызова.
В развитых Макроязыках возможны три формы задания параметров: позиционная, ключевая и смешанная. При использовании позиционной формы соответствие фактических параметров формальным определяется их порядковым номером. (Позиционная форма всегда применяется для подпрограмм).
Пример:
Заголовок макроопределения | Макровызов | Результат подстановки | M1 MACRO A,B,C | M1 X,Y,Z | A=X, B=Y, C=Z |
В позиционной форме количество и порядок следования фактических параметров макровызова должны соответствовать списку формальных параметров в заголовке макроопределения. При использовании ключевой формы каждый фактический параметр макровызова задается в виде:
имя_параметра=значение_параметра
В таком же виде они описываются и в списке формальных параметров, но здесь значение_параметра может опускаться. Если значение_параметра в списке формальных параметров не опущено, то это - значение по умолчанию. В макровызове параметры могут задаваться в любом порядке, параметры, имеющие значения по умолчанию, могут опускаться.
Пример:
Заголовок макроопределения | Макровызов | Результат подстановки | M1 MACRO A=Q,B=,C=R | M1 C=Z,B=X | A=Q, B=X, C=Z |
Пример:
Заголовок макроопределения | Макровызов | Результат подстановки | M1 MACRO A,B,C=Q,D=,E=R | M1 X,Y,Z,D=T,E=S | A=X,B=Y,C=Q,D=T,E=S |
Логика, которой следует большинство Макропроцессоров в этом вопросе, такова. &PA является именем в соответствии с правилами формирования имен. Поэтому оно не распознается как имя &P и остается без изменений. Если мы хотим, чтобы подстановка в этой подстроке все-таки произошла, следует поставить признак, отделяющий имя параметра от остальной части строки. Обычно в качестве такого признака используется точка - '.': '&P.A' заменяется на 'XA'.
Завершение обработки
Обработка макроопределения завершается при достижении оператора MEND. Однако, поскольку алгоритм обработки макроопределения может разветвляться, должна быть предусмотрена возможность выхода из обработки и до достижения конца макроопределения. Эта возможность обеспечивается оператором MEXIT. Операндом этого оператора может быть код_серьезности.