АСЕМБЛЕР Учебно-методический комплекс по дисциплине "Системное программное обеспечение". Составитель: доцент кафедры МОВС Андрей Леонидович Бескин. Информация о студентах Анкетирование База данных по студентам Для студентов Поиск по серверу Отчетные материалы Контрольные работы Вопросы к экзамену и зачету Примерные темы курсовых работ Требования к техническому заданию Дополнительная литература Книга А. Бека "Введение в системное программирование" Основные понятия Ассемблеры Загрузчики и программы связывания Макропроцессоры Операционные системы Приложения к книге А. Бека "Введение в системное программирование" Система команд УУМ и УУМ/ДС Форматы команд и способы адресации УУМ/ДС и УММ Программа COPY Форматы объектных программ Алгоритмы Модельная программа,иллюстрирующая связывания и перемещения Объектная программа для иллюстрации связывания и перемещения Программа копирования файла Условные предложения периода макрогенерации Предложения условного перехода периода макрогенерации Ключевые параметры макропроцессора для УУМ/ДС Система прерывания УУМ/ДС Процедуры ввода-вывода для УУМ/ДС Процессы с запросами на ввод-вывод SVC-2 и SVC-0 FTP-архив /labs - выполненные студентами работы /binary - программы и дистрибутивы /posobiya - пособия по курсу СПО /office97 - оффис 97 Глава I. Основные понятия В данной главе содержится различная информация, которая послужит нам основой при изучении последующих глав. В разд. 1.1 приводится обзор структуры книги и дается краткое введение в системное программное обеспечение. С разд. 1.2 начинается обсуждение взаимосвязи между системным программным обеспечением и структурой ЭВМ, которое продолжится в дальнейшем на протяжении всей книги. В разд. 1.3-1.7 даются общие сведения об архитектуре некоторых ЭВМ, используемых далее в качестве примеров. Более детальное обсуждение большинства вопросов, касающихся архитектуры машин, можно найти в работах Танненбаум [1984], Пфлигер [1982] и Гир [1981]. Основная часть данной главы содержит лишь общие сведения; многие детали опущены. Уровень детализации выбран таким, чтобы он обеспечивал лучшее понимание последующих глав. Не следует стремиться запоминать материал этой главы или углубляться в малозначительные вопросы. Вместо этого рекомендуется главу прочитать, а затем, при изучении последующих глав, использовать ее по мере необходимости в качестве справочника. Для читателей, желающих получить более подробную информацию по тем или иным вопросам, приводятся необходимые ссылки на литературу. 1.1. Введение Эта книга является введением в проектирование различных компонентов системного программного обеспечения. Мы также рассмотрим реализацию такого программного обеспечения для некоторых реально существующих машин. Одна из центральных проблем книги - взаимосвязь системного программного обеспечения и архитектуры ЭВМ. Очевидно, что структура целевой машины оказывает влияние на выбор тех или иных решений, применяемых при создании системных программ. Некоторые аспекты такого влияния обсуждаются в разд. 1.2, другие будут рассмотрены в остальных главах книги. Основные темы, которые рассматриваются в данной книге, касаются ассемблеров, загрузчиков, макропроцессоров, компиляторов и операционных систем. Каждая из гл. 2-6 посвящена одной из них. Предполагается, что с точки зрения пользователя читатель знаком со всеми рассматриваемыми здесь системными компонентами. В первую очередь в данной книге затрагиваются вопросы проектирования и реализации системного обеспечения. В гл. 7 содержится обзор некоторых других важных системных компонентов: систем управления базами данных, текстовых редакторов и систем интерактивной отладки. Глубина изложения материала неодинакова. Главы, посвященные ассемблерам, загрузчикам и макропроцессорам, содержат достаточно детальную информацию, позволяющую подготовить читателя к тому, чтобы он мог самостоятельно написать данные компоненты программного обеспечения для реальных машин. С другой стороны, компиляторы и операционные системы являются слишком обширными темами, каждая из которых сама по себе не раз была объектом многих отдельных монографий и курсов. Очевидно, что невозможно полностью рассмотреть какую-либо из этих тем в одной главе сколько-нибудь разумного размера. Поэтому вместо этого предлагается введение в наиболее важные понятия и вопросы, относящиеся к компиляторам и операционным системам; особое внимание уделяется взаимосвязи программного обеспечения и структуры ЭВМ. Другие подтемы обсуждаются настолько, насколько позволяет место, и снабжаются ссылками на литературу, предназначенными для читателей, желающих изучить их более глубоко. Наша цель - дать хороший общий обзор рассматриваемых тем, который мог бы послужить в дальнейшем базой при изучении студентами специальных курсов по программированию. Аналогичный подход используется и при изложении материала гл. 7. 1.2. Системное программное обеспечение и структура ЭВМ. Машинная зависимость является одной из характеристик, которая обычно отличает системное программное обеспечение от прикладного. Прикладная программа интересует нас главным образом с точки зрения решения некоторой задачи. При этом ЭВМ используется как инструмент и основное внимание сосредоточено на предметной стороне дела, а не на вычислительной системе. С другой стороны, системные программы предназначены скорее для обеспечения управления функционированием собственно ЭВМ, чем для решения какой-либо конкретной задачи. Вследствие этого они обычно тесно связаны со структурой машины, для которой созданы. Например, ассемблеры при переводе мнемонических инструкций в машинный код непосредственно учитывают форматы команд, способы адресации и другие аппаратные характеристики целевой машины. Компиляторы должны генерировать код на машинном языке, учитывая такие характеристики аппаратуры, как число и способ использования регистров или имеющиеся в наличии команды. Операционные системы непосредственно отвечают за управление практически всеми ресурсами вычислительной системы. В дальнейшем мы увидим много других примеров машинной зависимости. С другой стороны, системное программное обеспечение имеет ряд аспектов, непосредственно не связанных с типом вычислительной системы, которую они поддерживают. Так, общая схема и алгоритмы ассемблера в основном не различаются для большинства ЭВМ. Некоторые из способов оптимизации объектного кода, используемые в компиляторах, не зависят от целевой машины (хотя существует также и машиннозависимая оптимизация). Точно так же обычно не зависит от используемой ЭВМ и процесс установления связей между отдельно ассемблированными подпрограммами (Утверждение автора не совсем точно. Способы установления связей и передачи параметров между раздельно ассемблированными программами зависят очень заметно от структурных особенностей ЭВМ. - Прим, ред.). В последующих главах мы рассмотрим много других примеров аналогичных машинно-независимых характеристик. Ввиду того что большинство системных программ машинно-зависимые, мы должны в процессе обучения рассматривать реальные машины и реальные компоненты программного обеспечения. Однако большинство реальных ЭВМ обладает определенными нестандартными или даже уникальными особенностями. Поэтому порой бывает трудно отличить действительно основные свойства программного обеспечения от свойств, зависящих исключительно от специфических особенностей конкретной машины. Для того чтобы обойти эти трудности, мы для изучения основных функций каждого из компонентов программного обеспечения будем использовать Упрощенную Учебную Машину (УУМ). УУМ представляет собой гипотетическую ЭВМ, при проектировании которой ставилась цель включить в нее аппаратные возможности, наиболее часто встречающиеся в реальных ЭВМ, исключив в то же время не относящиеся к существу дела или редко встречающиеся сложности. Таким образом, основные концепции каждого компонента системного программного обеспечения могут быть четко отделены от деталей реализации, связанных с конкретной машиной. Такой подход даст читателю отправную точку, с которой он может начать проектирование системного программного обеспечения для новой или ранее незнакомой ему ЭВМ. Каждая основная глава книги (гл. 2-6) начинается с описания базовых функций обсуждаемого компонента системного обеспечения. Затем рассматриваются машиннонезависимые функции, являющиеся расширением базовых. В заключительной части главы содержатся примеры реализации системных программ для реальных машин. Основные главы содержат следующие разделы: 1. Основные функции, свойственные данному типу программного обеспечения и не зависящие от реализации. 2. Возможности, наличие и особенности которых тесно связаны с машинной архитектурой. 3. Другие возможности, которые имеют относительную машинную независимость и характерны для большинства реализаций программного обеспечения данного типа. 4. Основные варианты построения конкретных компонентов программного обеспечения (например, однопросмотровый разбор в сравнении с многопросмотровым). 5. Примеры реализации для реальных ЭВМ, в которых основное внимание уделяется нестандартным свойствам программного обеспечения, связанным с машинными характеристиками. В данной главе содержатся краткие описания реальных ЭВМ. В дальнейшем эти машины будут использоваться для иллюстрации обсуждаемого материала. Сейчас мы предлагаем вам прочитать эти описания, а затем обращаться к ним, когда это необходимо, при изучении приводимых в каждой главе примеров. 1.3. Упрощенная учебная машина (УУМ) В этом разделе мы опишем архитектуру нашей УУМ. Эта машина была спроектирована для того, чтобы проиллюстрировать наиболее часто встречающиеся аппаратные концепции и возможности, избегая в то же время большинства специфических особенностей, присущих реальным машинам. Во многих отношениях УУМ похожа на типичную микро-ЭВМ. Подобно многим другим ЭВМ, которые выпускаются промышленностью, УУМ имеется в двух вариантах: в виде стандартной модели и модели УУМ/ДС (где индекс ДС означает с "дополнительными средствами" или, возможно, за "дополнительную стоимость"). Оба варианта спроектированы таким образом, чтобы обеспечивалась совместимость "снизу вверх". Это означает, что объектная программа для стандартной модели УУМ будет правильно выполняться и на УУМ/ДС. (Такая совместимость часто встречается в реально существующих семействах ЭВМ.) Характеристики стандартной модели УУМ описываются в разд. 1.3.1, а дополнительные возможности, включенные в УУМ/ДС, - в разд. 1.3.2. 1.3.1. Структура УУМ Память Оперативная память состоит из 8-разрядных байтов. Три последовательных байта составляют слово (24 разряда). УУМ имеет байтовую адресацию. Слова адресуются по адресу байта с наименьшим номером. Общий объем оперативной памяти составляет 32768 (215) байт. Регистры Имеется пять регистров, у каждого из которых есть собственное назначение. В таблице, приведенной ниже, указаны мнемонические имена регистров, их номера и назначение. (Система нумерации регистров выбрана таким образом, чтобы была обеспечена совместимость с моделью УУМ/ДС.) Имя Номер А 0 X L 1 2 PC 8 SW 9 Назначение Сумматор. Используется при выполнении арифметических операций Индексный регистр. Используется для адресации Регистр связи. Команда перехода на подпрограмму (JSUB) запоминает в этом регистре адрес возврата Счетчик команд. Данный регистр содержит адрес очередной команды, выбираемой для исполнения Слово состояния. Данный регистр содержит системную информацию, включая код условия (СС- Conditional Code) Форматы данных Значения целого типа хранятся в виде 24-разрядного двоичного числа. Для представления отрицательных чисел используется дополнительный код. Значения символьного типа хранятся в 8-разрядном коде ASCII (см. приложение Б). Аппаратные средства для выполнения действий над числами с плавающей точкой в стандартной модели УУМ отсутствуют. Форматы команд Все машинные команды стандартной модели УУМ имеют следующий 24разрядный формат: 8 1 15 код операции (ор) Х адрес (addr) Признак x используется для задания индексного способа адресации. Способы адресации Имеются два способа адресации. В команде они задаются разрядом х. Правила вычисления целевого адреса, (target address) по адресу, заданному в команде, описываются ниже следующей таблицей. (Скобки используются для указания на содержимое регистра или ячейки оперативной памяти; например, (Х) обозначает содержимое регистра Х.) Способ Признак Вычисление адресации адресации целевого адреса ________________________________________________ Прямая x=0 TA=addr Индексная x=l TA=addr+(X) ________________________________________________ Система команд Для решения большинства простых задач вполне достаточно базового набора команд УУМ. Он включает команды загрузки регистра и записи его содержимого и память (LDA, LDX, STA, STX и т. п.), а также команды целочисленной арифметики (ADD, SUB, MUL, DIV). Все арифметические команды выполняются над содержимым сумматора и содержимым слова оперативной памяти. Результат остается в сумматоре. Специальная команда (СОМР- СОМРаге) служит для сравнения значения, содержащегося в сумматоре, со значением, хранимым в слове оперативной памяти. Эта команда устанавливает код условия СС, являющийся признаком результата сравнения (<, =,>). Команды условного перехода (JLT, JEQ, JGT) проверяют установленное значение СС и выполняют соответствующую передачу управления. Две команды предназначены для организации взаимодействия подпрограмм: JSUВ - переход на подпрограмму с занесением адреса возврата в регистр L; RSUB - возврат по адресу, содержащемуся в регистре L. В приложении А приведен полный список всех команд УУМ и УУМ/ДС с указанием их кодов и описанием выполняемых функций. Средства ввода-вывода В стандартной модели УУМ ввод и вывод выполняются побайтно. Для обмена используется самый правый байт сумматора. Каждому внешнему устройству присвоен уникальный 8-разрядный код. Существует три команды ввода-вывода. Каждая из этих команд в качестве своего операнда задает код устройства. Команда проверки состояния устройства (TD- Test Device) проверяет готово ли требуемое устройство передать или принять очередной байт данных. Для индикации результата проверки используется код условия. Значение кода условия "<" указывает на готовность устройства к обмену; значение "=" означает, что устройство занято; значение ">" означает что данное устройство неисправно или не подключено к машине. Программа, желающая выполнить обмен, должна ждать до тех пор, пока устройство не будет готово, и только после этого она может выполнить команду чтения данных (RD - Read Data) или команду записи данных (WD - Write Data). Эта последовательность действий должна быть повторена для каждого байта данных, участвующего в обмене. Программа, показанная на рис. 2.1 (гл. 2), иллюстрирует такой способ выполнения обмена. 1.3.2. Структура УУМ/ДС Память Структура памяти УУМ/ДС аналогична описанной ранее для УУМ. Однако максимальный объем памяти доступной в УУМ/ДС составляет 1 Мбайт (220 байт). Такое увеличение требует изменения формата команд и способов адресации. Регистры Дополнительные регистры УУМ/ДС приведены в следующей таблице: Имя Номер В 3 S 4 Т F 5 6 Назначение Базовый регистр. Используется для адресации Общий рабочий регистр. Специального назначения не имеет То же Сумматор с плавающей точкой (48 разрядов) Форматы данных Наряду с форматами данных, которые есть в стандартной модели УУМ/ДС предоставляет дополнительный формат для данных с плавающей точкой: 1 11 36 S порядок Мантисса Мантисса интерпретируется как число между 0 и 1, т. е. предполагается, что двоичная точка стоит непосредственно перед ее старшим разрядом. Для нормализованных чисел старший разряд мантиссы должен равняться единице. Порядок интерпретируется как двоичное целое без знака в диапазоне от 0 до 2047. Если порядок имеет значение е, а мантисса значение f, то абсолютная величина числа будет представлена как f * 2 (e-1024) Знак числа с плавающей точкой указывается с помощью значения разряда s (0 положительное число, 1 - отрицательное число). Машинный нуль представляется в виде слова, содержащего нули во всех разрядах. Форматы команд Большой объем оперативной памяти, доступный в УУМ/ДС, означает что в общем случае 15-разрядного поля для задания адреса будет недостаточно. Таким образом, формат команд стандартной модели УУМ не подходит для УУМ/ДС. Существует два способа решения этой проблемы: либо использовать относительную адресацию, либо расширять адресное поле до 20 разрядов. Оба эти способа используются в УУМ/ДС (см. форматы 3 и 4 в нижеследующем описании). Кроме того, в УУМ/ДС имеются команды, которые вообще не ссылаются на оперативную память. Для этих команд используются форматы 1 и 2. Ниже приведены форматы команд УУМ/ДС. Значения разрядов-признаков в форматах 3 и 4 будут рассмотрены при об суждении способов адресации. Разряд е используется для того, чтобы различать форматы 3 и 4 (е = О - формат 3, е =l- формат 4). В приложении А для каждой команды УУМ/ДС указан номер ее формата. Формат 1 (1 байт): 8 код операции (ор) Формат 2 (2 байт): 8 4 код операции (ор) r1 Формат 3 (3 байт): 6 1 1 1 1 1 1 код операции (ор) n i x b p e Формат 4 (4 байт): 6 1 1 1 1 1 1 код операции (ор) n i x b p e 4 r2 12 Смещение (disp) 20 Адрес (addr) Способы адресации По сравнению со стандартной моделью в УУМ/ДС реализованы два новых способа относительной адресации, для которых используются командный формат 3. Их суть описывается следующей таблицей: Способ Признаки Вычисление адресации адресации целевого адреса Относительно b=l базы Относительно p=0 b=0 счетчика команд р=1 TA = (B) + disp (0 <= disp>= 4095) TA = (PC) + disp (-2048 <= disp >= 2047) Для способа адресации относительно базы поле смещения в командном формате 3 интерпретируется как 12-разрядное целое без знака. Для способа адресации относительно счетчика команд это поле интерпретируется как 12-разрядное целое со знаком, причем отрицательные величины представляются в дополнительном коде. Если в командном формате 3 разряды b и р одновременно установлены в О, то в качестве целевого адреса используется значение поля disp. Для командного формата 4 разряды b и p должны оба равняться нулю, а целевой адрес берется из поля адреса команды. Мы будем называть такой способ прямой адресацией в отличие от относительной адресации, описанной выше. Каждый из этих способов адресации может сочетаться с индексированием адреса. Признаком индексирования является значение разряда х, равное 1 , и в этом случае выражение для вычисления целевого адреса содержит дополнительное слагаемое (Х). Заметим, что стандартная модель УУМ использует только прямую адресацию (с индексированием или без него). Разряды i и n в форматах 3 и 4 определяют способ использования целевого адреса. Если i = 1, а n = 0, то собственно значение целевого адреса используется в качестве операнда без выполнения каких-либо дополнительных ссылок в оперативную память. Такой способ задания операнда называется непосредственной адресацией. Если i = 0, а n = 1, то содержимое слова по целевому адресу используется в качестве адреса операнда. Такой способ называется косвенной адресацией. Если разряды i и n оба равны 0 или 1, то целевой адрес задает местонахождение операнда. Мы будем называть такой способ простой адресацией. При использовании непосредственной и косвенной адресации использование индексирования невозможно. * * * * 3030 * * * 3600 * * * * * 6390 * * * * * * С303 * * * * * * * * 003600 * * * 103000 * * * * * 00С303 * * * * * * 003030 * * * * а (В)=006000 (РС)=003000 (Х)=000090 Многие авторы используют термин исполнительный адрес (effective address) для обозначения того, что мы назвали целевым адресом команды. Однако имеется определенное противоречие, когда термин "исполнительный адрес" употребляется для команд, использующих косвенную адресацию. Для того чтобы избежать путаницы, мы в данной пособии используем термин "целевой адрес". Команды УУМ/ДС, в которых не используется непосредственная или косвенная адресации, переводятся ассемблером в машинные коды, имеющие в разрядах i и n значение 1.Ассемблер стандартной модели УУМ устанавливает в этих разрядах нулевые значения (так как 8-разрядный код операций всех команд стандартной модели заканчивается кодом 00). В УУМ/ДС предусмотрены специальные аппаратные средства для обеспечения упомянутой ранее совместимости "снизу вверх". Если оба разряда i и n имеют нулевое значение, то разряды b, р, е рассматриваются как часть поля адреса Машинная команда Двоичное Шестнад цатеричн ое 032600 030300 022030 010030 003600 03100303 Смещение / адрес Значен ие, загруж Целе аемое в вой регистр А адрес 3600 103000 ор n i x b p e 00000 0 00000 0 00000 0 00000 0 00000 0 00000 0 1 1 0 0 1 0 0110 0000 0000 1 1 1 1 0 0 0011 0000 0000 6390 000303 1 0 0 0 1 0 0000 0011 0000 3030 103000 0 1 0 0 0 0 0000 0011 0000 30 000030 0 0 0 0 1 1 0110 0000 0000 3600 103000 1 1 0 0 0 1 0000 1100 0011 0000 0011 0303 003030 б Рис.1.1 Примеры команд и способов адресации УУМ/ДС. (а не как признаки способа адресации). Таким образом, командный формат 3 становится идентичным командному формату, который используется в стандартной модели УУМ, что и обеспечивает требуемую совместимость. На рис. 1.1 приведены примеры различных способов адресации, которые обеспечиваются УУМ/ДС. На рис. 1.1а показано содержимое регистров В, PC и X, а также некоторых специально подобранных ячеек оперативной памяти. Значения приведены в шестнадцатеричном виде. Машинный код команды загрузки сумматора, ее целевой адрес и загружаемое значения показаны на рис. 1.16. Вам следует тщательно изучить эти примеры и убедиться в том, что вы правильно понимаете различные способы адресации. В приложении А дается полное описание всех форматов и способов адресации, используемых в УММ/ДС. Система команд УУМ/ДС обеспечивает выполнение всех команд стандартной модели. Дополнительно введены команды загрузки и запоминания содержимого новых регистров (LOB, STB и др.), а также команды арифметики с плавающей точкой (ADDF, SUBF, MULT, DIVF). Имеются команды, использующие в качестве операндов значения регистров. К ним наряду с командой пересылки содержимого одного регистра в другой регистр (RMO) относятся арифметические команды, выполняющие действия над содержимым регистров (ADDRSUBR, MULR, DIVR). Кроме едена специальная команда для обращения к супервизору (SVC). Выполнение этой команды вызывает прерывание, которое может быть использовано для связи с операционной системой. Подробно организация обращений к супервизору и прерывания будут обсуждаться в гл. 6. Имеются и другие дополнительные команды. Полный список команд УУМ/ДС с указанием их кода операции и выполняемых функций приведен в приложении А. Средства ввода-вывода Команды ввода-вывода, рассмотренные нами для стандартной модели выполняются и на УУМ/ДС. Кроме того, данная модель имеет каналы ввода-вывода, которые могут работать независимо от центрального процессора. Это позволяет совместить операции обмена с процессом вычислений и тем самым повысить общую эффективность системы. Для начала (start)проверки (test) и прекращения (halt) канальных операций обмена используются соответственно команды SIO, TIO и HIO. Более детально эти вопросы будут обсуждаться в гл.6. 1.4 Структура System / 370 В этом разделе дается краткое введение в System/370. System/370 представляет собой скорее архитектуру, нежели конкретную ЭВМ. Эта архитектура, реализованная на ряде различных машин, представляет модели семейства 370. Хотя эти модели различаются в аппаратной части и по другим физическим характеристикам, они логически совместимы друг с другом. Любая программа должна дать одинаковые результаты на любой ЭВМ семейства 370 при условии, что ее выполнение не связано с временными или другими машинно-зависимыми характеристиками. Архитектура System/370 совместима снизу вверх с машинами System/360, т. е. программы для System/360 должны правильно выполняться на ЭВМ System/370 (при вышеназванных условиях). Весьма похожие архитектуры были реализованы на некоторых других процессорах фирмы IBM и процессорах других фирм. В этом разделе приводятся основные сведения, которые необходимы чтобы понять примеры программного обеспечения System/370, обсуждаемые в дальнейшем. Дополнительную информацию можно найти в IBM [1983] и Страбл [1983]. Память Память состоит из 8-разрядных байтов; каждый байт имеет уникальный адрес. Группа из последовательных 2 байт, первый из которых имеет адрес, кратный 2, называется полусловом. Аналогично группа из последовательных 4 байт, начинающаяся с адреса, кратного 4, называется словом, а группа из 8 байт, начинающаяся с адреса, кратного 8,- двойным словом. Таким образом, полуслово имеет длину 16 разрядов, слово 32 и двойное слово - 64 разряда. Байтовые адреса 0 1 2 3 4 5 6 7 8 9 А В С D E F 10 11 12 … Полуслова … Слова … Двойные слова … Рис. 1.2. Деление памяти System/370 на байты, полуслова, слова и двойные слова. Рис. 1.2 иллюстрирует такое разделение памяти на байты, полуслова, слова и двойные слова. Машинные команды должны быть обязательно выравнены по границе полуслова (т. е. должны начинаться с адресов, кратных 2). В большинстве случаев операнды команд могут располагаться в памяти с любого адреса. Например, 16-разрядный операнд в команде АН (Add Halfword) может начинаться с любого места, т. е. он не должен выравниваться по границе полуслова. Однако скорость выполнения команд значительно выше, если операнды выравнены по границам, соответствующим их длинам. Максимальный объем оперативной Памяти, который обычно доступен в System/370, составляет 16 Мбайт (224 байт). Регистры В System/370 имеется 16 регистров общего назначения, пронумерованных от 0 до 15. Для их обозначения часто пользуются символическими именами вида R0,...,К15. Каждый регистр состоит из 32 разрядов. Для некоторых команд два последовательных регистра составляют один логический 64-разрядный операнд. Каждый регистр общего назначения может использоваться в качестве сумматора в арифметических и логических операциях. Кроме того, все эти регистры, за исключением RO, могут использоваться как базовые или индексные регистры. Имеется еще четыре других регистра, которые используются для операций с плавающей точкой. Каждый из этих регистров состоит из 64 разрядов. Некоторые команды используют два таких последовательных регистра для хранения 128-разрядных величин с плавающей точкой. Все упомянутые выше регистры доступны в прикладных программах. В дополнение к ним есть 16 управляющих регистров, которыми пользуется операционная система. Существует также специальный регистр PSW (Program Status Word), содержащий различную системную информацию (счетчик команд, код условия и т. п.). Форматы данных System/370 обеспечивает хранение двоичных и десятичных целых, величин с плавающей точкой и символов. Символы хранятся в 8-разрядном коде EBCDIC. Двоичное целое хранится как 16-разрядная (полуслово) или 32-разрядная (слово) двоичная величина. В зависимости от команды эти величины могут рассматриваться как целое со знаком или целое без знака. Для представления отрицательных целых со знаком используется дополнительный код. Десятичные целые представляются либо в зонном десятичном формате, либо в упакованном десятичном формате. В любом случае представление имеет переменную длину. Число используемых байтов выбирается программистом и зависит от величины максимального хранимого числа. В зонном десятичном формате четыре младших разряда каждого байта содержат двоичное представление десятичных цифр (от 0 до 9). Четыре старших разряда каждого байта, за исключением последнего, обычно содержат код 1111 (шестнадцатеричное F). В этом случае представление в виде зонного десятичного формата совпадает с представлением десятичных цифр в коде EBCDIC. Четыре старших разряда последнего байта могут интерпретироваться как знак. Обычно для положительных чисел эти разряды содержат шестнадцатеричный код С, для отрицательных - код D и для целых без знака (которые рассматриваются как положительные) - код F. Например, десятичное целое +53 842 представляется в зонном десятичном формате как шестнадцатеричное F5F3F8F4C2 (5 байт), а - 6071 - как F6FOF7DI (4 байт). В упакованном десятичном формате каждый байт делится на два 4-разрядных поля. Во всех байтах, за исключением последнего, каждое из этих полей содержит двоичное представление десятичной цифры. В каждом байте первое поле содержит десятичную цифру, а второе - знак (по схеме, описанной выше). Таким образом, десятичное целое +53842 представляется в упакованном десятичном формате как шестнадцатеричное 53842С (3 байт), а - 6071 - как 0607D (3 байт). Величины с плавающей точкой представляются в одном из следующих форматов: Короткий формат (слово): 1 7 24 S e f Длинный формат (двойное слово): 1 7 56 s e F Расширенный формат (два последовательных двойных cловa): 1 7 56 s el Fl 1 7 56 s e2 f2 В любом случае величина числа представляется с помощью мантиссы f и порядка е. Порядок интерпретируется как двоичное целое без знака. Мантисса - как шестнадцатеричное без знака, у которого "шестнадцатеричная точка" расположена непосредственно перед самой левой шестнадцатеричной цифрой. В нормализованных числах с плавающей точкой старшая шестнадцатеричная цифра отлична от нуля. Для расширенного формата с плавающей точкой порядок е формируется как конкатенация порядков е 1 и е 2, а мантисса f - как конкатенация f 1 и f 2. Абсолютная величина представляемого таким образом числа равна f *16(e-64) Знак числа определяется значением разряда s (0 - положительное, 1 отрицательное). Число нуль представляется кодом, содержащим нули во всех разрядах. Отметим, что порядок чисел с плавающей точкой в Sуstem/370 интерпретируется как степень 16 (а не 2, как в большинстве машин). Это сделано для того, чтобы увеличить порядок чисел, которые могут быть представлены с помощью одного 32-разрядного слова. Поэтому мантиссу следует интерпретировать как шестнадцатеричное (а не двоичное) число. Это означает, что три старших разряда мантиссы могут равняться нулю (если старшая шестнадцатеричная цифра равна 1). В силу этого количество значащих разрядов в мантиссе может быть различным в зависимости от величины числа. Форматы команд В System/370 имеется восемь основных форматов команд. В этом разделе мы кратко остановимся на трех наиболее общих из них. Детальное описание всех восьми форматов можно найти в IBM [1983]. У большинства команд System/370 есть два операнда. Для этих команд существуют три возможных варианта расположения операндов в памяти: оба операнда в регистрах; один в регистре, другой в памяти; оба в памяти. Следующие форматы команд являются типичными для каждого из этих случаев. Формат RR: 8 4 4 ор r1 r2 Формат RX: 8 4 4 4 12 Op r1 x2 b2 d2 Формат SS: 8 4 4 4 12 4 12 op i1 i2 bl dl b2 d2 В формате RR оба операнда находятся либо в регистрах общего назначения, либо в регистрах с плавающей точкой. Номер регистра кодируется в команде (как rl или r2). Тип регистра определяется кодом операции. Для команд формата RX один операнд находится в регистре rl, а другой - в оперативной памяти. Для задания полного адреса в System/370 требуется 24 разряда. Для того чтобы уменьшить объем памяти, занимаемый командой, исполнительный адрес (в общем случае) за дается с помощью базового регистра b2, индексного регистра x2 и 12-разрядного смещения d2. (Порядок вычисления целевого адреса для каждого способа адресации будет описан ниже.) В формате SS оба операнда расположены в памяти. Если команда данного типа ссылается на операнды переменной длины, то длина операндов задается в команде с помощью полей 11 и 12. Адреса операндов определяются с помощью базового регистра и смещения. Использование индексного регистра запрещено. В некоторых других форматах команд поле кода операции расширено до 16 разрядов (форматы RRE и SSE). Формат RX можно изменить так, чтобы разрешить использование второго регистрового операнда вместо индексного регистра (формат RS) или чтобы включить 8-разрядный непосредственный операнд вместо двух номеров регистров (формат SI). Одна из модификаций формата SS определяет 8-разрядный код, который используется в обоих операндах. Другая модификация имеет только один операнд. Способы адресации В System/370 команды, которые ссылаются на операнды, расположенные в оперативной памяти, должны использовать относительную адресацию с базированием. Целевой адрес получается как сумма содержимого базового регистра, индексного регистра (если он задан) и смещения. Смещение интерпретируется как 12-разрядное целое без знака (отрицательное смещение запрещено). Регистр общего назначения R0 нельзя использовать как базовый или индексный регистр. Если в команде задан в качестве базового или индексного регистра регистр с номером нуль, то при вычислении целевого адреса соответствующий регистр использоваться не будет. В System/370 применяются два способа непосредственной адресации. В команде LA (Load Address) целевой адрес загружается в заданный регистр (вместо того чтобы вызывать операнд из оперативной памяти). Некоторые другие команды, например такие, как MVI (MoVe Immediate), используют непосредственый однобайтовый операнд прямо из команды. Однако это особые случаи, когда непосредственная адресация определяется как часть команды. В большинстве команд использование непосредственных операндов невозможно. В System/370 не предусмотрено адресации относительно счетчика команд или косвенной адресации. Прямая адресация возможна только в весьма специфическом случае, когда в качестве и базового, и индексного регистров используется регистр с нулевым номером. В этом случае в качестве фактического адреса будет взято 12разрядное смещение. Возможность прямой адресации первых 4096 байт оперативной памяти иногда используется в операционной системе и дает определенные преимущества. Система команд Команды System/370 делятся на пять классов: общего назначения десятичной арифметики, арифметики с плавающей точкой, управляющие и ввода-вывода. Многие команды устанавливают 4-разрядный код условия для индикации различных ситуаций. Команды условного перехода могут проверить установленный код и выполнить соответствующее ветвление. Полный список команд System/370 с указанием устанавливаемых ими кодов условия можно найти в IBM [1983]. К командам общего назначения относятся команды загрузки и запоминания регистров общего назначения, а также команды, обеспечивающие выполнение двоичных арифметических операций и операций сравнения. Каждая из этих функций может выполняться с помощью нескольких различных команд в зависимости от типа и месторасположения используемых операндов. Например, двоичное сложение выполняется следующими машинными командами: А сложение слова, память - регистр; АН сложение полуслова, память - регистр; AL сложение слова, без знака, память-регистр; ALR сложение слова, без знака, регистр - регистр; AR сложение слова, регистр - регистр. К группе команд общего назначения относятся также команды условного и безусловного переходов, логические операции, команды пересылки данных и многие другие операции. В System/370 имеется команда обращения к супервизору (SVC), похожая на аналогичную команду УУМ/ДС. Команды десятичной арифметики могут быть использованы для выполнения арифметических операций над целыми в упакованном десятичном формате. Кроме того, имеются команды для преобразования зонного десятичного формата в упакованный и наоборот (команды PACK и UNPACK), а также команды преобразования двоичного целого в упакованный десятичный формат и обратно (команды CVD и CVB). Две другие команды предоставляют возможности для редактирования данных, выполняя, например, такие операции, как вставка запятой и десятичной точки в числовые величины. Команды арифметики с плавающей точкой используются для выполнения операций над числами с плавающей точкой (загрузка и запоминание регистров с плавающей точкой, арифметические операции и операции сравнения). Так же как и в случае команд общего назначения, код операции зависит от типа и месторасположения операндов. Например, группа команд сложения включает следующие команды: АD сложение с нормализацией, длинный формат память - регистр; ADR сложение с нормализацией, длинный формат регистр - регистр; АЕ сложение с нормализацией, короткий формат память - регистр; АЕR сложение с нормализацией, короткий формат регистр - регистр; AU сложение без нормализации, короткий формат память - регистр; AUR сложение без нормализации, короткий формат регистр - регистр; AW сложение без нормализации, длинный формат память - регистр; AWR сложение без нормализации, длинный формат регистр - регистр; AXR сложение с нормализацией, расширенный формат регистр - регистр. В группу команд управления входят привилегированные команды предназначенные главным образом для использования в операционной системе. К ним относятся команды для загрузки и запоминания PSW и управляющих регистров, команды защиты памяти и многие другие. Некоторые функции, выполняемые этими командами, мы рассмотрим при обсуждении операционных систем в гл. 6. Средства ввода-вывода В System/370 обмен с внешними устройствами осуществляется с помощью каналов ввода-вывода, похожих на те, что были описаны для УУМ/ДС. Команды ввода-вывода позволяют центральному процессору (ЦП) начать, остановить и проверить каналы, а также выполнить другие управляющие операции. Имеется, кроме того, возможность, позволяющая ЦП выполнять прямую побайтную передачу с помощью специального интерфейса, не зависящего от каналов. 1.5. Структура ЭВМ VAX Семейство ЭВМ VAX было представлено фирмой DEC в 1978 г. Аббревиатура VAX указывает на одну из наиболее важных особенностей данной архитектуры виртуальное адресное расширение (Virtual Address eXtension). Хотя многие другие ЭВМ (включая System/370) были модифицированы для предоставления виртуальной памяти, система VAX с самого начала проектировалась в расчете на виртуальное адресное пространство. Виртуальная память позволяет программам работать так, как будто они имеют доступ к очень большой памяти, вне зависимости от объема реальной оперативной памяти, используемой в системе. Заботу об управлении па мятью берет на себя операционная система. Мы обсудим виртуальную па мять в связи с изучением операционных систем в гл. 6. При проектировании архитектуры ЭВМ VAX была предусмотрена совместимость с более ранним семейством ЭВМ PDP-11. Средства совмести мости обеспечены на уровне аппаратуры, что позволяет выполнять без каких-либо изменений многие программы PDP11 на ЭВМ VAX. Более того имеется возможность одновременной работы в многопользовательском ре жиме программ для PDP-11 и VAX. В этом разделе дается сводная информация о некоторых основных характеристиках архитектуры ЭВМ VAX. Дополнительную информацию можно найти в DEC [1981] и Баас [1983]. Память Память ЭВМ VAX состоит из 8-разрядных байтов, каждый из которых имеет свой адрес. Два последовательных байта составляют слово (word) четыре - длинное слово (longword), восемь - квадрослово (quadword) шестнадцать - октослово (octaword). Как и в System/370, для уменьшения времени доступа к оперативной памяти желательно, чтобы слова, длинные слова, квадрослова и октослова были выравнены по соответствующей границе. Реальная оперативная память ЭВМ VAX может достигать 223 байт. В то же время все программы VAX работают в виртуальном адресном пространстве объемом 2 32 байт. Объем реальной оперативной памяти обычно никак не влияет на выполнение прикладных программ. Половина виртуального адресного пространства называется системным пространством (system space). Эта часть содержит программы операционной системы и используется совместно всеми программами. Другая половина адресного пространства называется пространством процессов (process space). Пространство процессов определено отдельно для каждой программы. Часть этого пространства содержит стеки, доступные программе. Для работы со стеками имеются специальные регистры и команды. Регистры В ЭВМ VAX есть 16 регистров общего назначения, которые обозначаются как R0 R15. В то же время некоторые из этих регистров имеют специальные имена и назначение. Длина каждого регистра общего назначения - 32 разряда. Регистр R15 используется как счетчик команд и имеет имя PC. Во время выполнения команды его значение изменяется таким образом, чтобы всегда указывать на очередной обрабатываемый байт команды. Регистр R14 используется в качестве указателя стека (stack pointer) и именуется SP. Этот регистр указывает на вершину стека данной программы в пространстве процессов. Хотя для этой цели можно использовать и другие регистры, однако в машинных командах, косвенно обращающихся к стеку, всегда используется регистр SР. Регистр R13 используется как указатель фрейма (frame pointer) и носит имя FP. Соглашение о связях между процедурами в ЭВМ VAX построено на структуре данных, называемой стеком фреймов. При вызове процедуры адрес фрейма в стеке фреймов помещается в регистр FP. Регистр R12 имеет имя АР и используется в качестве указателя аргументов (argument pointer). В соответствии с соглашением о связях при вызове процедуры этот регистр используется для передачи адреса начала списка фактических параметров. У регистров R6 - R11 нет специального назначения, и они могут использоваться в программах для обычных целей. Точно так же можно использовать и регистры R0 - R5, но они, кроме того, применяются специальным образом в некоторых командах. Кроме регистров общего назначения имеется регистр состояния процессора PSL (Processor Status Longword), который содержит переменные состояния и признаки, связанные с процессом. Наряду со многими другими информационными полями PSL содержит код условия и признак, указывающий на работу процесса а режиме совместимости с PDP-11. Есть также ряд управляющих регистров, употребляемых для поддержания различных функций операционной системы. Форматы данных Для хранения целых может использоваться байт, слово, длинное слово, квадрослово и октослово. Отрицательные целые величины хранятся в дополнительном коде. Для хранения символьных величин используется 8-разрядный код ASCII. В системе VAX предусмотрены четыре различных формата для хранения величин с плавающей точкой длиной от четырех до шестнадцати байтов. Два из них совместимы с форматами PDP-11 и являются стандартными для всех процессоров семейства VAX. Два других являются дополнительными и обеспечивают хранение величин в более широком диапазоне за счет дополнительных разрядов, используемых для представления порядка числа. В любом случае форматы данных ЭВМ VAX принципиально не отличаются от форматов, рассмотренных ранее. Величина числа с плавающей точкой представляется как мантисса, умноженная на соответствующую степень 2. В ЭВМ VAX предусмотрен упакованный десятичный формат, аналогичный соответствующему формату System/370. Имеется также числовой формат, в котором каждая цифра числа записывается в отдельном байте. В этом смысле числовой формат похож на зонный десятичный формат System/370, за исключением того, что цифры хранятся в коде ASCII, а не в EBCDIC. Поэтому обычно старшие четыре разряда каждого байта в этом формате имеют шестнадцатеричный код 3, а не F. В то же время числовой формат сложнее, чем зонный десятичный формат, так как знак числа может быть указан либо в последнем байте (как и в System/370), либо в отдельном байте перед первой цифрой. Эти две модификации называются соответственно суффиксным числовым форматом (trailing numeric) и раздельным префиксным числовым форматом (leading separate numeric). В ЭВМ VAX обеспечиваются средства для работы с очередями и строками битов переменной длины. Конечно, такие структуры данных могут быть реализованы на любой машине, однако в ЭВМ VAX для работы с ними предусмотрены специальные аппаратные средства. Так, с помощью всего одной машинной команды можно включить или исключить элемент очереди или выполнить различные операции над строкой битов. Наличие столь мощных команд и сложных типов данных является одним из наиболее необычных свойств архитектуры ЭВМ VAX. Форматы команд В ЭВМ VAX используется формат команд переменной длины. Каждая команда состоит из кода операции (1 или 2 байт), за которым в зависимости от типа операции следует до 6 спецификаций операндов. Каждая спецификация операнда использует один из способов адресации и задает некоторую дополнительную информацию для определения месторасположения операнда в памяти. Способы адресации В ЭВМ VAX предусмотрены разнообразные способы адресации. За небольшим исключением, каждый из этих способов адресации может использоваться в любой команде. Операнд может задаваться либо непосредственно в регистре, либо по адресу, находящемуся в регистре. Если операнд задается по адресу, находящемуся в регистре, то после выполнения команды содержимое регистра может автоматически увеличиваться или уменьшаться на длину операнда. Предусмотрено несколько способов адресации относительно базы, в которых могут использоваться поля смещения различной длины. Если этот способ аргументов, а также адресации используется с регистром PC, то мы получим способ адресации относительно счетчика команд. Каждый из этих способов адресации может включать в себя индексный регистр, а многие из них могут использоваться для определения косвенной адресации. Наконец, разрешены непосредственные операнды и различные способы адресации, предназначенные для специальных целей. Дальнейшую информацию можно найти в DEC [1981]. Система команд Одной из целей разработчиков системы VAX было создание системы команд, симметричной в отношении различных типов данных. Значительная часть мнемонических имен команд формируется с помощью комбинации следующих элементов: - префикса, определяющего тип операции; - суффикса, определяющего тип операндов; - модификатора (в ряде команд), задающего число операндов. Например, команда ADDW2 является операцией сложения с двумя операндами, каждый из которых занимает длинное слово. Аналогично команда MULL3 является операцией умножения с тремя операндами, каждый из которых занимает одно слово. Команда CVTWL определяет операцию преобразования слова в длинное слово (в последнем случае подразумевается использование двух операндов). В большинстве команд операнд может располагаться в регистре, в памяти или непосредственно в самой команде. Один и тот же код операции используется независимо от месторасположения операндов. Этот подход является более гибким по сравнению с подходом в System/370, который требует различных кодов операций в зависимости от месторасположения операндов. VAX обеспечивает все обычные типы команд для вычислений, пересылки данных, преобразования, сравнения, ветвления и т. п. Кроме того существует ряд более сложных команд, чем те, которые имеются в большинстве ЭВМ. Во множестве случаев такие операции являются аппаратной реализацией часто используемых последовательностей команд для увеличения эффективности и скорости их выполнения. Например, VAX предоставляет команды групповой загрузки и запоминания регистров, а также команды для работы с очередями и строками битов переменной длины. Предусмотрены мощные команды для вызова и возврата из процедур. С помощью одной-единственной команды осуществляется запоминание содержимого заданной группы регистров, передача списка фактических параметров процедуре, управление указателями стека, фрейма и списка, аргументов, а также установка маски для предотвращения ошибок в арифметических операциях. Более детальную информацию о системе команд VAX можно найти в DEC [1981]. Средства ввода-вывода Обмен в ЭВМ VAX выполняется с помощью контроллеров ввода-вывода. Каждый контроллер имеет набор регистров состояния/управления и регистров данных, задающих пространство в физической оперативной памяти. Участок адресного пространства, на которое указывают регистры контроллера, называется пространством ввода-вывода. Никаких специальных команд для обеспечения доступа регистров контроллера к пространству ввода-вывода не требуется. Драйвер ввода-вывода выполняет команды контроллера, запоминая значения в нужных регистрах, точно так же, как если бы они были физическими областями памяти. Установление соответствия между адресами в пространстве ввода-вывода и физическими регистрами устройства управления осуществляется подпрограммой управления оперативной памятью. 1.6. Структура ЭВМ CYBER В этом разделе мы рассмотрим архитектуру ЭВМ серий CYBER 70 и CYBER 170 фирмы CDC. Несмотря на различия моделей этих серий в аппаратной части, они совместимы на уровне программ. Структура ЭВМ CYBER в значительной степени схожа с архитектурой ЭВМ серии CDC 6000 с некоторыми дополнительными возможностями. Похожая архитектура реализована в ЭВМ серии CYBER 180, объявленной фирмой СDС в 1984 г. Эти машины имеют другую длину слова и значительные улучшения в аппаратной части, однако предусматривают возможность для работы в режиме CYBER 70 и CYВЕR 170. CYBER-это мультипроцессорная система. В ее состав входят центральный процессор (ЦП) и несколько периферийных процессоров (ПП). Обычно ЦП занят обработкой пользовательских программ, в то время как ПП выполняют функции операционной системы. ПП может начать или прервать выполнение программы, осуществляемое ЦП, для выполнения управляющих функций. Каждый ЦП имеет доступ к оперативной памяти ЦП и к устройствам ввода-вывода. Кроме того, каждый ПП имеет свою собственную память. Структура памяти, регистры и система команд ПП и ЦП абсолютно разные и никак не связаны друг с другом. В то же время они могут обмениваться информацией через общую память. В этом разделе мы коснемся лишь характеристик ЦП. Подробную информацию о структурах ЦП и ПП можно найти в CDC [1981a] и Гришман [1974]. Память Память центрального процессора CYBER состоит из 60-разрядных слов. Машина имеет словную адресацию. К полям внутри слова нет средств прямого доступа, за исключением небольшой группы команд, ориентированных на обработку символьной информации. Максимальный объем оперативной памяти 256К (218) слов. Регистры Программа пользователя может работать с тремя группами регистров: А, В и X. В каждой группе имеется по восемь регистров, которые соответственно обозначаются АО А7, ВО - В7 и ХО - Х7. Длина регистров групп А и В - 18 разрядов. Обычно регистры группы А используются для адресации, а регистры группы В - в качестве индексных регистров или для хранения небольших целочисленных значений (например, счетчик цикла). Регистр ВО всегда содержит нулевой код. Длина регистров группы Х - 60 разрядов. Они используются при выполнении большинства операций, а также для хранения величин, выбранных из оперативной памяти. Между регистрами группы А и регистрами группы X установлено не совсем обычное соответствие. Если в регистр А1 заносится некоторое значение, то оно рассматривается как адрес слова в оперативной памяти, и в регистр X1 автоматически заносится содержимое слова оперативной памяти по этому адресу. Такое же логическое соответствие существует между регистрами А2 - А5 и регистрами Х2 - Х5. Если некоторый адрес заносится в регистр А6, то содержимое регистра Х6 автоматически записывается в оперативную память по этому адресу. Аналогичное соответствие установлено между регистрами А7 и Х7. Регистры АО и ХО логически между собой не связаны. Форматы данных Целые величины хранятся в виде 60-разрядных двоичных чисел (хотя некоторые операции целочисленной арифметики используют только младшие 48 разрядов слова). Для предоставления отрицательных чисел используется обратный код. Символьная информация хранится в 6-разрядном представлении, которое называется дисплейным кодом CDC. Вследствие использования 6-разрядного символьного кода содержимое слова ЭВМ CYBER обычно представляется в виде восьмеричного (а не шестнадцатеричного) числа. Числа с плавающей точкой представляются в следующем формате: 1 11 s 48 e С Коэффициент с интерпретируется как 48-разрядное двоичное целое. Предполагается, что двоичная точка расположена непосредственно после младшего разряда коэффициента. Для нормализованных чисел старший разряд коэффициента должен содержать 1. Порядок е интерпретируется как 11- разрядное двоичное целое без знака. Абсолютная величина числа с плавающей точкой может быть записана в виде с * 2 (е-1024) Знак, числа определяется разрядом s. Для положительных чисел он равен 0. Отрицательные числа представляются инвертированием всех разрядов слова. Величина нуль, с плавающей точкой записывается в виде слова имеющего нули во всех 60 разрядах. Некоторые значения порядка зарезервированы и не используются для обычных чисел с плавающей точкой. Такие, зарезервированные значения порядка используются для представления положительной и отрицательной бесконечности (результат деления числа, отличного от нуля, на нуль). Форматы команд Для большинства команд ЦП ЭВМ CYBER используются следующие форматы: (15-разрядный формат): 6 3 3 3 оp i j K (30-разрядный формат): 6 3 3 18 оp i j Аddr В этих форматах ор является кодом операции. Регистры, используемые в качестве операндов, обозначены как i, j, k. Тип регистров определяется командой. Если один из операндов расположен в оперативной памяти, то необходимо использовать 30-разрядный формат. Поле addr в этом формате содержит полный 18-разрядный адрес. Для некоторых команд поле кода операции логически расширено до 9 разрядов за счет поля i. В других командах поля i и k составляют одно поле, в котором могут задаваться маска пли величина сдвига. Кроме того, есть специальный 60-разрядный формат для небольшой группы команд, ориентированных на обработку символьной информации. Используемые в этом формате поля различны для разных команд. Детальное описание этих форматов можно найти в CDC [1982а]. Способы адресации В ЭВМ CYBER предусмотрен единственный способ адресации. Для доступа к оперативной памяти необходимо поместить полный 18-разрядный адрес в регистр группы А, что обеспечивает загрузку соответствующего регистра группы X словом из оперативной памяти или запись его содержимого в память. Поскольку используются реальные адреса оперативной памяти, то этот процесс во многом похож на способ прямой адресации, который мы обсуждали ранее. Однако команда установки регистра А может также выполнять вычисления, включающие до трех операндов. Поэтому программист может реализовать широкий набор различных способов вычисления целевого адреса, включая способы, эквивалентные косвенной и индексной адресации, а также адресации относительно базы. Кроме того, предусмотрены средства для непосредственной адресации. Система команд Логически команды ЦП CYBER распадаются на несколько групп. Самая большая и наиболее часто используемая содержит команды установки регистров А, В и X. Иллюстрацией некоторых из возможных команд этой группы могут служить следующие команды: SAi Aj + Bk SAi адрес SBi Bj + К SXi Xj + Bk Так, например, команда SA3 A4+B1 помещает в регистр АЗ сумму содержимого регистров А4 и В1. В свою очередь это вызовет загрузку регистра ХЗ содержимым слова оперативной памяти по адресу, занесенному в АЗ. Команда SA6 BЕТА (где BETA-это метка, имеющая в качестве своего значения адрес в оперативной памяти) заносит значение BЕТА в регистр А6, что обеспечивает запоминание содержимого Х6 по этому адресу. Команды установки могут заносить значение в регистр любой группы (А, В или X) и имеют различные возможности для задания своих операндов. Команды установки выполняют вычисления над 18-разрядными величинами, даже если установка производится в регистр группы X. Существует другая группа команд, которая обеспечивает выполнение сложения и вычитания 60-разрядных целых, арифметику с плавающей точкой, сдвиги и логические операции над X регистрами. Кроме того, есть четыре команды, предназначенные для работы с символами. С помощью этих команд можно осуществить пересылку строк символов в оперативной памяти и выполнить посимвольное сравнение строк. В ЭВМ CYBER предусмотрены команды безусловного перехода и два типа команд условного перехода. Команды первого типа проверяют значение, содержащееся в регистре X, и осуществляют передачу управления в зависимости от результата. Команды второго типа обеспечивают передачу управления в зависимости от результата сравнения содержимого двух регистров группы В. Понятия код условия в ЭВМ CYBER нет. Сравнение и передача управления осуществляются одной командой. Предусмотрена специальная команда передачи управления с возвратом для вызова подпрограмм. Адрес возврата хранится в памяти в первой команде вызываемой подпрограммы. Средства ввода-вывода Все операции обмена выполняются периферийными процессорами. Как это делается, а также некоторые другие общие вопросы взаимодействия между ЦП и ПП мы обсудим в гл. 6. Глава 2. Ассемблеры В этой главе будут рассмотрены вопросы проектирования и реализации ассемблеров. Существует целый ряд основных функций, например таких, как трансляция мнемонических кодов операций в их эквиваленты на машинном языке или присваивание машинных адресов символическим меткам, которые должны выполняться любым ассемблером. Если мы будем рассматривать эти основные функции, то большинство ассемблеров окажутся весьма похожими. Однако за пределами этого базового уровня возможности, предоставляемые ассемблерами, а также схемы их построения сильно зависят как от входного языка, так и от языка машины. Одним из аспектов такой машинной зависимости, естественно, являются имеющиеся различия в форматах машинных команд н кодах операций. Как мы увидим в дальнейшем, существуют и менее явные зависимости между ассемблерами и архитектурой ЭВМ. С другой стороны, некоторые средства языка ассемблера (и соответствующих ассемблеров) не имеют прямой связи со структурой машины. Их выбор является в известном смысле произвольным и определяется разработчиком языка ассемблера. Вначале мы рассмотрим базовый ассемблер для стандартной модели нашей упрощенной учебной машины. В разд. 2.1 рассматриваются наиболее важные операции, выполняемые типовым ассемблером, и описываются общие методы их реализации. Приведенные здесь алгоритмы, структуры данных и др. используются в большинстве ассемблеров. Таким образом, данный уровень описания дает нам отправную точку для изучения более сложных средств, предоставляемых ассемблерами. Мы также можем использовать эту базовую структуру как каркас при разработке нового ассемблера. В разд. 2.2 мы изучим некоторые типичные расширения базовой структуры, которые диктуются аппаратными особенностями. Это будет сделано на примере ассемблера для УУМ/ДС. Конечно, ассемблер УУМ/ДС не покрывает все возможные машинно-зависимые свойства, однако он включает некоторые средства, которые наиболее часто встречаются в реальных машинах. Рассматриваемые здесь принципы и технические приемы могут быть легко применены к другим ЭВМ. В разд. 2.3 обсуждаются наиболее часто встречающиеся машинно-независимые расширения языка ассемблера и способы их реализации. Опять-таки нашей целью является не рассмотрение всех возможных вариантов, а обсуждение концепций и технических приемов, которые могут быть использованы при разработке нового ассемблера. В разд. 2.4 обсуждаются некоторые важные варианты схем ассемблирования. Схема ассемблирования относится к тем характеристикам ассемблера, которые не находят своего отражения в самом языке ассемблера. Мы рассмотрим однопросмотровые и многопросмотровые ассемблеры, а также двухпросмотровые ассемблеры с оверлейной структурой. Мы также коснемся вопросов реализации таких ассемблеров и обсудим ситуации, в которых они могут быть полезны. Наконец, в разд. 2.5 будут, вкратце рассмотрены некоторый примеры ассемблеров для реальных машин. Мы не будем стремиться к детальному обсуждению всех аспектов, Вместо этого мы концентрируем наше внимание на наиболее интересных возможностях, которые являются следствиями тех или иных аппаратных или программных решений . 2.1.Основные-функции ассемблера На рис.2.1 приведена программа на языке ассемблера базовой модели УУМ. Различные варианты этой программы мы будем использовать на протяжении всей этой главы для иллюстрации различных свойств ассемблера. Номера строк используются только для ссылок и не являются частью программы. Эти номера помогают также установить соответствие между участками различные вариантов программы. Мнемонические имена команд, используемые в примере, были рассмотрены в разд. 1.3.1 и приведены в приложении А. Модификатор, ",X", следующий за операндом (см. строку 160), указывает на использование индексной адресации. Строки, начинающиеся с . , являются комментариями. Кроме мнемонических имен машинных команд в примере использованы следующие директивы ассемблера: START Определяет имя и начальный адрес программы. END Указывает на конец исходной программы и (обычно) задает первую исполняемую команду программы. BYTE Формирует символьную или шестнадцатеричную константу, зани мающую столько байтов, сколько необходимо для представления константы. WORD Формирует целую константу, занимающую одно слово. RESB Резервирует заданное количество байтов для данных. RESW Резервирует заданное количество слов для данных. Строка Исходное предложение 5 COPY START 1000 КОПИРОВАНИЕ ФАЙЛА 10 FIRST STL RETADR СОХРАНЕНИЕ АДРЕСА ВОЗВРАТА 15 CLOOP JSUB RDREC ВВОД ВХОДНОЙ ЗАПИСИ 20 LDA LENGTH ПРОВЕРКА НА EOF (LENGTH = 0) 25 COMP ZERO 30 JEQ ENDFIL ВЫХОД, ЕСЛИ НАШЛИ EOF 35 JSUB WRREC ВЫВОД ВЫХОДНОЙ ЗАПИСИ 40 J CLOOP ЦИКЛ 45 ENDFIL LDA EOF ЗАНЕСЕНИЕ МАРКЕРА КОНЦА ФАЙЛА 50 STA BUFFER 55 LDA THREE УСТАНОВИТЬ LENGTH = 3 60 STA LENGTH 65 JSUB WRREC ЗАПИСЬ EOF 70 LDL RETADR УСТАНОВКА АДРЕСА ВОЗВРАТА 75 RSUB ВОЗВРАТ ИЗ ПРОГРАММЫ 80 EOF BYTE C"EOF" 85 THREE WORD 3 90 ZERO WORD 0 95 RETADR RESW 1 100 LENGTH RESW 1 ДЛИНА ЗАПИСИ 105 BUFFER RESB 4096 ДЛИНА БУФЕРА – 4096 БАЙТ 110 * 115 * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР 120 * 125 RDREC LDX ZERO ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 130 LDA ZERO ОБНУЛЕНИЕ РЕГИСТРА А 135 RLOOP TD INPUT ПРОВЕРКА УСТРОЙСТВА ВВОДА 140 JEQ RLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 145 RD INPUT ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 150 COMP ZERO ПРОВЕРКА НА КОНЕЦ ЗАПИСИ (Х"00") 155 JEQ EXIT ВЫХОД ИЗ ЦИКЛА ПО КОНЦУ ЗАПИСИ 160 STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР 165 TIX MAXLEN ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 170 JLT RLOOP ДЛИНЫ 175 EXIT STX LENGTH ЗАПОМИНАНИЕ ДЛИНЫ ЗАПИСИ 180 RSUB ВОЗВРАТ ИЗ ПОДПРОГРАММЫ 185 INPUT BYTE X"F1" КОД УСТРОЙСТВА ВВОДА 190 MAXLEN WORD 4096 195 * 200 * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА 205 * 210 WRREC LDX ZERO ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 215 WLOOP TD OUTPUT ПРОВЕРКА УСТРОЙСТВА ВЫВОДА 220 JEQ WLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 225 LDCH BUFFER,X ЧТЕНИЕ СИМВОЛА ИЗ БУФЕРА 230 WD OUTPUT ВЫВОД СИМВОЛА 235 TIX LENGTH ЦИКЛ, ПОКА НЕ БУДУТ ВЫВЕДЕНЫ 240 JLT WLOOP ВСЕ СИМВОЛЫ 245 RSUB ВОЗВРАТ ИЗ ПОДРОГРАММЫ 250 OUTPUT BYTE X"05" КОД УСТРОЙСТВА ВЫВОДА 255 END FIRST Рис. 2.1 Пример программы на языке ассемблера УУМ. Программа состоит из трех подпрограмм. Главная программа вводит записи с устройства ввода (код устройства F1) и копирует их на устройство вывода (код 05). Она вызывает под программу RDREC для ввода записи на буфер и подпрограмму WRREC для вывода записи из буфера на устройство вывода. Так как в УУМ имеются только команды RD и WD, то каждая подпрограмма должна передавать данные побайтно. Буфер необходим для согласования скорости обмена устройства ввода со скоростью выводного устройства (например, магнитного диска и устройства печати). В гл. 6 мы увидим, как для выполнения этих же функций используются канальные программы и макрокоманды операционной системы УУМ/ДС. Признаком конца записи служит нулевой код. Признаком конца файла служит запись, имеющая нулевую длину. При обнаружении конца файла программа выдает признак конца файла EOF на устройство вывода и заканчивает свою работу возвратом управления вызвавшей программе (возможно, операционной системе) . 2.1.1. Простой ассемблер для УУМ На рис.2.2 показана та же программа, что и на рис.2.1, но вместе с объектным кодом, сгенерированным для каждого предложения. В столбце " Адрес " даны шестнадцатеричные машинные адреса оттранслированной программы. Мы предполагаем, что начальный адрес программы равен 1000. Заметим, что в реальном листинге ассемблера комментарии будут, конечно, сохранены. В нашем примере они исключены только из-за недостатка места. Для перевода исходной программы в ее объектное представление необходимо выполнить следующие действия (не обязательно в указанном порядке): 1. Преобразовать мнемонические колы операций в их эквиваленты на машинном языке - например, перевести STL в 14 (строка 10). 2. Преобразовать символические операнды в эквивалентные им машинные адреса например, перевести RETADR в 1033 (строка 10). 3. Построить машинные команды в соответствующем формате. 4. Преобразовать константы, заданные в исходной программе, во внутреннее машинное представление (например, в строке 80 оттранслировать EOF в 454F46). 5. Записать объектную программу и выдать листинг. Все указанные действия, за исключением второго, легко могут быть выполнены простой построчной обработкой исходной программы. В то же время трансляция адресов вызывает определенные трудности. Рассмотрим предложение 10 1000 FIRST STL RETADR Мы не можем сразу оттранслировать это предложение, так как не знаем адрес, который будет присвоен метке RETADR. Строка 5 1000 10 1000 15 1003 20 1006 25 1009 30 100C 35 100F 40 1012 45 1015 50 1018 55 101B 60 101E 65 1021 70 1024 75 1027 80 102A 85 102D 90 1030 95 1033 100 1036 105 1039 110 115 120 125 2039 130 203C 135 203F 140 2042 145 2045 150 2048 155 204B 160 204E 165 2051 170 2054 175 2057 180 205A 185 205D 190 205E 195 200 205 210 2061 215 2064 220 2067 Адрес Исходное предложение Объектный код COPY START 1000 FIRST STL RETADR 141033 CLOOP JSUB RDREC 482039 LDA LENGTH 001036 COMP ZERO 281030 JEQ ENDFIL 301015 JSUB WRREC 482061 J CLOOP 3С1003 ENDFIL LDA EOF 00102А STA BUFFER 0С1039 LDA THREE 00102D STA LENGTH 0С1036 JSUB WRREC 482061 LDL RETADR 081033 RSUB 4С0000 EOF BYTE C"EOF" 454F46 THREE WORD 3 000003 ZERO WORD 0 000000 RETADR RESW 1 LENGTH RESW 1 BUFFER RESB 4096 * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * RDREC LDX ZERO 041030 LDA ZERO 001030 RLOOP TD INPUT E0205D JEQ RLOOP 30203F RD INPUT D8205D COMP ZERO 281030 JEQ EXIT 302057 STCH BUFFER,X 549039 TIX MAXLEN 2C205E JLT RLOOP 38203F EXIT STX LENGTH 101036 RSUB 4C0000 INPUT BYTE X"F1" F1 MAXLEN WORD 4096 001000 * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * WRREC LDX ZERO 041030 WLOOP TD OUTPUT E02079 JEQ WLOOP 302064 225 230 235 240 245 250 255 206A LDCH 206D WD 2070 TIX 2073 JLT 2076 RSUB 2079 OUTPUT BYTE END BUFFER,X OUTPUT LENGTH WLOOP 509039 DC2079 2C1036 382064 4C0000 X"05" 05 FIRST Рис.2.2 Объектный код для программы на рис.2.1. Поэтому большинство ассемблеров выполняет два просмотра исходной программы. Основной задачей первого просмотра является поиск символических имен и назначение им адресов. Фактическая трансляция, описанная выше, выполняется во время второго просмотра. Ассемблер наряду с трансляцией команд исходной программы должен также выполнять и так называемые директивы ассемблера (иногда их называют псевдокомандами). Эти директивы не переводятся непосредственно в машинные команды (хотя и могут оказывать влияние на объектную программу), а управляют работой самого ассемблера. Примерами таких директив могут служить предложения BYTE и WORD, которые служат для включения в объектную программу констант, и предложения RESB и RESW, обеспечивающие резервирование заданного пространства оперативной памяти. В нашей программе имеются и другие директивы - START, которая определяет начальный адрес объектной программы, и END, которая отмечает конец исходной программы. В заключительной фазе своей работы ассемблер должен записать полученный объектный код на некоторое устройство вывода. Позднее эта объектная программа будет загружена в оперативную память для исполнения. Для представления объектной программы мы будем использовать простой формат, в котором определены три типа записей: запись-заголовок, тело программы и запись-конец. Запись-заголовок содержит имя программы, ее начальный адрес и длину. Тело программы содержит машинные команды и данные программы с указанием адресов их загрузки. Запись-конец отмечает, конец объектной программы и определяет адрес, с которого следует начать исполнение программы (точку входа). (Данный адрес задается операндом предложения END. Если этот операнд не задан, то в качестве точки входа берется адрес первой исполняемой команды ( Начальный адрес программы не обязательно совпадает с адресом первой выполняемой команды (точкой входа)). Ниже приведены форматы, которые мы будем использовать для записей объектной программы. Реализация форматов (номер столбца и т. п.) может быть любой, однако содержащаяся в них информация должна в той или иной форме присутствовать в объектной программе. Запись-заголовок: Столбец 1 Н Столбцы 2-7 Имя программы Столбцы 8-13 Начальный адрес программы (шестнадцатеричный) Столбцы 14-19 Длина программы в байтах (шестнадцатеричная) Тело программы: Столбец 1 Т Столбцы 2-7 Начальный адрес в данной записи (шестнадцатеричный) Столбцы 8-9 Длина данной записи в байтах (шестнадцатеричная) Столбцы 10-69 Объектный код (шестнадцатеричный) Запись-конец: Столбец 1 Е Столбцы Адрес первой исполняемой команды объектной программы (шестнадцатеричный) Для того чтобы избежать путаницы, мы использовали термин столбец(column), а не байт для ссылки на месторасположения внутри записей объектной программы. Это отнюдь не означает, что для хранения объектной программы мы будем использовать перфокарты (или какое-либо другое специальное представление) . 2-7 H_COPY _001000_00107A T_001000_1E_141033_482039_001036_281030_301015_482061_3C1003_00102A_0C1039_0 0102D T_00101E_15_0S1036_482061_081033_4C0000_454F46_000003_000000 T_002039_1E_041030_001030_E0205D_30203F_D8205D_281030_302057_549039_2C205E_ 38203F T_002057_1C_101036_4C0000_F1_001000_041030_E02079_302064_509039_DC2079_2C10 36 T_002073_07_382064_4C0000_05 E_001000 Рис.2.3. Объектная программа, соответствующая рис.2.2. На рис. 2.3 показан объектный код, соответствующий рассмотренной нами программе (рис.2.2). На этом и последующих рисунках мы будем использовать символ _ для указания границы полей. Конечно, в реальной объектной программе этого символа нет. Обратите внимание, что для адресов 1033 - 2038 нет соответствующего объектного кода. Эта память просто резервируется загрузчиком и затем используется программой во время ее исполнения. (Детальное обсуждение функций загрузчика будет приведено в гл. 3.) Теперь мы можем дать общее описание функций каждого из двух просмотров нашего простого ассемблера. Первый просмотр (определение имен): 1. Назначение адресов для всех предложений исходной программы. 2. Запоминание значений (адресов), присвоенных всем меткам, для последующего их использования на втором просмотре. 3. Выполнение некоторых директив ассемблера. (К ним от носятся директивы, влияющие на адресацию, такие как BYTE, RESW и т.п.) Второй просмотр (трансляция команд и генерация объектного кода): 1. Трансляция команд (перевод кодов операций и разрешение адресных ссылок). 2. Генерация данных, заданных директивами BYTE, WORD и др. 3. Выполнение тех директив ассемблера, которые не были обработаны на первом просмотре. 4. Запись объектного кода и выдача листинга. В следующем разделе мы подробнее остановимся на этих функциях и дадим описание внутренних таблиц ассемблера и полное описание алгоритмов каждого из просмотров. 2.1.2. Таблицы и алгоритмы ассемблера Наш простой ассемблер использует две основные внутренние таблицы: таблицу кодов операций (ОРТАВ) и таблицу символических имен (SYMTAB). ОРТАВ используется для поиска мнемонических кодов операций и перевода их в эквивалентные им представления на машинном языке SYMTAB используется для хранения значений (адресов), присвоенных символическим именам. Еще нам потребуется счетчик адреса (LOCCTR). LOCCTR это переменная, которая используется для назначения адресов. Начальное значение LOCCTR определяется операндом предложения START. После обработки очередного предложения исходной программы длина полученной команды или области данных прибавляется к LOCCTR. Таким образом, если мы встречаем помеченное предложение исходной программы, то значение этой метки определяется текущим значением LOCCTR. Таблица кодов операций должна по крайней мере содержать мнемонические коды операций и их машинные эквиваленты. В более сложных ассемблерах эта таблица, кроме того, содержит информацию о длине и формате каждой команды. Во время первого просмотра ОРТАВ используется для поиска и проверки корректности задания кодов операций в исходной программе. Во время второго просмотра она используется для перевода мнемонических кодов в их машинное представление. Фактически в нашем простом ассемблере для УУМ оба эти процесса можно совместить в одном просмотре (неважно, в первом или во втором). Однако для машин, имеющих форматы команд переменной длины (например, УУМ/ДС), нам необходимо просматривать ОРТАВ и при первом просмотре для того, чтобы определить приращение переменной LOCCTR. При втором просмотре мы используем ОРТАВ для определения формата и других специальных характеристик ассемблируемой команды. В нашем случае мы решили придерживаться именно этой структуры, поскольку она типична для большинства ассемблеров. Pass1: begin прочитать первую входную строку if OPCODE = "START" then begin запомнить #[OPERAND] в качестве начального адреса занести начальный адрес в LOCCTR записать строку в промежуточный файл прочитать следующую входную строку end else занести 0 в LOCCTR while OPCODE "END" do begin if это не строка-комментарий then begin if есть символ в поле метки then begin поиск метки в SYMTAB if нашли then занести признак ошибки ( второе определение ) else занести ( LABEL, LOCCTR ) в SYMTAB end поиск OPCODE в OPTAB if нашли then прибавить 3 { длина команды } к LOCCTR else if OPCODE = "WORD" then прибавить 3 к LOCCTR else if OPCODE = "RESW" then прибавить 3 * #[COPERAND] к LOCCTR else if OPCODE = "RESB" then прибавить #[COPERAND] к LOCCTR else if OPCODE = "BYTE" then begin определить длину константы в байтах прибавить длину к LOCCTR end else занести признак ошибки ( неверный код операции ) end { if не комментарий } записать строку в промежуточный файл прочитать следующую входную строку end { while } записать последнюю строку в промежуточный файл запомнить ( LOCCTR – начальный адрес ) в качестве длины программы end { Pass 1 } Рис.2.4а. Алгоритм первого просмотра ассемблера. Pass2: begin прочитать первую входную строку (из промежуточного файла) if OPCODE = "START" then begin выдать строку листинга прочитать следующую входную строку end занести запись – заголовок в объектную программу инициализировать первую запись тела программы while OPCODE "END" do begin if это не строка-комментарий then begin поиск OPCODE в OPTAB if нашли then begin if в поле операнда есть имя then begin поиск имени в SYMTAB if нашли then запомнить значение имени в качестве адреса операнда else begin запомнить 0 в качестве адреса операнда занести признак ошибки ( имя не определено ) end end { if есть имя } else запомнить 0 в качестве адреса операнда ассемблировать команду в объектное представление end { if нашли } else if OPCODE = "BYTE" или "WORD" then преобразовать константу в объектное преставление if объектный код не помещается в текущей записи тела программы then begin занести текущую запись в объектную программу инициализировать новую запись тела программы end занести объектный код в запись тела программы end { if не комментарий } выдать строку листинга прочитать следующую входную строку end { while } занести последнюю запись тела в объектную программу занести запись – конец в объектную программу выдать последнюю строку листинга end { Pass 2 } Рис.2.4б. Алгоритм второго просмотра ассемблера. Обычно ОРТАВ организуется в виде хеш-таблицы (В нашей литературе иногда используется термин "перемешанные таблицы"), в которой в качестве ключа используется мнемонический код операции. (Конечно, ОРТАВ строится заранее - при создании ассемблера, а не во время его работы.) Такая организация очень удобна, поскольку обеспечивает быстрый поиск при минимальном числе сравнений. В большинстве случаев ОРТАВ представляет собой статическую таблицу, т. е. в процессе работы в ней не создаются новые элементы, а уже определенные не исключаются. В этом случае можно построить специальную хеш-функцию или другую структуру данных, обеспечивающую для конкретного набора ключей оптимальное время доступа. Однако чаще всего используются стандартные хеш-функции. Подробную информацию о построении хештаблиц можно найти в любом хорошем учебнике по структурам данных, например в Стендиш [1980] или Кнут [1973б]. Таблица имен (SYMTAB) состоит из имен и значений (адресов) всех меток исходной программы вместе с признаками, указывающими на ошибку (например, дважды определенное имя). Эта таблица может также содержать информацию о типе, длине и других характеристиках помеченной области данных или команды. Во время первого просмотра все встретившиеся в исходной программе имена вместе с их адресами (они определяются по LOCCTR) сразу заносятся в SYMTAB. Во время второго просмотра имена, используемые в качестве операндов, ищутся в SYMTAB и соответствующие им адреса используются для генерации команд. Для повышения эффективности поиска и внесения новых имен SYMTAB обычно организуется также в виде хеш-таблицы. Поскольку операция исключения элемента из таблицы применяется крайне редко (скорее никогда)то вопрос об эффективности ее выполнения не возникает. Интенсивное использование SYMTAB на протяжении всей работы ассемблера требует тщательного подбора хеш-функции. Очень часто программисты используют много похожих друг на друга имен, например имена, начинающиеся или заканчивающиеся одними и теми же символами (такие как LOOP1, LOOP2, LOOPA)или имена, имеющие одинаковую длину (А, X, Y, Z). Важно, чтобы используемая хеш-функция обеспечивала хорошую работу с такими неслучайными именами. Часто хороший результат достигается путем деления входного ключа на простое число, равное длине таблицы. Хотя в качестве входной информации на этапе второго просмотра можно использовать исходную программу, однако должна быть учтена и информация, полученная ранее - после первого просмотра (например, адрес предложения или признак ошибки). Поэтому обычно во время первого просмотра создается промежуточный файл, который содержит каждое предложение исходной программы вместе с его адресом, признаком ошибки и другой необходимой информацией. Промежуточный файл является входным для второго просмотра. Рабочая копия исходной программы, содержащаяся в промежуточном файле, может быть использована для того, чтобы сохранить результаты некоторых операций, выполненных во время первого просмотра (например, операции сканирования поля операндов для определения имен и способов адресации). Это дает возможность не выполнять их еще раз при втором просмотре. Аналогично для кодов операций и имен могут быть сохранены указатели на таблицы ОРТАВ и SYMTAB, что позволит избежать повторных операций поиска. На рис.2.4а и 2.4б представлены алгоритмы соответственно первого и второго просмотров нашего ассемблера. Эти алгоритмы, несмотря на то что они описывают простой ассемблер, являются основой для более сложных двухпросмотровых ассемблеров, которые мы рассмотрим позднее. Для простоты мы будем полагать, что исходная программа записана в фиксированном формате с полями МЕТКА, ОПЕРАЦИЯ и ОПЕРАНДЫ. Если какое-либо из этих полей содержит строку символов, представляющую число, мы будем обозначать это с помощью префикса # (например, #[OPERAND]). На данном этапе очень важно, чтобы вы досконально разобрались с алгоритмами, представленными на рис. 2.4а и 2.4б. Вам настоятельно рекомендуется тщательно разобраться в этих алгоритмах, применив их для ручной трансляции программы на рис.2.1 в ее объектное представление на рис. 2.3. Конечно, для того чтобы сконцентрировать внимание на общей структуре и основных концепциях, мы опустили многие детали алгоритмов. Вам надлежит самостоятельно обдумать их, а также определить те функции ассемблера, которые следует реализовать в виде отдельных процедур или модулей. (Например, операции "поиск в таблице имен" или "чтение входной строки" являются хорошими кандидатами на такую реализацию.) Такой внимательный анализ алгоритмов следует делать всегда, прежде чем приступать к фактической реализации ассемблера или какого-либо другого элемента программного обеспечения значительного размера. 2.2. Машинно-зависимые характеристики ассемблера В этом разделе на примере ассемблера для УУМ/ДС мы рассмотрим, как расширение аппаратных возможностей ЭВМ влияет на структуру и функции ассемблера. Архитектура многих реальных ЭВМ похожа на структуру УУМ/ДС. Поэтому в значительной степени наше обсуждение применимо не только к УУМ/ДС, но и к реальным машинам. На рис.2.5 показано, как предыдущая программа (см. рис.2.1) может быть записана с использованием дополнительных возможностей системы команд УУМ/ДС. Косвенная адресация в нашем языке ассемблера задается добавлением к операнду префикса @ (строка 70). Непосредственный операнд обозначается префиксом # (строки 25, 55, 133). Для команд, в которых есть ссылка на оперативную память, обычно используется либо адресация относительно счетчика команд, либо адресация относительно базы. Директива ассемблера BASE (строка 13) используется в связи со способом адресации относительно базы. (Подробности и примеры см. в разд. 2.2.1.). В не которых командах величина смещения в относительном способе адресации может быть очень большой и для ее размещения будет недостаточно поля 3-байтового командного формата. Для таких команд необходимо использовать 4-байтовый расширенный командный формат. На языке ассемблера этот формат задается добавлением к коду операции префикса + (см. строки 15, 35, 65). Ответственность за правильность спецификации команд расширенного формата возлагается на программиста. Основное отличие новой версии программы от программы для УУМ заключается в том, что в ней всюду, где это возможно, используются команды вида регистр - регистр (вместо команд вида регистр-память). Например, в строке 150 вместо предложения СОМР ZERO использовано COMPR А, S, а в строке 165 вместо TIХ MAXLEN использовано TIXR Т. Кроме того, всюду, где это возможно, используются непосредственные операнды и косвенная адресация (например, строки 25, 55, 70). Эти изменения внесены для того, чтобы использовать преимущества архитектуры УУМ/ДС с целью увеличения скорости выполнения программы. Операции вида регистр регистр быстрее, чем соответствующие операции вида регистр - память. Это связано с тем, что, во-первых, они занимают меньше места в памяти и, во-вторых, что более важно, не требуют дополнительных ссылок в память для выборки операндов. (Выборка операнда из регистра происходит намного быстрее, чем из оперативной памяти.) Точно так же, когда используется непосредственный операнд, то при обработке команды он уже присутствует как ее составная часть, и, следовательно, нет необходимости в его выборке откуда-либо еще. Использование косвенной адресации часто позволяет экономить команду (см. строку 70). Вы можете заметить, что в то же время некоторые изменения требуют внесения в программу дополнительных команд. Например, замена команды СОМР на COMPR в строке 150 вынудила нас добавить команду CLEAR в строку 132. Тем не менее в целом скорость выполнения программы возрастет, так как команда CLEAR выполняется только один раз для каждой читаемой записи, а команда COMPR (более быстрая, чем СОМР) выполняется для каждого байта. Строка Исходное предложение 5 COPY START 0 КОПИРОВАНИЕ ФАЙЛА 10 FIRST STL RETADR СОХРАНЕНИЕ АДРЕСА ВОЗВРАТА 12 LDB #LENGTH УСТАНОВКА БАЗОВОГО РЕГИСТРА 13 BASE LENGTH 15 CLOOP +JSUB RDREC ВВОД ВХОДНОЙ ЗАПИСИ 20 LDA LENGTH ПРОВЕРКА НА EOF (LENGTH = 0) 25 COMP #0 30 JEQ ENDFIL ВЫХОД, ЕСЛИ НАШЛИ EOF 35 +JSUB WRREC ВЫВОД ВЫХОДНОЙ ЗАПИСИ 40 J CLOOP ЦИКЛ 45 ENDFIL LDA EOF ЗАНЕСЕНИЕ МАРКЕРА КОНЦА ФАЙЛА 50 STA BUFFER 55 LDA #3 УСТАНОВИТЬ LENGTH = 3 60 STA LENGTH 65 +JSUB WRREC ЗАПИСЬ EOF 70 J @RETADR ВОЗВРАТ ИЗ ПРОГРАММЫ 80 EOF BYTE C"EOF" 95 RETADR RESW 1 100 LENGTH RESW 1 ДЛИНА ЗАПИСИ 105 BUFFER RESB 4096 ДЛИНА БУФЕРА – 4096 БАЙТ 110 * 115 * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР 120 * 125 RDREC CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 130 CLEAR A ОБНУЛЕНИЕ РЕГИСТРА А 132 CLEAR S ОБНУЛЕНИЕ РЕГИСТРА S 133 +LDT #4096 135 RLOOP TD INPUT ПРОВЕРКА УСТРОЙСТВА ВВОДА 140 JEQ RLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 145 RD INPUT ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 150 COMP A,S ПРОВЕРКА НА КОНЕЦ ЗАПИСИ (Х"00") 155 JEQ EXIT ВЫХОД ИЗ ЦИКЛА ПО КОНЦУ ЗАПИСИ 160 STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР 165 TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 170 JLT RLOOP ДЛИНЫ 175 EXIT STX LENGTH ЗАПОМИНАНИЕ ДЛИНЫ ЗАПИСИ 180 RSUB ВОЗВРАТ ИЗ ПОДПРОГРАММЫ 185 INPUT BYTE X"F1" КОД УСТРОЙСТВА ВВОДА 195 * 200 * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА 205 * 210 WRREC CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 212 LDT LENGTH 215 WLOOP TD OUTPUT ПРОВЕРКА УСТРОЙСТВА ВЫВОДА 220 JEQ WLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 225 LDCH BUFFER,X ЧТЕНИЕ СИМВОЛА ИЗ БУФЕРА 230 WD OUTPUT ВЫВОД СИМВОЛА 235 TIXR T ЦИКЛ, ПОКА НЕ БУДУТ ВЫВЕДЕНЫ 240 JLT WLOOP ВСЕ СИМВОЛЫ 245 RSUB ВОЗВРАТ ИЗ ПОДРОГРАММЫ 250 OUTPUT BYTE X"05" КОД УСТРОЙСТВА ВЫВОДА 255 END FIRST Рис. 2.5 Пример программы на языке ассемблера УУМ/ДС. В разд. 2.2.1 рассматривается процесс трансляции программы для УУМ/ДС. При этом основное внимание обращено на те отличия ассемблера УУМ/ДС, которые продиктованы новыми способами адресации. (Вам следует взглянуть на форматы команд и способы вычисления целевого адреса, описанные в разд. 1.3.2.). Эти отличия являются прямым следствием расширения аппаратных возможностей. В разд. 2.2.2 обсуждаются отличия версии ассемблера УУМ/ДС, имеющие косвенную связь с новыми аппаратными возможностями. Например, расширенный объем оперативной памяти УУМ/ДС позволяет предположить, что мы будем иметь достаточно места для одновременной загрузки и исполнения нескольких программ. Такое разделение машины между несколькими программами называется мультипрограммным режимом обработки. Этот режим позволяет более эффективно использовать аппаратуру ЭВМ. (Мы вернемся к этому вопросу в связи с операционными системами в гл. 6.). Однако для того, чтобы полностью использовать преимущества мультипрограммного режима, мы должны иметь возможность загружать программу в любое свободное место оперативной памяти (а не в фиксированное место, определенное во время трансляции). При этом возникает необходимость перемещения программ в оперативной памяти. В разд. 2.2.2 мы обсудим эту проблему в ее связи с ассемблером. 2.2.1. Форматы команд и способы адресации В этом разделе мы рассмотрим трансляцию предложений исходной программы УУМ/ДС (рис.2.5) в соответствующее им объектное представление (рис.2.6), обращая особое внимание на обработку ассемблером различных командных форматов и способов адресации. Заметим, что предложение START задает теперь 0 в качестве начального адреса программы. Как будет сказано в следующем разделе, это является признаком перемещаемой программы. Однако процедура трансляции команд остается при этом такой же, как если бы программа действительно загружалась с адреса 0. Трансляция команд вида регистр-регистр, таких как CLEAR (строка 125) или СОМРК (строка 150), не представляет никаких новых проблем. Ассемблер должен просто преобразовать мнемонические коды операций в их машинное представление (используя ОРТАВ) и заменить мнемонические имена регистров на их числовые эквиваленты. Так же как и для команд других видов, это делается во время второго просмотра. Преобразование имен регистров в их числовые значения может делаться с помощью отдельной таблицы. Однако часто бывает удобно использовать для этих целей таблицу имен. Для этого в SYMTAB заранее заносятся имена регистров (АX и т. д.) и их значения (О, 1 и т.д.). Строка 5 0000 10 0000 12 0003 13 15 0006 20 000A 25 000D 30 0010 35 0013 40 0017 45 001A 50 001D 55 0020 60 0023 65 0026 70 002A 80 002D 95 0030 100 0033 105 0036 110 115 120 125 1036 130 1038 132 103A 133 103C 135 1040 140 1043 145 1046 150 1049 155 104B 160 104E 165 1051 170 1053 175 1056 180 1059 185 105C 195 200 205 210 105D Адрес Исходное предложение Объектный код COPY START 0 FIRST STL RETADR 17202D LDB #LENGTH 69202D BASE LENGTH CLOOP +JSUB RDREC 4B101036 LDA LENGTH 032026 COMP #0 290000 JEQ ENDFIL 332007 +JSUB WRREC 4B10105D J CLOOP 3F2FEC ENDFIL LDA EOF 032010 STA BUFFER 0F2016 LDA #3 010003 STA LENGTH 0F200D +JSUB WRREC 4B10105D J @RETADR 3E2003 EOF BYTE C"EOF" 454F46 RETADR RESW 1 LENGTH RESW 1 BUFFER RESB 4096 * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * RDREC CLEAR X B410 CLEAR A B400 CLEAR S B440 +LDT #4096 75101000 RLOOP TD INPUT E32019 JEQ RLOOP 332FFA RD INPUT DB2013 COMPR A,S A004 JEQ EXIT 332008 STCH BUFFER,X 57C003 TIXR T B850 JLT RLOOP 3B2FEA EXIT STX LENGTH 134000 RSUB 4F0000 INPUT BYTE X"F1" F1 * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * WRREC CLEAR X B410 212 215 220 225 230 235 240 245 250 255 105F LDT 1062 WLOOP TD 1065 JEQ 1068 LDCH 106B WD 106E TIXR 1070 JLT 1073 RSUB 1076 OUTPUT BYTE END LENGTH 774000 OUTPUT E32011 WLOOP 332FFA BUFFER,X 53C003 OUTPUT DF2008 T B850 WLOOP 3B2FEF 4F0000 X"05" 05 FIRST Рис.2.6 Объектный код для программы на рис.2.5. Для трансляции большинства команд вида память-регистр используется относительный способ адресации (либо относительно счетчика команд, либо относительно базы). В любом случае ассемблер должен вычислить смещение, которое является составной частью объектной команды. Оно вычисляется так, чтобы после его сложения с регистром счетчика команд (PC) или базовым регистром (В) получить требуемый целевой адрес. Конечно, величина полученного смещения должна быть не слишком большой, чтобы поместиться в 12-разрядном поле команды. Это означает, что величина смещения должна находиться в диапазоне от 0 до 4095 (для адресации относительно базы) или в диапазоне от -2048 до +2047 (для адресации относительно счетчика команд). Если не подходит ни один из относительных способов адресации (из-за того что величина смещения слишком велика), следует использовать расширенный 4-байтовый командный формат (формат 4). Этот формат имеет 20-разрядное адресное поле, позволяющее хранить полный адрес. В этом случае никакого смещения не вычисляется. Например, в команде 15 0006 CLOOP + JSUB RDREC 4В101036 адрес команды равен 1036. Это полный адрес, который хранится в команде вместе с признаком, указывающим на использование расширенного командного формата (разряд е установлен в 1). Отметим, что программист должен с помощью префикса + сам указать команды, в которых используется расширенный командный формат (см. строку 15). Если нельзя применить ни один из способов относительной адресации и нет указания об использовании расширенного формата, то правильная трансляция команды невозможна. В этом случае ассемблер должен выдать сообщение об ошибке. Рассмотрим теперь детально, как вычисляется смещение для относительных способов адресации. По существу, ассемблер должен в обратном порядке выполнить те операции, которые выполняются при вычислении целевого адреса. Вам следует вспомнить, как это делается, посмотрев еще раз разд. 1.3.2. Типичным примером команды, в которой используется адресация относительно счетчика команд, является команда 10 0000 FIRST STL RETADR 17202D Во время выполнения команд в УУМ/ДС (как и в большинстве ЭВМ) счетчик команд продвигается вперед после выборки очередной команды (но до ее выполнения). Таким образом, во время выполнения команды STL регистр РС будет содержать адрес следующей команды (в данном случае 0003). Из листинга мы также видим, что метка RETADR (строка 95) имеет адрес 0030. (Ассемблер, естественно может получить эту информацию из SYMTAB.) Необходимое нам смещение вычисляется как 30 - 3 = 2D. Во время выполнения этой команды ее целевой адрес будет вычисляться как (PC) + disp, что и даст нужный нам результат (0030). Обратите внимание, что разряд р установлен в 1, что является признаком адресации относительно счетчика команд. Таким образом, два последних байта машинной команды содержат код 202. Тот факт, что в данной команде не используется ни косвенная адресация, ни непосредственное задание операнда, указывается установкой 1 в разряды n и i. Поэтому первый байт машинной команды содержит код 17, а не 14 (см. описание значений разрядов-признаков на рис.1.1 в разд.1.3.2). Другим примером адресации относительно сметчика команд может служить предложение 40 0017 J CLOOP 3F2FEC Здесь адрес операнда равен 0006. Во время выполнения команды регистр PC будет иметь значение 0001А. Величина смещения вычисляется как 6 - 1A = -14. Taк как это отрицательное число, то оно будет представлено в дополнительном коде. В машиной команде для поля смещения отводится 12 разрядов. Поэтому смещение будет записано как FEC. Аналогичным образом вычисляется смещение для адресации относительно базового регистра. Основное отличие заключается в том, что ассемблеру известно значение счетчика команд на момент выполнения программ, в то время как значение базового регистра определяется программистом. Поэтому программист должен сообщить ассемблеру значение базового регистра на момент выполнения программы. В нашем примере это делается с помощью директивы ассемблера BASE. Данное предложение (строка 13) сообщает ассемблеру, что базовый регистр будет содержать адрес метки LENGTH. Предшествующая команда LDB #LENGTH загружает это значение в регистр во время работы программы. До тех пор, пока ассемблер не встретит другую команду BASE, он будет полагать, что значение базового регистра не меняется. Возможно, что в последующих фрагментах программы будет желательно использовать регистр В для других целей (например, для временного хранения некоторого значения). В этом случае программист должен использовать другую директиву ассемблера (например, NOBASE), для того, чтобы сообщить ему, что содержимое базового регистра теперь нельзя использовать для базирования. Важно уяснить, что BASE и NOBASE это директивы ассемблера, которые не переводятся в выполняемые машинные команды. Для загрузки базового регистра требуемым значением программист должен предусмотреть в программе соответствующие команды. Если этого не сделать, то целевой адрес будет вычисляться неверно. Типичным примером команды, в которой используется адресация относительно базы, является команда 160 104Е STCH BUFFER,X 57С003 В соответствии с предложением BASE регистр В будет содержать во время исполнения программы значение 0030 (адрес LENGTH). Адрес метки BUFFER равен 0036. Таким образом, смещение определяется как 36 - 33 = 3. Заметим, что в машинном представлении команды оба разряда х и b установлены в 1, что является признаком адресации относительно базы с индексированием. Другим примером может служить команда STX LENGTH (строка 175). Здесь смещение равно 0. Отметим различия в трансляции предложений в строках 20 и 175. В строке 20 для предложения LDA LENGTH использована адресация относительно счетчика команд. В строке 175 для STX LENGTH использована адресация относительно базы. (Если вы вычислите для этой команды смещение относительно счетчика команд, то убедитесь, что его значение слишком велико для 12-разрядного поля.) Для предложения в строке 20 можно использовать оба способа адресации. Однако наш ассемблер вначале делает попытку использовать адресацию относительно счетчика команд. Выбор приоритета того или иного способа адресации достаточно произволен и устанавливается разработчиком ассемблера. Трансляция команд, в которых используются непосредственные операнды, проще, поскольку в них не нужна организация ссылок на оперативную память. Все, что требуется, это преобразовать операнд во внутреннее представление и занести его в команду. Типичным примером команды, в которой используется непосредственный операнд, является команда 55 0020 LDA #3 010003 Операнд 003 хранится непосредственно в команде. Разряд i установлен в 1, что является признаком непосредственного операнда. Другим примером может служить команда 133 103С +LDT #4096 75101000 В данном случае операнд (4090) слишком велик для 12-разрядного поля и поэтому используется расширенный командный формат. (Если бы операнд был велик и для 20разрядного поля, то мы не могли бы задать его непосредственно в команде.) Другой способ использования непосредственного операнда показан в команде 12 0003 LDB #LENGTH 69202D В этом предложении непосредственный операнд задан именем LENGTH. Поскольку его значением является адрес, то в результате выполнения этой команды в регистр В будет загружен адрес, назначенный переменной LENGTH. Заметим, что в данном случае используется комбинация адресации относительно счетчика команд с непосредственным заданием операнда. Хотя на первый взгляд такая адресация может выглядеть не совсем обычной, тем не менее она не противоречит предыдущим рассуждениям. Дело в том, что в общем случае всегда вначале вычисляется целевой адрес, и если в команде установлен признак непосредственного операнда, то этот адрес (а не содержимое по этому адресу) будет использован в качестве операнда. (В предложении LDA в строке 55 разряды х, b и р установлены в 0. Таким образом, целевой адрес просто совпадает со смещением 003.) Ассемблирование команд, использующих косвенную адресацию, (фактически не представляет ничего нового. Для того чтобы получить требуемый целевой адрес, вначале обычным образом вычисляется смещение. Затем в разряд n генерируемой команды устанавливается признак косвенной адресации. Примером использования адресации относительно счетчика команд в комбинации с косвенной адресацией является предложение в строке 70. 2.2.2. Перемещение программ Как было отмечено ранее, очень часто желательно иметь возможность одновременно выполнять несколько программ, разделяющих между собой оперативную память и другие ресурсы машины. Если бы нам заранее было точно известно, какие программы будут выполняться одновременно, то мы могли бы во время ассемблирования назначить каждой программе подходящие адреса таким образом, чтобы не было ни взаимного перекрытия программ, ни потери места в оперативной памяти. Однако чаще всего столь точное планирование выполнения программ не пригодно с практической точки зрения. (Обычно мы не знаем абсолютно точно, когда задания начнут выполняться, сколько времени займет их выполнение и т. п.). Поэтому хотелось бы иметь возможность загружать программу на любое место в оперативной памяти, где для нее имеется достаточно пространства. В этом случае фактический начальный адрес программы не известен до момента загрузки. Программа, которую мы рассмотрели в разд. 2.1, представляет собой пример абсолютной программы (или абсолютного ассемблирования). Для того чтобы она правильно выполнялась, ее необходимо загрузить с адреса 1000 (т. е. с адреса, назначенного при ассемблировании). Чтобы убедиться в этом, рассмотрим команду 55 101В LDA THREE 00102D из программы, представленной на рис.2.2. В объектной программе (рис. 2.3) этому предложению соответствует команда 001020, которая обеспечивает загрузку содержимого оперативной памяти по адресу 102D в регистр А. Предположим, что мы попытались загрузить и выполнить эту программу, с адреса 2000 (вместо 1000). В этом случае адрес 102D не будет содержать ожидаемого значения. Скорее всего данный адрес будет принадлежать какой-либо другой пользовательской программе. Очевидно, для того чтобы программа могла правильно работать, начиная с адреса 2000, мы должны сделать определенные изменения в адресной части данной команды. С другой стороны, некоторые фрагменты программы (например, константа 3, сгенерированная в строке 85) должны остаться неизменными вне зависимости от начального адреса загрузки. Если рассматривать объектный код отдельно, то в общем случае невозможно определить какие значения представляют адреса, зависящие от месторасположения программы, а какие - неизменяемые объекты. Поскольку ассемблеру не известен фактический адрес начала загрузки, он не может выполнить необходимую настройку адресов, используемых программой. Однако ассемблер может указать загрузчику те части объектной программы, которые нуждаются в настройке при загрузке. Объектная программа, содержащая информацию, необходимую для выполнения подобной модификации, называется перемещаемой программой. Для того чтобы детально обсудить возникающие здесь проблемы, рассмотрим программу на рис.2.5 и 2.6. В предыдущем разделе мы ассемблировали данную программу с нулевым начальным адресом. На рис. 2.7а показана ее загрузка с адреса 0. Команда JSUB (строка 15) загружается, начиная с адреса 0006. Ее адресное поле содержит значение 01036, которое является адресом команды с меткой RDREC. (Это, конечно, те же самые адреса, которые были назначены ассемблером.). Предположим теперь, что мы хотим загрузить эту же программу с адреса 5000, как показано на рис. 2.7б. Теперь команда с меткой RDREC имеет адрес 6036. Таким образом, адресная часть команды JSUB должна быть модифицирована так, как показано на рисунке. Аналогично, если бы мы загрузили программу с адреса 7420 (рис. 2.7в), то потребовалось бы приведение команды JSUB к виду 4В108456, чтобы ее адресная часть соответствовала новому значению адреса RDREC. Обратите внимание, что независимо от того, куда загружается программа, разница между адресом RDREC и началом программы составляет 1036 байт. Это означает, что мы можем 0000 0006 1036 1076 4В101036 В410 а (+JSUB RDREC) RDREC 5000 5006 6036 6076 4В106036 В410 (+JSUB RDREC) RDREC 7420 7426 RDREC) 8456 8496 б 4В108456 В410 (+JSUB RDREC в Рис. 2.7. Примеры перемещения программ. решить проблему перемещения программы следующим образом: 1. Когда ассемблер генерирует объектный код команды JSUB, мы полагаем, что он заносит адрес метки RDREC относительно начала программы. (Вот почему мы установили вначале счетчик адреса равным 0.). 2. Ассемблер, кроме того, вырабатывает команду для загрузчика, которая предписывает ему прибавить к содержимому адресного поля команды JSUB начальный адрес программы. Естественно, что команда загрузчика должна быть составной частью объектной программы. Это можно сделать с помощью дополнительного типа записи объектного формата - записи-модификатора. Эта запись имеет следующий формат: Запись-модификатор: Столбец 1 М. Столбцы 2-7 Начальный адрес модифицируемого адресного поля относительно начала программы (шестнадцатеричный). Столбцы 8-9 Длина модифицируемого адресного поля в полубайтах (шестнадцатеричная). Длина хранится с полубайтах (а не в байтах), так как модифицируемое адресное поле не обязательно содержит целое число байтов. (Например, адресное поле в рассмотренной выше команде JSUB занимает 20 разрядов, что составляет 5 полубайтов.) Месторасположение модифицируемого адресного поля задается адресом байта, в котором содержатся старшие разряды адресного поля. Если модифицируемое адресное поле занимает нечетное количество полубайтов, то предполагается, что оно начинается с середины байта, адрес которого задан в столбцах 2-7. Эти соглашения, естественно, тесно связаны с архитектурой УУМ/ДС. Для других типов машин представление длины адресного поля в полубайтах может оказаться неудобным (см. упр. 2.2.7). Для команды JSUB, которую мы используем в качестве примера, записьмодификатор должна иметь вид М00000705 Она указывает на то, что начальный адрес программы необходимо добавить к полю, которое начинается с адреса 00007 (относительно начала программы), и что длина адресного поля равна 5 полубайтам. Таким образом, в ассемблированной команде 4В101036 первые 12 разрядов (код 4В1) останутся неизменными. Адрес начала загрузки программы будет добавлен к последним 20 разрядам (01036) для того, чтобы получить правильный адрес операнда. (Вам следует самостоятельно убедиться в том, что это даст результат, показанный на рис. 2.7.). Точно такие же действия необходимо выполнить для перемещения команд, заданных в строках 35 и 65 (распечатка 4). Остальные команды не требуют модификации при загрузке. В одних случаях потому, что операнды команд расположены не в оперативной памяти (например, CLEAR S или LDA #З). В других потому, что для задания операнда использована относительная адресация. Например, команда в строке 10 (STL RETADR) ассемблируется с помощью адресации относительно счетчика команд со смещением 02D. Неважно, с какого адреса будет фактически загружена программа. В любом случае слово, помеченное меткой RETADR, будет смещено на заданную величину (2D) относительно команды STL. Поэтому данную команду модифицировать не нужно, так как во время ее выполнения счетчик команд будет содержать реальный адрес следующей команды и, таким образом, процесс вычисления целевого адреса обеспечит получение правильного реального адреса, соответствующего RETADR. Точно так же расстояние между LENGTH и BUFFER всегда будет равно трем байтам. Таким образом, после смещения команды в строке 160, использующей адресацию относительно базы, не будет требовать модификации при перемещении программы. (Содержимое базового регистра будет, конечно, зависеть от месторасположения программы однако его значение устанавливается автоматически при выполнении команды LDB #LENGTH, в которой используется адресация относительно счетчика команд.) H_COPY _000000_001077 T_000000_1D_17202D_69202D_4B101036_032026_290000_332007_4B10105D_3F2FIC_032 010 T_00001D_13_0F2016_010003_0F200D_4B10105D_3E2003_454F46 T_001036_1D_B410_B400_B440_75101000_E32019_332FFA_DB2013_A004_332008_57C0 03_B850 T_001053_1D_3B2FEA_134000_4F0000_F1_B410_774000_E32011_332FFA_53C003_DF20 08_B850 T_001070_07_3B2FEF_4F0000_05 M_000007_05 M_000014_05 M_000027_05 E_000000 Рис. 2.8. Объектная программа, соответствующая рис.2.6. Теперь нам абсолютно ясно, что модификация во время загрузки требуется только в командах, использующих прямую (как альтернатива относительной) адресацию. Для программ УУМ/ДС прямая адресация используется только в расширенном (4-байтовом) командном формате. Это существенное преимущество относительной адресации. Если бы мы попытались переместить программу, приведенную на рис.2.1, то увидели бы, что почти все команды требуют модификации. На рис. 2.8 показано объектное представление, соответствующее исходной программе на рис.2.5. Обратите внимание, что записи тела программы остались такими же, как если бы они были подготовлены абсолютным ассемблером (с нулевым начальным адресом). Однако теперь адреса загрузки рассматриваются не как абсолютные, а как относительные величины. (То же самое, конечно, справедливо для адресов в записяхмодификаторах и записи-конце.) Для каждого адресного поля, требующего настройки при перемещении программы, имеется своя запись-модификатор (в нашем случае их 3 и они все для команд +JSUB). Вам следует самостоятельно проверить каждую записьмодификатор и убедиться, что вы поняли, как она формируется. В гл. 3 мы детально рассмотрим, как загрузчик осуществляет требуемую модификацию программ. Сейчас важно, чтобы вы хорошо уяснили изложенные здесь концепции, так как они послужат основой для материала, обсуждаемого в следующем разделе. 2.3. Машинно-независимые характеристики ассемблера В этом разделе мы обсудим некоторые общие характеристики ассемблера, не имеющие тесной связи со структурой машины. Конечно, более совершенные машины обычно имеют более сложное программное обеспечение, и поэтому средства ассемблера, которые мы здесь рассмотрим, вероятнее встретить на больших и сложных машинах. Однако наличие или отсутствие этих средств определяет скорее такие факторы, как удобство программирования или операционное окружение, нежели структура конкретной машины. В разд. 2.3.1 мы обсудим таблицы и алгоритмы, необходимые для реализации литералов. В разд.2.3.2 будут рассмотрены две директивы ассемблера (EQU и ORG), основной функцией которые является определение имен. В разд. 2.3.3 коротко рассматриваются выражения в языке ассемблера. Обсуждаются различные типы таких выражений, их вычисление и использование. В разд.2.3.4 и 2.3.5 мы познакомимся с такими важными понятиями, как программный блок и управляющая секция. Мы обсудим доводы в пользу включения этих средств в ассемблер и проиллюстрируем их использование с помощью примеров. Мы также познакомимся с набором директив ассемблера, которые служат для поддержания этих средств, и обсудим пути их реализации. 2.3.1. Литералы Очень часто бывает удобно иметь возможность записывать значения константы, используемой в качестве операнда, непосредственно в команде, где она используется. Это позволяет отказаться от использования отдельного предложения для определения константы и соответствующей ей метки. Такой операнд называется литеральным (literal), поскольку значение константы задается в виде строки символов. На рис.2.9 приведен пример использования литералов. Объектный код, сгенерированный для предложений этой программы, показан на рис.2.10. (Эта программа является модификацией программы, представленной на рис.2.5; другие изменения мы обсудим в разд. 2.3.) Строка Исходное предложение 5 COPY START 0 КОПИРОВАНИЕ ФАЙЛА 10 FIRST STL RETADR СОХРАНЕНИЕ АДРЕСА ВОЗВРАТА 13 LDB #LENGTH УСТАНОВКА БАЗОВОГО РЕГИСТРА 14 BASE LENGTH 15 CLOOP +JSUB RDREC ВВОД ВХОДНОЙ ЗАПИСИ 20 LDA LENGTH ПРОВЕРКА НА EOF (LENGTH = 0) 25 COMP #0 30 JEQ ENDFIL ВЫХОД, ЕСЛИ НАШЛИ EOF 35 +JSUB WRREC ВЫВОД ВЫХОДНОЙ ЗАПИСИ 40 J CLOOP ЦИКЛ 45 ENDFIL LDA =C"EOF" ЗАНЕСЕНИЕ МАРКЕРА КОНЦА ФАЙЛА 50 STA BUFFER 55 LDA #3 УСТАНОВИТЬ LENGTH = 3 60 STA LENGTH 65 +JSUB WRREC ЗАПИСЬ EOF 70 J @RETADR ВОЗВРАТ ИЗ ПРОГРАММЫ 93 LTORG 95 RETADR RESW 1 100 LENGTH RESW 1 ДЛИНА ЗАПИСИ 105 BUFFER RESB 4096 ДЛИНА БУФЕРА – 4096 БАЙТ 106 BUFEND EQU * 107 MAXLEN EQU BUFEND-BUFFER МАКСИМАЛЬНАЯ ДЛИНА ЗАПИСИ 110 * 115 * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР 120 * 125 RDREC CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 130 CLEAR A ОБНУЛЕНИЕ РЕГИСТРА А 132 CLEAR S ОБНУЛЕНИЕ РЕГИСТРА S 133 +LDT #MAXLEN 135 RLOOP TD INPUT ПРОВЕРКА УСТРОЙСТВА ВВОДА 140 JEQ RLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 145 RD INPUT ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 150 COMP A,S ПРОВЕРКА НА КОНЕЦ ЗАПИСИ (Х"00") 155 JEQ EXIT ВЫХОД ИЗ ЦИКЛА ПО КОНЦУ ЗАПИСИ 160 STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР 165 TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 170 JLT RLOOP ДЛИНЫ 175 EXIT STX LENGTH ЗАПОМИНАНИЕ ДЛИНЫ ЗАПИСИ 180 RSUB ВОЗВРАТ ИЗ ПОДПРОГРАММЫ 185 INPUT BYTE X"F1" КОД УСТРОЙСТВА ВВОДА 195 * 200 * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА 205 * 210 WRREC CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 212 LDT LENGTH 215 WLOOP TD ВЫВОДА 220 JEQ 225 LDCH 230 WD 235 TIXR 240 JLT 245 RSUB 255 END =X"05" WLOOP BUFFER,X =X"05" T WLOOP ПРОВЕРКА УСТРОЙСТВА ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ ЧТЕНИЕ СИМВОЛА ИЗ БУФЕРА ВЫВОД СИМВОЛА ЦИКЛ, ПОКА НЕ БУДУТ ВЫВЕДЕНЫ ВСЕ СИМВОЛЫ ВОЗВРАТ ИЗ ПОДРОГРАММЫ FIRST Рис. 2.9. Программа, демонстрирующая дополнительные возможности ассемблера. Строка 5 0000 10 0000 13 0003 14 15 0006 20 000A 25 000D 30 0010 35 0013 40 0017 45 001A 50 001D 55 0020 60 0023 65 0026 70 002A 93 002D 95 0030 100 0033 105 0036 106 1036 107 1000 110 115 120 125 1036 130 1038 132 103A 133 103C 135 1040 140 1043 145 1046 150 1049 155 104B 160 104E 165 1051 170 1053 175 1056 Адрес Исходное предложение Объектный код COPY START 0 FIRST STL RETADR 17202D LDB #LENGTH 69202D BASE LENGTH CLOOP +JSUB RDREC 4B101036 LDA LENGTH 032026 COMP #0 290000 JEQ ENDFIL 332007 +JSUB WRREC 4B10105D J CLOOP 3F2FEC ENDFIL LDA =С"EOF" 032010 STA BUFFER 0F2016 LDA #3 010003 STA LENGTH 0F200D +JSUB WRREC 4B10105D J @RETADR 3E2003 LTORG * =C"EOF" 454F46 RETADR RESW 1 LENGTH RESW 1 BUFFER RESB 4096 BUFEND EQU * MAXLEN EQU BUFEND-BUFFER * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * RDREC CLEAR X B410 CLEAR A B400 CLEAR S B440 +LDT #MAXLEN 75101000 RLOOP TD INPUT E32019 JEQ RLOOP 332FFA RD INPUT DB2013 COMP A,S A004 JEQ EXIT 332008 STCH BUFFER,X 57C003 TIXR T B850 JLT RLOOP 3B2FEA EXIT STX LENGTH 134000 180 185 195 200 205 210 212 215 220 225 230 235 240 245 255 1059 RSUB 4F0000 105C INPUT BYTE X"F1" F1 * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * 105D WRREC CLEAR X B410 105F LDT LENGTH 774000 1062 WLOOP TD =X"05" E32011 1065 JEQ WLOOP 332FFA 1068 LDCH BUFFER,X 53C003 106B WD =X"05" DF2008 106E TIXR T B850 1070 JLT WLOOP 3B2FEF 1073 RSUB 4F0000 END FIRST 1076 * =X"05" 05 Рис.2.10. Объектный код для программы на рис.2.9. В нашем языке ассемблера литералы задаются с помощью префикса =, за которым следует константа, задаваемая точно так же, как в предложении BYTE. Таким образом, в предложении 45 001A ENDFIL LDA =С'ЕОF' 032010 литерал определяет 3-байтовый операнд, значением которого является строка символов EOF. Аналогично в предложении 215 1062 WLOOP ТD =X'05' ЕЗ2011 задается 1-байтовый литерал, имеющий шестнадцатеричное значение 05. Символьная нотация для задания литералов может быть различной, однако в большинстве ассемблеров для того, чтобы упростить идентификацию литералов, используется определенный символ (у нас =). Важно уяснить различие между литералом и непосредственным операндом. В случае непосредственного операнда его значение транслируется как составная часть машинной команды. В случае литерала его значение генерируется в виде константы в оперативной памяти, а ее адрес используется в качестве целевого адреса команды. Употребление литерала дает точно та кой же результат, как если бы программист явно определил константу и использовал в качестве операнда ее метку. (Действительно, сгенерированный объектный код для строк 45 и 215 в распечатке 5 идентичен объектному коду для соответствующих строк программы на рис.2.6.) Вам следует сравнить объектный код, сгенерированный для строк 45 и 55 (рис.2.10), для того, чтобы убедиться, что вы уяснили разницу в обработке литералов и непосредственных операндов. Все литеральные операнды, используемые в программе, объединяются в один или несколько литеральных пулов (literals pools). Обычно литеральный пул помещается в конце программы и распечатывается в ее листинге. В литеральном пуле для каждого литерала показаны назначенный ему адрес и сгенерированное значение. Пример такого литерального пула приведен на рис.2.10 (он расположен непосредственно после предложения END). В данном случае пул состоит только из одного литерала =X'05'. Однако в некоторых случаях хотелось бы размещать литеральный пул в другом месте объектной программы. Это можно сделать с помощью директивы ассемблера LTORG (строка 93 на рис.2.10). Когда ассемблер встречает предложение LTORG, он создает литеральный пул, содержащий все литеральные операнды, которые были использованы в программе с момента обработки предыдущего предложения LTORG (или с начала программы). Литеральный пул помещается в объектную программу в то место, где было встречено предложение LTORG (см. рис.2.10). Конечно, литералы, помещенные в литеральный пул по команде LTORG, не будут повторно помещаться в литеральный пул в конце программы. Если бы мы не использовали предложение LTORG в строке 93, то литерал =С'EOF' был бы помещен в литеральный пул в конце программы. В этом случае данный операнд получил бы адрес 1073 и мы не смогли бы использовать для него адресацию относительно счетчика команд. Конечно, все дело в том, что мы зарезервировали очень большое пространство для массива BUFFER. Расположив литеральный пул перед этим массивом, мы смогли избежать использования расширенного командного формата для ссылок на литералы. Обычно необходимость в команде, подобной LTORG, возникает тогда, когда желательно расположить литеральные операнды в непосредственной близости от команд, в которых они используются. Большинство ассемблеров умеют распознавать повторное использование литералов и хранят только один экземпляр каждого из них. Например, литерал =X'05' используется в нашей программе в строках 215 и 230. Однако для его хранения генерируется только одна константа и операнды обеих команд ссылаются на адрес этой константы. Простейший способ распознать повторное использование литерала заключается в сравнении определяющих их строк символов (в нашем случае это строка =Х'05'). Однако в некоторых случаях лучше сравнивать не определения, а сгенерированные коды. Например, литералы =C'EOF' и =X'454F46' определяют идентичные значения. Если бы ассемблер умел распознавать их эквивалентность, то он мог бы не заводить дублирующую константу. В то же время выигрыш от подобного способа обработки литералов обычно не столь велик, чтобы оправдать дополнительные усложнения ассемблера. Если для распознавания повторного использования литералов используется символьная строка, то следует быть внимательными при обработке литералов, значение которых зависит от месторасположения в программе. Предположим, что мы разрешаем использовать литералы, которые ссылаются на текущее значение счетчика размещений (часто обозначаются с помощью символа *). Такие литералы иногда бывают полезны для загрузки базового регистра. Например, предложения LDB = * BASE * расположенные в самом начале программы, обеспечивают загрузку в регистр В начального адреса программы. В дальнейшем это значение можно использовать для адресации относительно базы. Однако такое обозначение может вызвать трудности распознавания повторно используемых литералов. Если литерал =* появился бы в нашей программе в строке 13, то его значение было бы 0003. Если тот же операнд был бы задан в строке 55, то он определял бы операнд, имеющий значение 0020. В этом случае литеральные операнды имеют идентичные имена, нo различные значения. Следовательно, оба эти операнда должны быть помещены в литеральный пул. Те же самые проблемы возникают, если литерал ссылается на какой-либо другой объект, значение которого изменяется между двумя точками программы. Теперь мы готовы к тому, чтобы описать, как ассемблер обрабатывает литеральные операнды. Основной структурой данных, используемой при обработке литералов, является таблица литералов LITTAB. Для каждого использованного в программе литерала эта таблица содержит следующую информацию: имя литерала, его значение, длину и назначенный ему адрес. Очень часто LITTAB организуется в виде хеш-таблицы, в которой в качестве ключа используется имя литерала или его значение. Если во время первого просмотра ассемблер встречает литеральный операнд, то он пытается найти его в LITTAB. Если литерал уже присутствует в таблице, то никаких дополнительных действий не требуется. Если литерала в таблице еще нет, то он помещается в LITTAB (значение адреса пока остается неопределенным). Когда во время первого просмотра встречается предложение LTORG или конец программы, ассемблер просматривает таблицу литералов и назначает каждому литералу его адрес (за исключением тех литералов, которым адреса были назначены ранее). После назначения адреса для очередного литерала счетчик размещений продвигается на длину литерального операнда. Во время второго просмотра адреса литеральных операндов извлекаются из LITTAB и используются для генерации объектного кода. Значения литеральных операндов в литеральном пуле заносятся в соответствующие места объектной программы точно так же, как если бы эти значения были сгенерированы с помощью предложений BYTE или WORD. Если значение литерального операнда представляет собой программный адрес (например, значение счетчика размещений), то ассемблер должен сгенерировать для него соответствующую запись-модификатор в объектном представлении. Для того чтобы убедиться, что вы поняли, как создается и используется таблица литералов, вам следует применить описанную нами процедуру к исходной программе на рис.2.9. Объектный код и литеральные пулы, которые вы получите, должны быть такими, как показано на рис.2.10. 2.3.2. Средства определения имен До сих пор в рассмотренных нами ассемблерных программах мы имели дело только с одним способом определения символических имен пользователя. Эти имена появлялись в качестве меток команд или областей данных. Значением такой метки является адрес, назначаемый предложению, в котором она определена. Большинство ассемблеров предоставляет программисту дополнительные средства для определения имен и задания их значений. К числу обычно используемых директив ассемблера относится директива EQU (от EQUate). В общем виде это предложение записывается следующим образом: имя EQU значение Данное предложение определяет некоторое имя (т. е. вносит его в SYMTAB) и присваивает ему значение. Значение может задаваться в виде константы пли выражения, в котором могут использоваться константы и ранее определенные имена. Образование и использование выражений мы обсудим в следующем разделе. Одним из общих применений EQU является введение символических имен вместо числовых значений, чтобы упростить чтение программы. Например, в строке 133 программы на рис.2.5 для того, чтобы загрузить в регистр Т число 4096, мы использовали предложение +LDT #4096 Данная величина представляет собой максимальную длину записи, которую мы можем прочитать с помощью подпрограммы RDREC. Однако смысл этого предложения далеко не очевиден. Если мы включим в нашу программу предложение MAXLEN EQU 4096 то сможем записать строку 133 как +LDT #MAXLEN Когда ассемблер встретит предложение EQU, он занесет МАХLEN в таблицу имен (со значением 4096). Во время трансляции команды LDT ассемблер найдет MAXLEN в SYMTAB и использует его значение в качестве операнда. Получаемый при этом объектный код будет таким же, как и в предыдущей версии, однако исходная программа легче для понимания. Кроме того, если понадобится изменить значение величины, задающей максимальную длину записи, то проще сделать это в одном предложении EQU, чем искать все вхождения 4096 в исходной программе. Другое общее применение EQU заключается в определении мнемонических имен для регистров. Мы предположили, что наш ассемблер распознает стандартные мнемонические имена регистров - А, X, L и т. д. Однако представим себе, что ассемблер требует использовать для задания регистров их номера (например, в команде RMO). Тогда мы должны были бы писать RMO 0, 1 вместо RMO А, X. В этом случае программист мог бы включить в свою программу следующую последовательность предложений: А EQU 0 X EQU 1 L EQU 2 . . . В результате обработки этих предложений имена А, X, L,... были бы занесены в SYMTAB и имели бы значения 0, 1, 2,... . После этого команда, подобная RMO А, X, была бы допустима. Ассемблер, обрабатывая такое предложение, должен был бы просмотреть SYMTAB и при трансляции использовать для имен А и X соответствующие им значения 0 и 1. На машине, подобной УУМ, задание для регистров пользовательских имен большого смысла не имеет. Гораздо проще использовать символические имена, встроенные в ассемблер. К тому же стандартные имена (base, index и т. п.) отражают способ использования регистров. Рассмотрим, однако, машину, имеющую регистры общего назначения (например, System/370). Обычно для обозначения этих регистров используются номера вида 0, 1, 2,... или имена вида R0, R1, R2,... . В конкретной программе некоторые из них могут быть использованы как базовые регистры, другие - как индексные, третьи - как сумматоры и т. д. Более того, в различных программах функциональное распределение регистров может быть различным. В этом случае программист может определить имена, отражающие функциональное назначение регистров конкретной программы, с помощью следующих предложений: BASE EQU 1 COUNT EQU 2 INDEX EQU 3 Имеется еще одна общеупотребительная директива ассемблера, позволяющая косвенно задавать значения символическим именам. Обычно эта директива называется ORG (от ORiGin). Она имеет следующий вид: ORG значение где значение представляет собой константу или выражение, в котором могут быть использованы константы и ранее определенные имена. Если во время ассемблирования программы встречается данное предложение, то заданное им значение заносится в счетчик размещений (LOCCTR). Поскольку для определения значений имен используется LOCCTR, то предложение ORG распространяет свое влияние на все последующие определения меток. Конечно, ввиду того что счетчик размещений используется для управления распределением памяти в объектной программе, его изменение чаще всего приводит к ошибкам ассемблирования. Однако иногда предложение ORG может быть полезно. Предположим, что мы хотим определить таблицу, имеющую следующую структуру: STAB 100 элементов SYMBOL VALUЕ FLAGS 100 элементов . . . . . . . . . В этой таблице SYMBOL представляет собой 6-байтовое поле для хранения имен; VALUE - 1-словное поле для хранения значения, присвоенного имени; FLAGS - 2-байтовое поле для спецификации типа и другой информации. Мы могли бы зарезервировать пространство для этой таблицы с помощью предложения STAB RESB 1100 Естественно, что мы хотим иметь возможность ссылаться на элементы таблицы с помощью индексной адресации (помещая в индексный регистр величину смещения требуемого элемента относительно начала таблицы). Так как нам нужен доступ к отдельным полям элементов, то мы должны определить метки SYMBOL, VALUE и FLAGS. С помощью предложений EQU это можно сделать следующим образом: SYMBOL EQU STAB VALUE EQU STAB+6 FLAGS EQU STAB+9 Теперь для ссылки на поле VALUE мы можем использовать предложения вида LDA VALUE,X Однако такой способ определения лишь задает метки, но не дает наглядного представления о структуре таблицы. С помощью предложения ORG мы можем определить те же имена следующим образом: STAB RESB 1100 ORG STAB SYMBOL RESB 6 VALUE RESW 1 FLAGS RESB 2 ORG STAB + 1100 Первое предложение ORG заносит в счетчик размещений начальный адрес таблицы STAB. В следующем предложении метке SYMBOL будет присвоено текущее значение LOCCTR (т. е. тот же адрес, что и STAB). Затем LOCCTR будет продвинут и его новое значение (STAB + 6) будет присвоено метке VALUE. Аналогично будет определено значение метки FLAGS. В результате эти метки получат такие же значения, как и при определении с помощью EQU, но теперь структура STAB стала очевидной. Последнее предложение ORG очень важно. Оно возвращает в LOCCTR значение, которое было в нем до первого предложения ORG - адрес первого свободного байта, следующего за таблицей STAB. Это необходимо сделать для того, чтобы в по следующих предложениях меткам, которые не являются частью STAB, были назначены правильные адреса. В некоторых ассемблерах предыдущее значение LOCCTR запоминается автоматически, и для его восстановления достаточно просто написать предложение ORG с пустым операндом. Определения, задаваемые предложениями EQU и ORG, содержат ограничения, являющиеся общими для всех директив ассемблера, в которых определяются имена. В случае EQU все метки, используемые в правой части предложения (т. е. все термы, используемые для определения значения нового имени), должны быть уже определены. Таким образом, последовательность предложений ALPHA RESW 1 BETA EQU ALPHA является допустимой, тогда как последовательность BETA EQU ALPHA ALPHA RESW 1 недопустима. Причина этого ограничения кроется в самом процессе распознавания имен. Так, во втором случае мы не сможем присвоить значение метке ВЕТА (так как метка ALPHA еще не определена), а алгоритм работы нашего двухпросмотрового ассемблера требует, чтобы все имена были определены во время первого просмотра. Сходные ограничения применяются и для предложения ORG: все имена, используемые для задания нового значения счетчика размещений, должны быть предварительно определены. Так, например, последовательность ORG ALPHA ВYТЕ1 RESB 1 ВYТЕ2 RESB 1 ВYТЕЗ RESB 1 ORG ALPHA RESW 1 нe может быть обработана. В данном случае ассемблер не знает (во время первого просмотра), какое значение следует установить в счетчик размещений в ответ на первое предложение ORG. В результате невозможно во время первого просмотра вычислить адреса для меток BYTE1, BYTE2, BYTE3. Может показаться, что данное ограничение возникло вследствие конкретного распределения функций между первым и вторым просмотрами нашего ассемблера. На самом деле это проблема носит более общий характер, и она связана с трудностями, возникающими при разрешении ссылок вперед. Легко можно убедиться, что последовательность предложений ALPHA EQU BETA BETA EQU DELTA DELTA RESW 1 нe может быть обработана с помощью обычного двухпросмотрового ассемблера при любом распределении функций между его первым и вторым просмотрами. В разд. 2.4.3 мы вкратце познакомимся, как осуществляется обработка подобных последовательностей в более сложных ассемблерах. 2.3.3. Выражения Во всех предложениях языка ассемблера, рассмотренных нами ранеев качестве операндов использовались отдельные термы (константы, метки и т. п.). В большинстве ассемблеров наряду с одиночными термами разрешается использовать выражения. Каждое такое выражение вычисляется ассемблером во время трансляции, а затем полученное значение используется в виде адреса или непосредственного операнда. Обычно допускаются арифметические выражения, которые строятся по стандартным правилам с помощью операций +,-,*,/. Деление чаще всего определяется как целочисленное. В выражениях можно использовать константы, метки и специальные термы. Одним из таких общеупотребительных специальных термов является терм, ссылающийся на текущую величину счетчика размещений (часто обозначается как *). Значением этого терма является адрес, который будет присвоен очередной команде или области данных. Так, на рис.2.9 предложение 106 BUFEND EQU * устанавливает в качестве значения BUFEND адрес байта, расположенного непосредственно после поля, отведенного под буфер. В разд. 2.2 мы обсуждали проблемы, связанные с перемещением программ. Мы видели, что в объектной программе имеются адреса двух типов: относительные (их значения могут быть вычислены суммированием начального адреса программы с некоторым постоянным смещением) и абсолютные (не зависящие от начального адреса программы). Аналогично могут быть относительными или абсолютными термы и выражения. Константа - абсолютный терм. Метки команд и областей данных, а также ссылка на текущее значение счетчика размещений - относительные термы. Имена, определенные в предложениях EQU (или других подобных директивах ассемблера)могут быть как абсолютными, так и относительными в зависимости от выражений, определяющих их значения. Выражения классифицируются как абсолютные или относительные в зависимости от того, к какому типу относится его значение. Естественно, что выражение, состоящее только из абсолютных термов, является абсолютным выражением. Однако абсолютное выражение может содержать и относительные термы. Для этого необходимо, чтобы относительные термы образовывали пары, причем в каждую пару должен входить как терм со знаком плюс, так и терм со знаком минус. Совершенно необязательно, чтобы парные термы располагались друг за другом. Единственное требование иметь возможность так преобразовать выражение, чтобы все термы могли быть сгруппированы в пары, в каждую из которых входит как положительный, так и отрицательный терм. Относительные термы не могут быть использованы в операциях умножения и деления. Относительное выражение - это выражение, в котором все относительные термы, кроме одного, могут быть объединены в пары, как это было описано выше, причем терм, не вошедший ни в одну из пар, должен быть положительным. Как и в первом случае, относительные термы нельзя использовать в операциях умножения и деления. Выражения, которые не удовлетворяют ни условиям абсолютного выражения, ни условиям относительного выражения, отмечаются ассемблером как ошибочные. Хотя приведенные выше правила могут показаться произвольными, они на самом деле имеют глубокий смысл. К числу выражений, удовлетворяющих данным определениям, относятся только те выражения, значения которых не теряют смысла при перемещении программ. Относительный терм или выражение представляет собой некоторое значение, которое может быть записано в виде (S+r), где S - начальный адрес программы, а r - величина смещения относительно начального адреса. По этому обычно относительный терм задает некоторую точку внутри программы. Когда относительные термы комбинируются в пары, каждая из которых включает термы с противоположными знаками, величины S, определяющие начальный адрес программы, взаимно исключаются. В результате получается абсолютное значение. Рассмотрим, например, программу на рис.2.9. В выражении 107 MAXLEN EQU BUFEND - BUFFER обе метки BUFEND и BUFFER - относительные термы, каждый из которых определяет внутренний адрес программы. Однако само выражение (разность между двумя адресами) является величиной абсолютной и определяет длину в байтах области данных, отведенной под буфер. Значения таких выражений, как BUFEND + BUFFER, 100 - BUFFER или 3 * BUFFER, не представляют ни абсолютную величину, ни внутренний адрес программы. Зависимость этих значений от адреса начальной загрузки такова, что она не имеет связи с чем-либо в самой программе. Поскольку маловероятно, чтобы от таких выражений была какая-либо польза, они рассматриваются как ошибочные. Для того чтобы определить тип выражения, мы должны иметь информацию о типе всех имен, определенных в программе. Эта информация хранится в таблице имен в виде признака, определяющего тип значения (абсолютное или относительное). Так, для программы на рис.2.10 некоторые из элементов таблицы имен могли бы иметь вид ИМЯ ТИП ЗНАЧЕНИЕ RETADR R 0030 BUFFER R 0036 BUFEND R 1036 MAXLEN A 1000 С помощью этой информации ассемблер может легко определить тип каждого выражения и сгенерировать в объектной программе записи-модификаторы для относительных выражений. В разд. 2.3.5 мы рассмотрим программы, состоящие из нескольких частей, каждая из которых может перемещаться независимо от других. Как мы увидим, в этом случае должны быть модифицированы правила для определения типа выражения. 2.3.4. Программные блоки Во всех примерах, которые мы видели до сих пор, ассемблируемая программа обрабатывалась как единое целое. Хотя логически исходная программа содержит подпрограммы, области данных и т. п., она рассматривалась ассемблером как неделимый объект, который переводился в единственный блок объектного кода. Внутри этой объектной программы сгенерированные машинные команды и данные располагались в том же порядке, в котором они были написаны в исходной программе. Многие ассемблеры предоставляют более гибкие средства обработки исходной программы и соответствующего ей объектного кода. Одни из этих средств позволяют располагать сгенерированные машинные команды и данные в объектной программе в порядке, отличном от порядка, в котором расположены соответствующие им исходные предложения. Другие позволяют создавать несколько независимых частей объектной программы, Каждая из этих частей сохраняет свою индивидуальность и обрабатывается загрузчиком отдельно от других частей. Мы используем термин программные блоки (program blocks) для обозначения сегментов, расположенных в пределах одного объектного модуля, и термин управляющие секции (control sections) для обозначения сегментов, которые транслируются в независимые объектные модули. (Данная терминология, к сожалению, не является общепризнанной. Фактически в некоторых системах одни и те же средства языка ассемблера используются для обеспечения обеих этих логически различных функций.) В этом разделе мы рассмотрим как используются и обрабатываются ассемблером программные блоки. Разд. 2.3.5 посвящен обсуждению управляющих секций. На рис.2.11 показано, как с помощью программных блоков может быть записана наша программа. В данном случае используются три блока. Первый (непоименованный) программный блок содержит выполняемые команды. Второй (с именем CDATA) содержит все области данных, длина которых не превышает нескольких слов. Третий (с именем CBLKS) состоит из областей данных, занимающих значительный объем оперативной памяти. Некоторые из возможных доводов и пользу подобного разделения обсуждаются в данном разделе. Строка Исходное предложение 5 COPY START 0 КОПИРОВАНИЕ ФАЙЛА 10 FIRST STL RETADR СОХРАНЕНИЕ АДРЕСА ВОЗВРАТА 15 CLOOP +JSUB RDREC ВВОД ВХОДНОЙ ЗАПИСИ 20 LDA LENGTH ПРОВЕРКА НА EOF (LENGTH = 0) 25 COMP #0 30 JEQ ENDFIL ВЫХОД, ЕСЛИ НАШЛИ EOF 35 JSUB WRREC ВЫВОД ВЫХОДНОЙ ЗАПИСИ 40 J CLOOP ЦИКЛ 45 ENDFIL LDA =C"EOF" ЗАНЕСЕНИЕ МАРКЕРА КОНЦА ФАЙЛА 50 STA BUFFER 55 LDA #3 УСТАНОВИТЬ LENGTH = 3 60 STA LENGTH 65 JSUB WRREC ЗАПИСЬ EOF 70 J @RETADR ВОЗВРАТ ИЗ ПРОГРАММЫ 92 USE CDATA 95 RETADR RESW 1 100 LENGTH RESW 1 ДЛИНА ЗАПИСИ 103 USE CBLKS 105 BUFFER RESB 4096 ДЛИНА БУФЕРА – 4096 БАЙТ 106 BUFEND EQU * АДРЕС ПЕРВОГО БАЙТА ПОСЛЕ БУФЕРА 107 MAXLEN EQU BUFEND-BUFFER МАКСИМАЛЬНАЯ ДЛИНА ЗАПИСИ 110 * 115 * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР 120 * 123 USE 125 RDREC CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 130 CLEAR A ОБНУЛЕНИЕ РЕГИСТРА А 132 CLEAR S ОБНУЛЕНИЕ РЕГИСТРА S 133 +LDT #MAXLEN 135 RLOOP TD INPUT ПРОВЕРКА УСТРОЙСТВА ВВОДА 140 JEQ RLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 145 RD INPUT ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 150 COMP A,S ПРОВЕРКА НА КОНЕЦ ЗАПИСИ (Х"00") 155 JEQ EXIT ВЫХОД ИЗ ЦИКЛА ПО КОНЦУ ЗАПИСИ 160 STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР 165 TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 170 JLT RLOOP ДЛИНЫ 175 EXIT STX LENGTH ЗАПОМИНАНИЕ ДЛИНЫ ЗАПИСИ 180 RSUB ВОЗВРАТ ИЗ ПОДПРОГРАММЫ 183 USE CDATA 185 INPUT BYTE X"F1" КОД УСТРОЙСТВА ВВОДА 195 * 200 * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА 205 * 208 USE 210 WRREC CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 212 LDT LENGTH 215 WLOOP TD =X"05" ПРОВЕРКА УСТРОЙСТВА ВЫВОДА 220 JEQ WLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 225 LDCH BUFFER,X ЧТЕНИЕ СИМВОЛА ИЗ БУФЕРА 230 WD =X"05" ВЫВОД СИМВОЛА 235 TIXR T ЦИКЛ, ПОКА НЕ БУДУТ ВЫВЕДЕНЫ 240 JLT WLOOP ВСЕ СИМВОЛЫ 245 RSUB ВОЗВРАТ ИЗ ПОДРОГРАММЫ 252 USE CDATA 253 LTORG 255 END FIRST Рис. 2.11. Пример программы с несколькими программными блоками. Директива ассемблера USE указывает на то, какие части исходной программы принадлежат к тем или иным блокам, Вначале предполагается, что все предложения исходной программы являются частью непоименованного (задаваемого по умолчанию) блока. Если не использовано ни одного предложения USE, то считается, что вся программа принадлежит одному этому блоку. Предложение USE в строке 92 сигнализирует о начале блока с именем СDАТА. К этому блоку относятся все последующие предложения исходной программы вплоть до следующего предложения USE в строке 103которое начинает блок с именем CBLKS. Предложение USE может также указывать на продолжение ранее начатого блока. Так, предложение в строке 123 продолжает непоименованный блок, а предложение в строке 183 - блок СDАТА. Как вы видите, каждый блок фактически может содержать несколько отдельных сегментов исходной программы. Ассемблер перегруппирует (логически) эти сегменты так, чтобы собрать вместе отдельные части каждого блока. Затем каждому из этих блоков будут назначены адреса в объектной программе в порядке появления блоков в исходной программе. Результат будет в точности таким же, как если бы программист собственноручно перегруппировал предложения исходной программы, собрав вместе предложения, принадлежащие каждому из блоков. Такая логическая перегруппировка осуществляется ассемблером во время первого просмотра. Для этого с каждым программным блоком связывается свой счетчик размещений. При объявлении нового блока связанный с ним счетчик размещений устанавливается в 0. Текущее значение счетчика размещений сохраняется при переключении на другой блок и восстанавливается, когда возобновляется ранее определенный блок. Таким образом, во время первого просмотра каждая метка программы получает адрес относительно начала содержащего ее блока. Когда метка заносится в таблицу имен, то вместе с присвоенным ей адресом заносится имя или номер блока, которому она принадлежит. По завершению первого просмотра значение счетчика размещений каждого блока указывает на длину этого блока. Затем ассемблер может назначить каждому блоку начальный адрес в объектной программе (начиная с относительного адреса 0). Во время второго просмотра для генерации объектного кода ассемблеру для каждого имени нужно знать значение адреса относительно начала объектной программы (а не относительно начала блока). Эту информацию можно легко получить из SYMTAB: достаточно просто прибавить к адресу метки начальный адрес ее блока. Строка Адрес Исходное предложение Объектный код 5 0000 0 COPY START 0 10 0000 0 FIRST STL RETADR 172063 15 0003 0 CLOOP JSUB RDREC 4B2021 20 0006 0 LDA LENGTH 032060 25 0009 0 COMP #0 290000 30 000C 0 JEQ ENDFIL 332006 35 000F 0 JSUB WRREC 4B203B 40 0012 0 J CLOOP 3F2FEE 45 0015 0 ENDFIL LDA =С"EOF" 032055 50 0018 0 STA BUFFER 0F2056 55 001B 0 LDA #3 010003 60 001E 0 STA LENGTH 0F2048 65 0021 0 JSUB WRREC 4B2029 70 0024 0 J @RETADR 3E203F 92 0000 1 USE CDATA 95 0000 1 RETADR RESW 1 100 0003 1 LENGTH RESW 1 103 0000 2 USE CBLKS 105 0000 2 BUFFER RESB 4096 106 107 110 115 120 123 125 130 132 133 135 140 145 150 155 160 165 170 175 180 183 185 195 200 205 208 210 212 215 220 225 230 235 240 245 252 253 1000 2 BUFEND EQU * 1000 MAXLEN EQU BUFEND-BUFFER * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * 0027 0 USE 0027 0 RDREC CLEAR X B410 0029 0 CLEAR A B400 002B 0 CLEAR S B440 002D 0 +LDT #MAXLEN 75101000 0031 0 RLOOP TD INPUT E32038 0034 0 JEQ RLOOP 332FFA 0037 0 RD INPUT DB2032 003A 0 COMP A,S A004 003C 0 JEQ EXIT 332008 003F 0 STCH BUFFER,X 57A02F 0042 0 TIXR T B850 0044 0 JLT RLOOP 3B2FEA 0047 0 EXIT STX LENGTH 13201F 004A 0 RSUB 4F0000 0006 1 USE CDATA 0006 1 INPUT BYTE X"F1" F1 * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * 004D 0 USE 004D 0WRREC CLEAR X B410 004F 0 LDT LENGTH 772017 0052 0 WLOOP TD =X"05" E3201B 0055 0 JEQ WLOOP 332FFA 0058 0 LDCH BUFFER,X 53A016 005B 0 WD =X"05" DF2012 005E 0 TIXR T B850 0060 0 JLT WLOOP 3B2FEF 0063 0 RSUB 4F0000 0007 1 USE CDATA LTORG 0007 1 * =C"EOF" 454F46 000A 1* =X"05" 05 255 END FIRST Рис.2.12. Объектный код для программы на рис.2.11. На рис.2.12 показано, как этот процесс применяется к нашей модельной программе. В столбце "Адрес" приведены относительный адрес (внутри блока) каждого предложения и номер соответствующего ему блока (0 = непоименованный блок, 1 = CDATA, 2 = CBLKS). По существу это та же информация, которая хранится в SYMTAB для каждого имени. Обратите внимание, что для значения MAXLEN (строка 107) номер блока не указан. Это означает, что MAXLEN является абсолютной меткой, значение которой не связано ни с одним из блоков. В конце первого просмотра ассемблер создает рабочую таблицу, которая содержит начальные адреса и длины всех блоков. Для нашей модельной программы таблица выглядит так: Имя блока Номер блока Адрес Длина (непоименованный) 0 0000 0066 CDATA 1 0066 000B CBLKS 2 0071 1000 Рассмотрим теперь команду 20 0006 0 LDA LENGTH 032060 В SYMTAB значение ее операнда (метка LENGTH) определено как относительный адрес 0003, принадлежащий программному блоку 1 (CDATA). Начальный адрес CDATA равен 0066. Таким образом, требуемый целевой адрес для этой команды будет 0003 + 0066 = 0069. Команда должна ассемблироваться с использованием адресации относительно счетчика команд. Во время исполнения данной команды счетчик команд будет содержать адрес следующей команды (строка 25). Относительный адрес этой команды равен 0009 и принадлежит непоименованному блоку. Поскольку этот блок начинается с адреса 0000, то, следовательно, значение этого адреса будет просто равно 0009. Итак, требуемое смещение вычисляется: 0069 - 0009 = 60. Вычисление других адресов осуществляется аналогично. Сразу видно, что разделение программы на блоки существенно уменьшило проблемы адресации. Так как теперь область памяти, отведенная под буфер большого объема, переместилась в конец программы, то отпала необходимость в использовании расширенного командного формата в строках 15, 35, 65. Более того, отпала надобность в базовом регистре, и мы можем исключить предложения LDB и BASE, которые ранее были в строках 13 и 14. Проблема размещения литералов (и организации ссылок на них) также решается намного проще. Мы просто включили предложение LTORG в блок СDАТА для того, чтобы быть уверенными, что литералы будут расположены впереди всех областей данных, занимающих значительный объем памяти. Конечно, использование программных блоков не дает ничего такого, что мы не могли бы сделать простым переупорядочиванием предложений исходной программы. Например, чтение программы упрощается, если определения областей данных располагаются в исходной программе вблизи использующих их команд. Это особенно справедливо, если программа редактируется или анализируется с помощью терминала. В больших подпрограммах эту задачу (без помощи программных блоков) можно решить, размещая области данных в удобных местах. H_COPY _000000_001071 T_000000_1E_172063_4B2021_032060_290000_332006_4B203B_3F2FEE_032055_0F2056_ 010003 T_00001E_09_0F2048_4B2029_3E203F T_000027_1D_B410_B400_B440_75101000_E32038_332FFA_DB2032_A004_332008_57A0 2F_B850 T_000044_09_3B2FEA_13201F_4F0000 T_00006C_01_F1 T_00004D_19_B410_772017_E3201B_332FFA_53AO16_DF2012_B850_3B2FEF_4F0000 T_00006D_04_454F46_05 E_000000 Рис. 2.13. Объектная программа, соответствующая рис.2.11. Однако в этом случае программист должен позаботиться о командах безусловного перехода для обхода таких областей данных. Рассматривая данную ситуацию с машинной точки зрения, желательно располагать фрагменты объектной программы в оперативной памяти в определенном порядке. С другой стороны, человеческая логика подсказывает нам, что предложения исходной программы должны располагаться в другом порядке. Использование программных блоков является одним из способов удовлетворения обоих этих требований с помощью ассемблера, обеспечивающего необходимую реорганизацию. Для того чтобы разместить фрагменты каждого программного блока рядом, совершенно необязательно выполнять физическую реорганизацию сгенерированного объектного кода. Ассемблер может просто записывать объектный код по мере его генерации во время второго просмотра и вставлять соответствующий адрес загрузки в каждую запись тела программы. Эти адреса загрузки будут, конечно, отражать наряду с относительным расположением команд и данных внутри блока также и начальный адрес самого блока. Иллюстрация этому дана на рис. 2.13. Две первые записи тела программы генерируются из строк исходной программы с номерами с 5 до 70. Когда в строке 92 встречается предложение USE, ассемблер записывает текущую запись тела программы (хотя в ней еще есть свободное место). Затем ассемблер подготавливается к открытию новой записи тела программы для нового программного блока. Однако ввиду того, что предложения в строках с 95 по 105 не приводят к генерации объектного кода, не создается и соответствующих им записей тела программы. Следующие две записи тела программы получаются из строк 125 - 180. Программа, загружаемая Исходная программа Объектная программа в память ОтносительСтрока ный адрес 5 0000 Default(1) Default(1) Default(1) 0027 Default(2) Default(2) 70 95 004D CDATA(1) CDATA(2) Default(3) 100 0066 105 CBLKS(1) Default(3) CDATA(1) 125 006C CDATA(3) CDATA(2) 006D Default(2) CDATA(3) 0071 180 185 210 CDATA(2) CBLKS(1) Default(3) 245 253 CDATA(3) 1070 Рис. 2.14. Программные блоки для рис.2.11 после выполнения ассемблирования и загрузки. На этот раз предложения, принадлежащие очередному программному блоку, действительно требуют генерации объектного кода. Пятая запись тела программы содержит один-единственный байт данных, генерируемый из строки 185. Шестая запись тела программы возобновляет непоименованный блок, и оставшаяся часть объектной программы обрабатывается таким же образом. То, что записи тела программы расположены в объектной программе не в порядке возрастания адресов, не имеет значения, Загрузчик просто помещает каждую запись по указанному адресу. По завершению загрузки сгенерированный код непоименованного блока будет занимать относительные адреса с 0000 по 0065; сгенерированный код и зарезервированные области данных блока СDАТА будут занимать адреса с 0066 по 0070, а память, зарезервированная для CBLKS, будет занимать адреса с 0071 по 1070. На рис. 2.14 прослежен процесс ассемблирования и загрузки блоков нашей модельной программы. Обратите внимание на то, что предложения программы, обозначенные СDАТА(1) и CBLKS(l), фактически не присутствуют в объектной программе. Резервирование памяти для этих областей обеспечивается автоматически при загрузке программы с помощью механизма назначения адресов. Для того чтобы убедиться, что вы поняли, как ассемблер обрабатывает программы, состоящие из нескольких блоков, вам следует тщательно изучить сгенерированный код (рис.2.12) и выполнить ассемблирование еще нескольких команд. А чтобы уяснить, как различные части каждого программного блока собираются вместе, вам следует также воспроизвести (вручную) загрузку объектной программы, приведенной на рис. 2.13. 2.3.5. Управляющие секции и связывание программ В этом разделе мы обсудим, как обрабатываются программы, состоящие из нескольких управляющих секций. Управляющая секция - это часть программы, которая после ассемблирования сохраняет свою индивидуальность и может загружаться и перемещаться независимо от других. В отдельные управляющие секции чаще всего выделяются подпрограммы или другие логические подразделы программы. Программист может отдельно ассемблировать и загружать каждую из таких управляющих секций. Достигаемая за счет этого гибкость является основным преимуществом их использования. Примеры этого будут рассмотрены в гл. 3, когда мы будем обсуждать редакторы связей. Поскольку управляющие секции образуют логически связанные части программы, то, следовательно, необходимо предоставить некоторые средства для их связывания (linking) друг с другом. Например, команды одной управляющей секции должны иметь возможность ссылаться на команды или области данных, расположенные в другой секции. Такие ссылки нельзя обрабатывать обычным образом, поскольку управляющие секции загружаются и перемещаются независимо друг от друга и ассемблеру ничего не известно о том, где будут расположены другие управляющие секции во время исполнения программы. Такие ссылки между управляющими секциями называются внешними ссылками (external references). Для каждой внешней ссылки ассемблер генерирует информацию, которая дает возможность загрузчику выполнить требуемое связывание программ. В этом разделе мы опишем, как наш ассемблер обрабатывает внешние ссылки. Реальное выполнение связывания будет детально обсуждаться в гл. 3. На рис.2.15 показано, как наша модельная программа может быть записана в виде нескольких управляющих секций, В данном случае имеются три управляющие секции: одна для главной программы и по одной для каждой из подпрограмм. Предложение START определяет начало ассемблируемой программы и задает имя (COPY) первой управляющей секции. Первая секция расположена вплоть до предложения CSECT в строке 109. Эта директива ассемблера сигнализирует о начале новой управляющей секции с именем RDREC. Аналогично предложение CSECT в строке 193 начинает управляющую секцию с именем WRREC. Для каждой управляющей секции ассемблер заводит отдельный счетчик размещений (начинающийся с 0) точно так же, как он это делает для программных блоков. От программных блоков управляющие секции отличаются тем, что они обрабатываются ассемблером независимо друг от друга. (Необязательно даже, чтобы все управляющие секции программы ассемблировались одновременно.) Имена, определенные в одной управляющей секции, не могут непосредственно использоваться в другой секции. Для того чтобы загрузчик смог их обработать, они должны быть описаны как внешние ссылки. На рис.2.15 показаны две директивы ассемблера для задания таких ссылок: EXTDEF (определение внешних имен) и ЕХТREF (объявление внешних ссылок). Предложение EXTDEF определяет имена, описанные в данной управляющей секции, но доступные для использования в других секциях. Такие имена называются внешними именами (external symbols). Имена управляющих секций (в данном случае COPY, RDREC и WRREC) определять в предложении EXTDEF не требуется, так как они автоматически считаются внешними. Предложение EXTREF объявляет имена, которые используются в данной управляющей секции, но определены где-либо еще. Например, имена BUFFER, BUFEND и LENGTH, определенные в управляющей секции с именем COPY, делаются доступными другим секциям с помощью предложения EXTDEF в строке 6. В третьей управляющей секции (WRREC), согласно предложению EXTREF (строка 207), используются два внешних имени. Порядок перечисления имен в предложениях EXTDEF и EXTREF несуществен. Теперь мы готовы к тому, чтобы посмотреть, как ассемблер обрабатывает внешние ссылки. На рис.2.16 для каждого предложения программы показан сгенерированный объектный код. Строка Исходное предложение 5 COPY START 0 КОПИРОВАНИЕ ФАЙЛА 6 EXTDEF BUFFER,BUFEND,LENGTH 7 EXTREF RDREC,WRREC 10 FIRST STL RETADR СОХРАНЕНИЕ АДРЕСА ВОЗВРАТА 15 CLOOP +JSUB RDREC ВВОД ВХОДНОЙ ЗАПИСИ 20 LDA LENGTH ПРОВЕРКА НА EOF (LENGTH = 0) 25 COMP #0 30 JEQ ENDFIL ВЫХОД, ЕСЛИ НАШЛИ EOF 35 +JSUB WRREC ВЫВОД ВЫХОДНОЙ ЗАПИСИ 40 J CLOOP ЦИКЛ 45 ENDFIL LDA =C"EOF" ЗАНЕСЕНИЕ МАРКЕРА КОНЦА ФАЙЛА 50 STA BUFFER 55 LDA #3 УСТАНОВИТЬ LENGTH = 3 60 STA LENGTH 65 +JSUB WRREC ЗАПИСЬ EOF 70 J @RETADR ВОЗВРАТ ИЗ ПРОГРАММЫ 95 RETADR RESW 1 100 LENGTH RESW 1 ДЛИНА ЗАПИСИ 103 LTORG 105 BUFFER RESB 4096 ДЛИНА БУФЕРА – 4096 БАЙТ 106 BUFEND EQU * 107 MAXLEN EQU BUFEND-BUFFER МАКСИМАЛЬНАЯ ДЛИНА ЗАПИСИ 109 RDREC CSECT 110 * 115 * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР 120 * 122 EXTREF BUFFER,LENGTH,BUFEND 125 CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 130 CLEAR A ОБНУЛЕНИЕ РЕГИСТРА А 132 CLEAR S ОБНУЛЕНИЕ РЕГИСТРА S 133 LDT MAXLEN 135 RLOOP TD INPUT ПРОВЕРКА УСТРОЙСТВА ВВОДА 140 JEQ RLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 145 RD INPUT ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 150 COMP A,S ПРОВЕРКА НА КОНЕЦ ЗАПИСИ (Х"00") 155 JEQ EXIT ВЫХОД ИЗ ЦИКЛА ПО КОНЦУ ЗАПИСИ 160 +STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР 165 TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 170 JLT RLOOP ДЛИНЫ 175 EXIT +STX LENGTH ЗАПОМИНАНИЕ ДЛИНЫ ЗАПИСИ 180 RSUB ВОЗВРАТ ИЗ ПОДПРОГРАММЫ 185 INPUT BYTE X"F1" КОД УСТРОЙСТВА ВВОДА 190 MAXLEN WORD BUFEND-BUFFER 193 WRREC CSECT 195 * 200 * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА 205 * 207 EXTREF LENGTH,BUFFER 210 CLEAR X ОБНУЛЕНИЕ СЧЕТЧИКА ЦИКЛА 212 +LDT LENGTH 215 WLOOP TD =X"05" ПРОВЕРКА УСТРОЙСТВА ВЫВОДА 220 JEQ WLOOP ЦИКЛ ДО ПОЛУЧЕНИЯ ГОТОВНОСТИ 225 +LDCH BUFFER,X ЧТЕНИЕ СИМВОЛА ИЗ БУФЕРА 230 WD =X"05" ВЫВОД СИМВОЛА 235 TIXR T ЦИКЛ, ПОКА НЕ БУДУТ ВЫВЕДЕНЫ 240 JLT WLOOP ВСЕ СИМВОЛЫ 245 RSUB ВОЗВРАТ ИЗ ПОДРОГРАММЫ 255 END FIRST Рис. 2.15.Иллюстрация управляющих секций и связывания программы. Строка 5 0000 6 7 10 0000 15 0003 20 0007 25 000A 30 000D 35 0010 40 0014 45 0017 Адрес Исходное предложение Объектный код COPY START 0 EXTDEF BUFFER,BUFEND,LENGTH EXTREF RDREC,WRREC FIRST STL RETADR 172027 CLOOP +JSUB RDREC 4B100000 LDA LENGTH 032023 COMP #0 290000 JEQ ENDFIL 332007 +JSUB WRREC 4B100000 J CLOOP 3F2FEC ENDFIL LDA =С"EOF" 032016 50 55 60 65 70 95 100 103 105 106 107 109 110 115 120 122 125 130 132 133 135 140 145 150 155 160 165 170 175 180 185 190 193 195 200 205 207 210 212 215 220 225 230 235 240 245 255 001A STA BUFFER 0F2016 001D LDA #3 010003 0020 STA LENGTH 0F200A 0023 +JSUB WRREC 4B100000 0027 J @RETADR 3E2000 002A RETADR RESW 1 003D LENGTH RESW 1 LTORG 0030 * =C"EOF" 454F46 0033 BUFFER RESB 4096 1033 BUFEND EQU * 1000 MAXLEN EQU BUFEND-BUFFER 0000 RDREC CSECT * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * EXTREF BUFFER,LENGTH,BUFEND 0000 CLEAR X B410 0002 CLEAR A B400 0004 CLEAR S B440 0006 LDT MAXLEN 77201F 0009 RLOOP TD INPUT E3201B 000C JEQ RLOOP 332FFA 000F RD INPUT DB2015 0012 COMPR A,S A004 0014 JEQ EXIT 332009 0017 +STCH BUFFER,X 57900000 001B TIXR T B850 001D JLT RLOOP 3B2FE9 0020 EXIT +STX LENGTH 13100000 0024 RSUB 4F0000 0027 INPUT BYTE X"F1" F1 0028 MAXLEN WORD BUFEND-BUFFER 000000 0000 WRREC CSECT * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * EXTREF LENGTH,BUFFER 0000 CLEAR X B410 0002 +LDT LENGTH 77100000 0006 WLOOP TD =X"05" E32012 0009 JEQ WLOOP 332FFA 000C +LDCH BUFFER,X 53900000 0010 WD =X"05" DF2008 0013 TIXR T B850 0015 JLT WLOOP 3B2FEE 0018 RSUB 4F0000 END FIRST 001B * =X"05" 05 Рис.2.16. Объектный код для программы на рис.2.15. Для начала рассмотрим команду 15 0003 CLOОР + JSUB RDREC 4В100000 Ее операнд (RDREC) объявлен в предложении EFTREF, и, следовательно, это внешняя ссылка. Ассемблеру ничего неизвестно о том, куда будет загружена управляющая секция, содержащая RDREC, и поэтому он не может обработать адресную часть данной команды. Вместо этого он устанавливает адрес равным 0 и передает загрузчику информацию, позволяющую ему занести во время загрузки нужный адрес. Поскольку неизвестно, будет ли адрес RDREC взаимосвязан с чем-либо в данной управляющей секции, относительную адресацию использовать нельзя. Следовательно, для того чтобы иметь достаточно места для занесения фактического адреса, необходимо использовать расширенный командный формат. Это справедливо для любой команды, операнд которой ссылается на внешнее имя. Аналогично в команде 160 0017 +SТCН BUFFER,Х 57 900 000 имеется внешняя ссылка на метку BUFFER. Эта команда ассемблируется с использованием расширенного командного формата и нулевым адресом. Разряд х установлен в 1, что является признаком индексной адресации. Предложение 190 0028 MAXLEN WORD BUFEND - BUFFER 000000 лишь незначительно отличается от предыдущих. Здесь значение генерируемой 1-словной константы определяется с помощью выражения, в котором используются две внешние ссылки: BUFEND и BUFFER. Как и ранее, ассемблер заносит для этой константы нулевое значение. Загрузчик во время загрузки прибавит к этому значению адрес метки BUFEND, а затем вычтет из него адрес BUFFER, что и даст в итоге требуемое значение. Укажем на различие в обработке выражения в строке 190 и похожего выражения в строке 107. Имена BUFEND и BUFFER определены в той же управляющей секции, что и предложение EQU в строке 107. Поэтому значение этого выражения может быть вычислено непосредственно ассемблером. Для строки 190 это сделать нельзя, так как BUFEND и BUFFER определены в другой управляющей секции и, следовательно, во время ассемблирования их значения неизвестны. Как видно из предыдущих рассуждений, ассемблер должен запоминать (в SYMTAB), в какой управляющей секции было определено то или иное имя. Любая попытка использовать имя из другой управляющей секции должна отмечаться как ошибка, если только это имя не объявлено (с помощью EXTREF) в качестве внешнего. Ассемблер должен разрешать использовать одинаковые имена в разных управляющих секциях. Например, противоречивые определения MAXLEN в строках 107 и 190 не должны вызвать никаких проблем. Ссылки на MAXLEN в управляющей секции COPY будут использовать определение в строке 107, тогда как ссылки в RDREC - определение в строке 190. До сих пор мы видели, что ассемблер оставляет место в объектном коде для занесения значений внешних имен. Кроме того, он должен включить в объектную программу информацию, позволяющую загрузчику занести необходимые значения туда, куда требуется. Для этого нам необходимо видоизменить структуру записи-модификатора и ввести два новых типа записей. Как и раньше, конкретный формат этих записей может быть произвольным, однако в любом случае эта информация должна в той или иной форме передаваться загрузчику. Два новых типа записей - это запись-определение и запись-ссылка. Записьопределение содержит информацию о внешних именах, определенных в данной управляющей секции, т. е. именах, объявленных в EXTDEF. В записи-ссылке содержится список имен, используемых в данной управляющей секции в качестве внешних ссылок, т. е. имен, объявленных в EXTREF. Ниже приводятся форматы этих записей. Запись-определение: Столбец 1 D. Столбцы Столбцы Столбцы Запись-ссылка: Столбец Столбцы Идентификатор внешнего имени, определенный в данной управляющей секции 8–13 Относительный адрес имени (шестнадцатеричный) . 14–73 Информация, аналогичная столбцам 2-13 для других внешних имен. 2–7 1 2-7 R. Идентификатор внешнего имени, на который есть ссылка в данной управляющей секции. Столбцы 8-73 Информация, аналогичная столбцам 2-7 для других внешних имен. Другая информация, необходимая для связывания программ, добавляется в записьмодификатор. Ее новый формат показан ниже. Запись-модификатор: Столбец 1 М. Столбцы 2-7 Начальный адрес модифицируемого адресного поля относительно начала управляющей секции (шестнадцатеричный). Столбцы 8-9 Длина модифицируемого адресного поля в полубайтах (шестнадцатеричная). Столбец 10 Признак модификации (+ или -). Столбцы 11-16 Внешнее имя, значение которого надо добавить к заданному полю или вычесть из него. Первые три поля этой записи остались без изменений. Два новых поля определяют тип требуемой модификации: сложение или вычитание значения внешнего имени. Имена, используемые для модификации, могут быть определены как в данной управляющей секции, так и в другой. На рис. 2.17 показана объектная программа, соответствующая исходному тексту рис.2.16. Обратите внимание, что для каждой управляющей секции имеется отдельный набор записей объектной программы (от записи-заголовка до записи-конца). Записи каждой управляющей секции в точности такие же, как если бы эти секции ассемблировались раздельно. Записи-определения и записи-ссылки каждой управляющей секции содержат имена, объявленные в предложениях EXTDEF и EXTREF. Запись-определение содержит, кроме того, относительный адрес каждого внешнего имени внутри данной управляющей секции. Для имен, объявленных в EXTREF, информации об адресах нет. Эти имена просто перечисляются в записи -ссылке. Давайте теперь изучим процесс связывания внешних ссылок, начиная с ранее рассмотренных исходных предложений. Поле адреса команды JSUB в строке 15 начинается с относительного адреса 0004. В объектной программе его начальное значение равно 0. Запись-модификатор М00000405 + RDREC управляющей секции COPY определяет, что адрес метки RDREC должен быть прибавлен к этому полю, что и даст требуемый машинный адрес. Две другие записи-модификаторы в секции COPY выполняют сходные функции для команд в строках 35 и 65. Аналогично первая запись-модификатор в управляющей секции RDREC указывает на занесение внешней ссылки для строки 160. Обработка 1-словной константы, сгенерированной по строке 190, мало чем отличается от ранее описанного процесса. Значением этой константы является выражение BUFEND - BUFFER, где обе метки определены в другой управляющей секции. Ассемблер генерирует эту константу (расположенную по относительному адресу 0028 в управляющей секции RDREC) с нулевым начальным значением. Две последние записи- модификаторы в RDREC требуют, чтобы адрес BUFEND был добавлен к указанному полю, а адрес BUFFER был вычтен. Эти вычисления выполняются во время загрузки, и в результате получается требуемая константа. H_COPY _000000_001033 D_BUFFER_000033_BUFEND_001033_LENGTH_00002D R_RDREC _WRREC T_000000_1D_172027_4B100000_032023_290000_332007_4B100000_3F2FEC_032016_0F2 016 T_00001D_0D_010003_0F200A_4B100000_3E2000 T_000030_03_454F46 M_000004_05_+RDREC M_000011_05_+WRREC M_000024_05_+WRREC E_000000 H_RDREC _000000_00002D R_BUFFER_LENGTH_BUFEND T_000000_1D_B410_B400_B440_77201F_E3201B_332FFA_DB2015_A004_332009_579000 00_B850 T_00001D_0E_3B2FE9_13100000_4F0000_F1_000000 M_000018_05_+BUFFER M_000021_05_+LENGTH M_000028_06_+BUFEND M_000028_06_-BUFFER E H_WRREC _000000_00001C R_LENGTH_BUFFER T_000000_1C_B410_77100000_E32012_332FFA_53900000_DF2008_B850_3B2FEE_4F0000 _05 M_000003_05_+LENGTH M_00000D_05_+BUFFER E Рис. 2.17. Объектная программа, соответствующая рис.2.15. В гл. 3 мы детально рассмотрим, как загрузчик выполняет заданные модификации; сейчас же важно, чтобы вы досконально разобрались с концепциями процесса связывания. Вам следует тщательно изучить другие записи-модификаторы на рис. 2.17 и самостоятельно реконструировать их генерацию по предложениям исходной программы. Обратите внимание, что измененная запись-модификатор может по-прежнему использоваться для перемещения программ. В случае перемещения модификация заключается в добавлении начального адреса управляющей секции к определенным полям в объектной программе. Значением имени, используемого в качестве названия управляющей секции, является требуемый адрес. Поскольку имя управляющей секции автоматически является внешним именем, то оно может быть использовано в записимодификаторе. Так, например, записи-модификаторы рис. 2.8 М00000705 М00001405 М00002705 будут заменены на записи-модификаторы вида М00000705 + COPY М00001405 + COPY М00002705 + COPY Таким образом, один и тот же механизм может быть использован как для перемещения программ, так и для их связывания. В следующей главе будет рассмотрено еще несколько примеров. Наличие нескольких управляющих секций, каждая из которых может перемещаться независимо от других, создает дополнительные трудности при обработке выражений. Ранее мы требовали, чтобы все относительные термы выражения группировались в пары (для абсолютного выражения) или чтобы все относительные термы, кроме одного, группировались в пары (для относительного выражения). Теперь мы вынуждены усилить эти ограничения и потребовать, чтобы парные термы принадлежали одной управляющей секции. Причина этого проста - если оба терма определяют относительные метки в одной и той же управляющей секции, то их разность является абсолютной величиной (независимо от того, где будет расположена управляющая секция). С другой стороны, если метки относятся к различным управляющим секциям, то значение их разности заранее непредсказуемо (и потому, вероятно, бесполезно). Например, выражение BUFEND - BUFFER определяет длину буфера в байтах. С другой стороны, выражение RDREC - COPY определяет разность между адресами загрузки двух управляющих секций. Эта величина зависит от способа размещения программы в оперативной памяти, и маловероятно, чтобы она была полезна в прикладных программах. Когда в выражении используются внешние ссылки, ассемблер в общем случае не может определить, корректно оно или нет. Группировка относительных термов для проверки корректности не может быть сделана, если не известно, какие термы принадлежат одной секции, а во время ассемблирования эта информация отсутствует. В этом случае ассемблер вычисляет все известные ему термы и комбинирует их так, чтобы получить начальное значение выражения. Кроме того, он генерирует записи-модификаторы, позволяющие загрузчику закончить вычисление. Загрузчик может проконтролировать корректность выражения. Все эти вопросы мы обсудим в гл. 3 при изучении связывающего загрузчика. 2.4. Варианты построения ассемблеров В этом разделе мы рассмотрим несколько важных вариантов общей схемы ассемблирования. Во многих случаях программист не ощущает прямого воздействия схемы ассемблирования на возможности, предоставляемые языком ассемблера, хотя иногда действительно имеются побочные эффекты, которые следует принимать во внимание. В разд. 2.4.1 мы опишем оверлейную структуру, которая обычно используется для двухпросмотровых ассемблеров. В разд. 2.4.2 и 2.4.3 обсуждаются две альтернативные (по отношению к стандартной двухпросмотровой) схемы ассемблирования. В разд. 2.4.2 описываются структура и алгоритм однопросмотрового ассемблера. Такие ассемблеры используются там, где желательно или даже необходимо обойтись без второго просмотра исходной программы. В разд. 2.4.3 дается представление о многопросмотровых ассемблерах. В этих ассемблерах получает свое дальнейшее развитие двухпросмотровая схема ассемблирования, что позволяет обрабатывать ссылки вперед в предложениях определения имен. 2.4.1. Двухпросмотровый ассемблер с оверлейной структурой Как мы видели, в большинстве ассемблеров процесс обработки исходной программы делится на два просмотра. Внутренние таблицы и подпрограммы, которые используются только во время первого просмотра, становятся ненужными после его завершения. Многие подпрограммы и таблицы используются только для одного просмотра и никогда не требуются для другого. В то же время некоторые таблицы (например, SYMTAB) и подпрограммы (например, поиск в SYMTAB) используются в обоих просмотрах. На рис. 2.18 представлена общая структура двухпросмотрового ассемблера. Этот ассемблер состоит из трех сегментов, образующих дерево. Корневой сегмент содержит простую управляющую программу, функцией которой является поочередный вызов двух других сегментов (Просмотр 1 и Просмотр 2). Корневой сегмент содержит таблицы и подпрограммы, необходимые для обоих просмотров. Поскольку сегменты Просмотр 1 и Просмотр 2 никогда не требуются одновременно, то во время работы ассемблера они могут занимать одно и то же пространство оперативной памяти. Вначале в память загружается корневой сегмент и сегмент Корневой сегмент Управляющая программа Общие таблицы и подпрограммы Подпрограммы Сегмент и таблицы Просмотр 1 первого просмотра Подпрограммы и таблицы Сегмент второго Просмотр 2 просмотра Рис. 2.18. Структура двухпросмотрового ассемблера. Просмотр 1, и ассемблер выполняет первый просмотр. После его завершения на место сегмента Просмотр 1 загружается Максимальная Объем память, тре- памяти буемая при занимаемый оверлейной всеми под загрузке программами и таблицами ассемблера Управляющая программа Общие таблицы и подпрограммы Подпрограммы первого просмотра Таблицы первого просмотра Управляющая программа Резидентная часть Общие таблицы и подпрограммы Подпрограммы второго просмотра Загружаются на одно и то же место в оперативной Таблицы памяти второго просмотра Рис. 2.19. Двухпросмотровый ассемблер с оверлейной структурой. сегмент Просмотр 2. После этого ассемблер выполняет второй просмотр ассемблируемой программы (или промежуточного файла) и завершает свою работу. Этот процесс показан на рис. 2.19. Обратите внимание, что при использовании оверлейной структуры ассемблеру требуется значительно меньше оперативной памяти, чем если бы одновременно загружались сегменты обоих просмотров. Во многих двухпросмотровых ассемблерах этот прием используется для сокращения требуемого объема оперативной памяти. Программа, построенная подобным образом, называется оверлейной программой или программой с перекрытием, так как во время ее исполнения некоторые из ее сегментов перекрывают другие. В гл. 3 мы более подробно обсудим оверлейные программы и рассмотрим, как они обрабатываются загрузчиком и сервисными процедурами операционной системы. 2.4.2. Однопросмотровые ассемблеры В этом разделе мы рассмотрим структуру и проектирование однопросмотровых ассемблеров. Как мы отмечали в разд. 2.1, основная трудность, возникающая при попытке ассемблировать программу за один просмотр, связана со ссылками вперед. Часто в качестве операндов команд используются имена, которые еще не были определены в исходной программе. Поэтому ассемблер не знает, какие адреса занести в транслируемую программу. Довольно легко исключить ссылки вперед на данные; достаточно потребовать, чтобы все области данных определялись в исходной программе раньше, чем появляются команды, которые на них ссылаются. Это не слишком жесткое ограничение. Программист просто размещает все области данных в начало программы, а не в конце. К несчастью, невозможно столь же легко исключить ссылки вперед на метки команд. Логика программы часто требует передачи управления вперед. (Например, выход из цикла после проверки некоторого условия.) Требование исключить все такие передачи управления оказалось бы гораздо более жестким и неудобным. Поэтому ассемблер должен предпринимать специальные меры для обработки ссылок вперед. Однако для того, чтобы облегчить задачу, многие однопросмотровые ассемблеры действительно запрещают (или по крайней мере не рекомендуют) ссылки вперед на данные. Существует два основных типа одиопросмотровых ассемблеров. Ассемблеры первого типа записывают объектный код не посредственно в оперативную память для немедленного исполнения; ассемблеры второго тина создают объектную программу, которая будет выполняться позднее. Обсуждение обоих типов мы проиллюстрируем с помощью программы на рис.2.20. Это тот же самый пример, что и на рис.2.2, по все определения данных расположены перед командами, в которых они используются. Сгенерированный объектный код показан на рис.2.20 только для справки. Строка 0 1000 1 1000 2 1003 3 1006 4 1009 5 100C 6 100F 9 10 200F 15 2012 20 2015 25 2018 30 201B 35 201E 40 2021 45 2024 50 2027 55 202A 60 202D 65 2030 70 2033 75 2036 110 115 120 121 2039 122 203A 124 125 203D 130 2040 135 2043 140 2046 Адрес Исходное предложение Объектный код COPY START 1000 EOF BYTE C"EOF" 454F46 THREE WORD 3 000003 ZERO WORD 0 000000 RETARD RESW 1 LENGTH RESW 1 BUFFER RESB 4096 * FIRST STL RETADR 141009 CLOOP JSUB RDREC 48203D LDA LENGTH 00100C COMP ZERO 281006 JEQ ENDFIL 302024 JSUB WRREC 482062 J CLOOP 3С2012 ENDFIL LDA EOF 001000 STA BUFFER 0С100F LDA THREE 001003 STA LENGTH 0С100C JSUB WRREC 482062 LDL RETADR 081009 RSUB 4С0000 * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * INPUT BUTE X"F1" F1 MAXLEN WORD 4096 001000 * RDREC LDX ZERO 041006 LDA ZERO 001006 RLOOP TD INPUT E02039 JEQ RLOOP 302043 145 150 155 160 165 170 175 180 195 200 205 206 210 215 220 225 230 235 240 245 255 2049 RD INPUT D82039 204C COMP ZERO 281006 204F JEQ EXIT 30205B 2052 STCH BUFFER,X 54900F 2055 TIX MAXLEN 2C203A 2058 JLT RLOOP 382043 205B EXIT STX LENGTH 10100C 205E RSUB 4C0000 * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * 2061 OUTPUT BYTE X"05" 05 2062 WRREC LDX ZERO 041006 2065 WLOOP TD OUTPUT E02061 2068 JEQ WLOOP 302065 206B LDCH BUFFER,X 50900F 206E WD OUTPUT DC2061 2071 TIX LENGTH 2C100C 2074 JLT WLOOP 382065 2077 RSUB 4C0000 END FIRST Рис.2.20. Модельная программа для однопросмотрового ассемблера. Мы обсудим, как однопросмотровый ассемблер каждого типа мог бы в действительности сгенерировать требуемую объектную программу. Вначале мы рассмотрим однопросмотровые ассемблеры, генерирующие объектный код непосредственно в оперативную память для немедленного исполнения. В этом случае не создается объектная программа и не требуется загрузчик. Такие ассемблеры типа загрузка - выполнение полезны в системах, ориентированных на разработку и тестирование программ. Типичным примером такой системы может служить университетская студенческая система. В такой системе значительную долю от общего объема работы составляет трансляция программ. Поскольку практически при каждом новом запуске программ происходит их переассемблирование, эффективность процесса ассемблирования приобретает важное значение. Ассемблеры типа загрузка - выполнение позволяют избежать накладных расходов, связанных с записью объектной программы на внешнюю память и ее последующим считыванием. Такой процесс может быть организован как однопросмотровымтак и двухпросмотровым ассемблером. Однако однопросмотровый ассемблер позволяет избежать также и накладных расходов, связанных с дополнительным просмотром исходной программы. Поскольку генерируемая объектная программа вместо того, чтобы записываться на внешнюю память, располагается в оперативной памяти, то обработка ссылок вперед оказывается менее сложной. Ассемблер просто генерирует команды объектного кода по мере просмотра исходной программы. Если операндом команды является еще неопределенное имя, то при ассемблировании команды обработка ее адресной части пропускается. Имя, использованное в качестве операнда, заносится в таблицу имен (если оно не было занесено ранее). Строка таблицы помечается признаком, указывающим на то, что данное имя еще не определено. Адрес операндного поля команды, ссылающейся на неопределенное имя, добавляется в список ссылок вперед, связанный с соответствующим элементом таблицы имен. Если встречается определение имени, то просматривается его список ссылок вперед (если он есть) и требуемый адрес заносится в каждую предварительно сгенерированную команду. Рассмотрим пример, который поможет прояснить этот процесс. На рис. 2.21а показано, как будут выглядеть объектный код и элементы таблицы имен после просмотра строки 40 программы на рис.2.20. Первая ссылка вперед была в строке 15. Поскольку операнд (RDREC) еще не был определен, то команда ассемблировалась без назначения адреса для этого операнда (на рисунке обозначено как ----). Затем имя RDREC было занесено в SYMTAB как неопределенное имя (обозначено *), а адрес операндного поля команды (2013) был занесен в список, связанный с RDREC. Аналогично обрабатывались команды в строках 30 и 35. Адрес памяти 1000 454F4600 1010 xxxxxxxx Содержимое Имя 00030000 00xxxxxx xxxxxxxx LENGTH 100C xxxxxxxx xxxxxxxx xxxxxxxx RDREC * 2013 0 THREE 2000 xxxxxxxx 2010 100948-2020 --3C2012 Значение 1003 xxxxxxxx xxxxxxxx xxxxxx14 ZERO 1006 --00100C 28100630 -------48-WRREC * EOF 201F 0 1000 ENDFIL * RETARD 1009 BUFFER 100F CLOOP 2012 201C 0 FIRST 200F a Адрес памяти Содержимое Имя Значение 1000 454F4600 00030000 00xxxxxx xxxxxxxx LENGTH 1010 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx RDREC 203D THREE 2031 2000 2010 2020 0 2030 2040 2050 100C 1003 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxx14 10094820 3D00100C 28100630 202448---3C2012 0010000C 100F0010 030C100C ZERO 1006 WRREC * 48-----08 10094C00 00F10010 00041006 001006E0 20393020 43D82039 28100630 EOF ------5490 0F ENDFIL 2024 RETARD 1009 BUFFER 100F CLOOP 2012 1000 201F FIRST 200F MAXLEN 203A INPUT 2039 EXIT * RLOOP 2043 2050 0 б Рис. 2.21. Объектные коды, записанные в оперативную память, и элементы таблицы имен после просмотра строки 40 (а) и строки 160 (б) программы на рис.2.20. Рассмотрим теперь рис. 2.21б , соответствующий ситуации после просмотра строки 160. К этому моменту была разрешена часть ссылок вперед, в то время как другие были добавлены. Когда было определено имя ENDFIL (строка 45), ассемблер занес его значение в SYMTAB. Затем он занес это значение в операндное поле команды (по адресу 201С), как это предписано списком ссылок вперед. С этого момента все ссылки на ENDFIL не будут являться ссылками вперед и не будут заноситься в список. Аналогично определение RDREC (строка 125) позволило заполнить операндное поле по адресу 2013. Тем временем были добавлены две новые ссылки вперед: WRREC (строка 65) и EXIT (строка 155). Вам следует продолжить этот процесс до конца программы и самостоятельно убедиться в том, что все ссылки вперед будут занесены правильно. Когда будет обнаружен конец программы, все элементы SYMTAB с признаком * будут указывать на неопределенные имена, и, следовательно, ассемблер должен отметить их как ошибки. По достижению конца программы ассемблирование завершается. Если не было ошибок, то ассемблер ищет в SYMTAB значение имени, указанное в предложении END (в данном случае FIRST), и передает управление на данный адрес для исполнения ассемблированной программы. В нашем примере мы использовали абсолютную программу, так как для ассемблера типа загрузка-выполнение фактический адрес загрузки должен быть известен во время ассемблирования. Конечно, необязательно, чтобы адрес загрузки определялся программистом; он может назначаться и системой. Это не влияет на процесс ассемблирования, так как в любом случае в счетчик размещений будет занесен фактический адрес начала программы. Одпопросмотровые ассемблеры, результатом работы которых является объектная программа, необходимы в системах, где отсутствуют рабочие внешние запоминающие устройства для хранения промежуточного файла между двумя просмотрами. Такие ассемблеры также могут быть полезны, когда внешняя память медленна или неудобна для использования по каким-либо другим причинам. Работа однопросмотровых ассемблеров, производящих объектные программы, несколько отличается от описанной ранее. Как и прежде, ссылки вперед заносятся в список. Однако теперь, когда будет встречено определение имени, команды, ссылающиеся на это имя, могут уже отсутствовать в оперативной памяти и, следовательно, будут недоступны для модификации. В общем случае они будут уже записаны на внешнее устройство как часть тела объектной программы. Поэтому ассемблер должен сгенерировать другую запись тела программы с соответствующим адресом команды, а загрузчик занесет этот адрес в команду во время загрузки программы. Иллюстрация этого процесса дана на рис. 2.22. Вторая запись тела программы содержит код, сгенерированный для строк 10 - 40 исходной программы, изображенной на рис. (см. распечатку). Адреса операндов команд для строк 15, 30 и 35 сгенерированы с нулевым значением. Когда в строке 45 встречается определение имени ENDFIL, ассемблер генерирует третью запись тела программы. Эта запись определяет, что значение 2024 H_COPY _001000_00107A T_001000_09_454F46_000003_000000 T_00200F_15_141009_480000_00100C_281006_300000_480000_3C2012 T_00201C_02_2024 T_002024_19_001000_0C100F_001003_0C100C_480000_081009_4C0000_F1_001000 T_002013_02_203D T_00203D_1E_041006_001006_E02039_302043_D82039_281006_300000_54900F_2C203A_ 382043 T_002050_02_205B T_00205B_07_10100C_4C0000_05 T_00201F_02_2062 T_002031_02_2062 T_002062_18_041006_E02061_302065_50900F_DC2061_2C100C_382065_4C0000 E_00200F Рис. 2.22. Объектная программа, полученная из исходной программы на рис.2.20 с помощью однопросмотрового ассемблера. (адрес ENDFIL) должно быть занесено по адресу 201С (адресное поле операнда команды JEQ в строке 30). Таким образом, во время загрузки программы величина 2024 заменит ранее занесенное значение 0000. Точно так же обрабатываются и другие ссылки вперед нашей программы. В результате для разрешения ссылок вперед, которые не могут быть обработаны ассемблером, используются средства загрузчика. Конечно, при передаче объектной программы загрузчику должен быть сохранен первоначальный порядок ее записей. В этом разделе мы рассмотрели только простые однопросмотровые ассемблеры для обработки абсолютных программ (т.е. программы в абсолютных адресах). Мы предполагали, что в качестве операндов команд используются одиночные имена, а ассемблированные команды содержат фактические (не относительные) адреса операндов. Более развитые средства ассемблера, такие как литералы, были запрещены. Вам предлагается подумать о способах снятия некоторых из этих ограничений (некоторые предложения можно найти в упражнениях к данному разделу). 2.4.3. Многопросмотровые ассемблеры При обсуждении директивы ассемблера EQU мы требовали, чтобы все имена в правой части (т. е. в выражении, задающем значение нового имени) были предварительно определены в исходной программе. Аналогичные ограничения накладывались и на директиву ассемблера ORG. В действительности такие ограничения обычно накладываются на все директивы ассемблера, которые (прямо или косвенно) определяют имена. Причина этого кроется в самом процессе определения имен, который используется в двухпросмотровом ассемблере. Рассмотрим следующую последовательность: ALPHA EQU BETA BETA EQU DELTA DELTA EQU 1 Когда во время первого просмотра встретится метка BETA, мы не сможем назначить ей адрес, так как DELTA еще не определена. Поэтому мы не сможем вычислить значение метки ALPHA во время второго просмотра. Это означает, что любой ассемблер, выполняющий только два последовательных просмотра исходной программы, не может обрабатывать такие последовательности определений. Ограничения, связанные с запрещением ссылок вперед в определениях имен, обычно не вызывают у программиста серьезных неудобств. В действительности подобные ссылки вперед скорее порождают сложности у человека, читающего программу, чем у ассемблера. Тем не менее некоторые ассемблеры проектируются так, чтобы исключить необходимость таких ограничений. Общее решение заключается в использовании многопросмотрового ассемблера, который может сделать столько просмотров, сколько необходимо для выполнения процесса определения имен. Совершенно необязательно, чтобы такой ассемблер просматривал всю программу целиком более двух раз. Вместо этого фрагменты программы, содержащие ссылки вперед, запоминаются во время первого просмотра. Дополнительный просмотр таких запомненных определений делается по мере продвижения процесса ассемблирования. По завершению этого процесса выполняется обычный второй просмотр. Существует несколько способов решения обрисованной выше задачи. Метод, который мы опишем, заключается в запоминании в таблице имен тех определений, которые содержат ссылки вперед. Для того чтобы облегчить вычисление значений 1 2 3 4 5 HALFSZ MAXLEN PREVBT * * * BUFFER BUFEND EQU MAXLEN/2 EQU BUFEND-BUFFER EQU BUFFER-1 RESB 4096 EQU * a HALFSZ &1 MAXLEN/2 0 MAXLEN * HALFSZ * MAXLEN 0 b BUFEND 0 HALFSZ &1 MAXLEN/2 0 MAXLEN &2 BUFEND-BUFFER BUFFER * MAXLEN 0 BUFEND * MAXLEN 0 HALFSZ &1 MAXLEN/2 0 PREVBT &1 BUFFER-1 MAXLEN &2 BUFEND-BUFFER BUFFER 0 * HALFSZ 0 в г 0 HALFSZ MAXLEN 0 PREVBT BUFEND * MAXLEN HALFSZ &1 MAXLEN/2 0 PREVBT 1033 MAXLEN &1 BUFEND-BUFFER BUFFER 1034 0 BUFEND 2034 0 HALFSZ 800 0 PREVBT 1033 0 MAXLEN 1000 0 BUFFER 1034 0 0 0 HALFSZ 0 д е Рис. 2.23. Пример работы многопросмотрового ассемблера. имен, элементы данной таблицы содержат признак, который указывает на то, что данное имя зависит от значений других имен. На рис. 2.23а показана последовательность предложений, определяющих имена, в которых используются ссылки вперед; остальная часть исходной программы для нашего обсуждения интереса не представляет ч потому опущена Последующие части рис. 2.23 показывают, как могла бы выглядеть информация, занесенная в таблицу имен, после обработки каждого изприведенных исходных предложений. На рис. 2.23б приведены элементы таблицы имен, получившиеся после первого просмотра предложения HALFSZ EQU MAXLEN/2 Имя MAXLEN еще не определено, поэтому невозможно вычислить значение HALFSZ. Вместо значения HALFSZ в таблице имен запоминается определяющее его выражение Элемент &1показывает, что в определяющем выражении не определено одно имя. Конечно, в реальной реализации это определение может храниться где-нибудь в другом месте. SYMTAB может просто содержать указатель на определяющее выражение. Имя MAXLEN также занесено в таблицу имен и снабжено признаком *, указывающим на то, что оно не определено. С этим именем связан список имен, значения которых зависят отMAXLEN; в данном случае HALFSZ. Обратите внимание на схожесть со способом обработки ссылок вперед в однопросмотровом ассемблере. Аналогично обрабатывается определение MAXLEN (рис. 223в). В данном случае в состав определения входят два неопределенных имени: BUFEND и BUFFER. Оба этих имени заносятся в SYMTAB вместе со списком, указывающим на зависимость MAXLEN от этих имен. Точно так же при обработке определения PREVBT потребуется занесение этого имени в список имен, зависящих от BUFFER (рис. 2.23г). До сих пор мы просто запоминали определения имен для последующей обработки. Определение BUFFER в строке 4позволяет нам начать вычисление некоторых из них. Предположим, что, когда мы читаем строку 4, счетчик размещений содержит шестнадцатеричное значение 1034. Этот адрес запоминается в качестве значения BUFFER. Затем ассемблер просматривает список имен, зависящих от значения BUFFER. Элемент таблицы имен для первого имени данного списка (MAXLEN) показывает, что он зависит от двух имен, которые к данному моменту не определены. Поэтому сразу вычислить значение MAXLEN нельзя. Вместо этого &2 заменяется на &1для того, чтобы показать, что теперь только одно имя в определении (BUFEND) осталось неопределенным. Другое имя списка (PREVBT) можно вычислить, так как оно зависит только от BUFFER. Выражение, определяющее значение PREVBT, вычисляется и запоминается в SYMTAB. Результат показан на рис. 2.23д. Дальнейшая обработка осуществляется аналогично. Когда в строке 5 определяется BUFEND, его значение заносится в таблицу имен. Список, связанный с BUFEND, предписывает ассемблеру вычислить MAXLEN, а занесение значения MAXLEN вызывает, в свою очередь, вычисление имен из связанного с ним списка (HALFSZ). Как показано на рис. 2.23е, на этом процесс определения имен заканчивается. Если какие-либо имена остаются неопределенными, то они отмечаются как ошибочные. Процедура, которую мы только что описали, применяется к именам, определяемым с помощью директив ассемблера типа EQU. Вам следует подумать, как модифицировать этот метод для того, чтобы он позволял обрабатывать ссылки вперед и в предложениях ORG. 2.5. Примеры реализации В предыдущих разделах мы обсудили многие самые общие средства ассемблеров. Однако разнообразие машин и языков ассемблера очень велико. В большинстве ассемблеров имеются нестандартные средства, обусловленные структурой машины или особенностями языка. В этом разделе будут рассмотрены примеры трех ассемблеров реальных машин. Очевидно, что в рамках одного раздела невозможно дать полного описания какого-либо из этих ассемблеров. Вместо этого мы сфокусируем внимание на наиболее интересных или необычных средствах каждого из них и выделим те аспекты конкретного проекта, где базовые алгоритмы и структуры таблиц отличаются от описанных ранее. Мы обсудим примеры ассемблеров для System/370, VAX и CYBER. Прежде чем продолжить чтение данного раздела, вам следует посмотреть в гл. 1 описания этих машин. 2.5.1. Ассемблер System/370 Относительная базовая адресация в System/370 используется с той же целью, что и в УУМ/ДС, - для экономии места, занимаемого машинной командой. Полный адрес System/370 занимает 24 разряда. Вместе с тем тот же самый адрес можно закодировать с помощью номера базового регистра (4 разряда) и смещения (12 разрядов). Однако есть и отличие: в System/370 нет ни адресации относительно счетчика команд, ни чего-либо похожего на расширенный командный формат УУМ/ДС. Поэтому все команды, ссылающиеся на оперативную память, должны использовать относительную базовую адресацию. Как следствие этого при перемещении объектной программы System/370 модификация требуется только для элементов данных, значениями которых являются фактические адреса (т. е. для адресных констант). В System/370 любой регистр общего назначения (за исключением R0) может использоваться в качестве базового регистра. Решение о том, какие регистры использовать для этой цели, возлагается на программиста. В больших программах обычно одновременно используются несколько различных базовых регистров. Какие регистры могут использоваться в качестве базовых и каково их содержимое, программист сообщает при помощи директивы ассемблера USING. По своим функциям она похожа на директиву BASE языка ассемблера УУМ/ДС. Таким образом, предложения USING LENGTH,R1 USING BUFFER,R4 определяют в качестве базовых регистры R1 и R4. Предполагается, что R1 будет содержать адрес LENGTH, а R4 - адрес BUFFER. Так же как и в УУМ/ДС, программист должен предусмотреть команды, обеспечивающие загрузку этих регистров во время выполнения программы. Дополнительные предложения USING можно помещать в любом месте программы. Если базовый регистр в дальнейшем необходимо использовать для других целей, то программист с помощью предложения DROP может сообщить ассемблеру, что дальнейшее использование регистра в качестве базового запрещено. Дополнительная гибкость в использовании регистров влечет за собой увеличение объема работы, выполняемой ассемблером. Для того чтобы знать, какие регистры общего назначения могут в данный момент использоваться в качестве базовых регистров и каково их значение, используется таблица базовых регистров. При выполнении предложения USING в таблицу заносится новый элемент (или модифицируется уже существующий); при выполнении предложения DROP соответствующий элемент удаляется из таблицы. Для каждой команды, операнд которой является адресом оперативной памяти, ассемблер ищет в таблице подходящий для адресации базовый регистр. Если для адресации подходит более одного регистра, то выбирается тот, который дает наименьшую величину смещения. Если ни один из регистров не подходит, то ассемблирование команды невозможно. Процесс вычисления смещения в точности такой же, как и для УУМ/ДС. Наряду с этим в языке ассемблера System/370 разрешено явное задание базового регистра и смещения непосредственно в исходном предложении. Например, в предложении L R2,8(R4) в качестве адреса операнда задан адрес смещенный на 8 байт относительно адреса, содержащегося в R4. Такая форма адресации может быть полезна, если известно, что некоторый регистр содержит начальный адрес таблицы или записи данных, а программист хотел бы сослаться на определенную область внутри этой таблицы или записи. При обработке команд, в которых явно заданы базовый регистр и смещение, ассемблер просто заносит заданные значения в объектный код (в данном случае базовый регистр R4 и смещение 8). Таблица базовых регистров в этом случае не требуется; поэтому регистр, используемый подобным образом, не нужно описывать в предложении USING. Другой проблемой, которая возникает в ассемблере System/370, является выравнивание областей данных и команд. Некоторые команды выполняются более эффективно, если их операнды выровнены по границам памяти, соответствующим их длинам. Например, операнд длиной в одно слово (32 разряда) должен начинаться с байта, адрес которого кратен 4, а операнд длиной в полслова (16 разрядов) - с адреса, кратного 2. Точно так же требуется, чтобы машинная команда начиналась с байта, адрес которого кратен 2. Заботу о таком выравнивании берет на себя ассемблер, продвигая, если это необходимо, счетчик размещений. Предположим, например, что счетчик размещений содержит 0015 и необходимо разместить элемент данных длиной в одно слово. В этом случае, прежде чем резервировать память для этого слова, счетчик размещений следует увеличить до 0018 (ближайшее кратное 4 больше чем 0015). Байты объектной программы, пропущенные для того, чтобы обеспечить соответствующее выравнивание, называются пассивными байтами (slack bytes). (В некоторых ассемблерах System/370 включение или выключение выравнивания задается по выбору во время ассемблирования.) Пассивные байты являются для объектной программы потерянной памятью. Величина этих потерь существенно зависит от того, в каком порядке определяются элементы данных. Рассмотрим, например, последовательность предложений резервирования памяти, изображенную на рис. 2.24а. Если при обработке первого предложения счетчик размещений содержит, как это показано, адрес, кратный 4, то для размещения с выравниванием потребуется 20 байт памяти. Так как сами данные занимают только 14 байт, то это означает, что внесение пассивных байтов увеличило объем занимаемой оперативной памяти почти на 50 %. Адрес . . . 0024 0025 0028 002C 002F 0030 0032 0034 0038 . . . Исходное предложение A DS C B C DS DS F 3C D DS H E DS F 1 БАЙТ (3 пассивных байта) СЛОВО 3 БАЙТА (1 пассивный байт) ПОЛУСЛОВО (2 пассивных байта) СЛОВО а Адрес . . . 0024 0028 002C 002E 0031 Исходное предложение B E D C A DS DS DS DS DS F F H 3C C СЛОВО СЛОВО ПОЛУСЛОВО 3 БАЙТА 1 БАЙТ 0032 . . . б Рис. 2.24. Влияние выравнивания данных на требуемый объем оперативной памяти. Если сгруппировать элементы данных, требующие одинакового выравнивания, то можно значительно сократить эти накладные расходы. Например, если те же определения расположить в порядке, показанном на рис. 2.24б, то не потребуется ни одного пассивного байта. Ассемблер мало что может сделать с последовательностью определений данных, написанной программистом. В то же время размещение литеральных операндов непосредственно контролируется ассемблером. Большинство ассемблеров System/370 располагают элементы данных в литеральном пуле так, чтобы минимизировать потери памяти. Вначале назначаются адреса для всех литералов, которые требуют для своего размещения двойное слово, затем - слово, полуслово и т. д. Ассемблер System/370 обеспечивает поддержку аппарата управляющих секций (их называют CSECT). По своим функциям этот аппарат похож на средства, описанные в разд. 2.3.5. В то же время не предусмотрено никакого механизма, прямо соответствующего программным блокам (см. разд. 2.3.4). В System/370 управляющие секции могут состоять из нескольких отдельных сегментов. Сборка этих сегментов осуществляется ассемблером примерно так же, как это было описано для программных блоков. Однако управляющие секции не образуют единый программный модуль. Каждая из них остается самостоятельной единицей и обрабатывается загрузчиком или редактором связей независимо от других секций. Для каждого внешнего имени, используемого в CSECT, программист должен зарезервировать одно слово для хранения адресной константы. Значение этой константы заносится загрузчиком с помощью механизма, похожего на тот, что был описан в разд.2.3.5. Программист должен предусмотреть команды, которые обеспечат во время исполнения программы загрузку адресной константы в базовый регистр. Информация о том, какой базовый регистр может быть, использован для разрешения внешних ссылок, сообщается ассемблеру с помощью предложения USING. Предположим, что в одной из CSECT с помощью описанной выше процедуры определен базовый регистр для ссылки на метку, расположенную в другой CSECT. Если теперь обеспечить совместную трансляцию обеих этих секций, то тот же базовый регистр можно будет использовать для ссылки на другие метки этой внешней CSECT. Такое использование базового регистра допустимо, поскольку ассемблер сохраняет все элементы таблицы имен на протяжении всего периода своей работы. Однако в этом случае будет утрачена возможность раздельного ассемблирования этих управляющих секций. Как можно видеть, язык ассемблера System/370 использует аппарат CSECT для достижения двух логически различных целей - реорганизации объектной программы (аналогично программным блокам УУМ/ДС) и разбиения программы на отдельные модули (аналогично управляющим секциям УУМ/ДС). Это часто приводит к тому, что программисты, впервые познакомившиеся с данными концепциями на подобной системе, путают эти два понятия. Язык ассемблера System/370 предоставляет также возможность использовать управляющую секцию специального вида, которая называется фиктивной секцией (DSECT - Dummy SECTion). В DSECT может быть использовано любое предложение языка ассемблера, однако эти предложения не становятся частью объектной программы. Чаще всего DSECT используется для описания структуры сложных областей данных (записей, таблиц и т. п.), определенных вне данной программы. Метки, использованные в DSECT, определяют имена, которые могут быть использованы для адресации полей записи или таблицы (после того как будет задан подходящий базовый регистр). Дополнительную информацию о типовом ассемблере System/370 можно найти в IBM [1979], IBM [1974] и IBM [1982]. 2.5.2. Ассемблер ЭВМ VAX В ЭВМ VAX используется намного более гибкий метод кодирования операндов, чем в большинстве машин. Имеется необычно много способов адресации, и (за небольшим исключением) каждый из этих способов может быть использован в любой команде. Спецификаторы операндов, связанные с различными способами адресации, занимают различный объем памяти. Как мы увидим, это увеличивает объем работы, выполняемой ассемблером. Терминология, используемая в языке ассемблера VAX, несколько отличается от стандартного набора терминов, который мы использовали. Например, есть два способа адресации, в которых значения операнда хранятся как часть команды, - не посредственный и литеральный. (Для операндов этого типа мы использовали в разд. 2.2 термин непосредственная адресация.) Функционально эти два способа эквивалентны, и в исходной программе они задаются с помощью одной и той же нотации. Во время первого просмотра ассемблер, анализируя величину операнда, решает, какой способ адресации использовать. Если операнд является целым между 0 и 63 (или какой-либо другой величиной, которую можно разместить в 6-разрядном поле), то используется литеральный способ. Если операнд нельзя разместить в 6-разрядном поле или ассемблер еще не может определить его значение, то выбирается непосредственный способ. При непосредственном способе длина спецификатора операнда зависит от типа операнда, используемого в команде (слово, длинное слово и т. д.). Длина спецификатора операнда для литерального способа всегда равна одному байту. Обратите внимание, что операнды VAX, использующие литеральный способ адресации, обрабатываются совершенно иначе, чем литералы, о которых мы говорили в разд. 2.3. В языке ассемблера VAX нет никаких средств, которые бы соответствовали тому, что мы называли литералами в нашем ассемблере для УУМ/ДС. Имеется еще ряд случаев, когда длина спецификатора операнда (а следовательно, и длина команды) должна определяться ассемблером. Например, при использовании способа адресации относительно счетчика команд поле смещения может кодироваться как байт, слово или длинное слово. Ассемблер выбирает длину этого поля в соответствии с величиной требуемого смещения. Если в операнде команды какое-либо имя еще не определено, то ассемблер во время первого просмотра не может вычислить смещение. В этом случае выбирается смещение максимально возможного размера (длинное слово). Заметим, что это решение нельзя отложить до второго просмотра, так как длина команды влияет на значения всех меток, определенных в программе ниже данной команды. Величина смещения для адресации относительно счетчика команд вычисляется так, как это было описано в разд. 2.2. Однако в момент вычисления целевого адреса во время исполнения программы счетчик команд будет содержать не адрес следующей команды, а адрес следующего спецификатора операнда (если он есть). Это необходимо учитывать при вычислении величины смещения. В большинстве языков ассемблера длина ассемблированной команды однозначно определяется по ее мнемоническому коду операции. Для ЭВМ VAX это не так. Каждый спецификатор операнда VAX в зависимости от типа операнда может иметь длину от 1 до 9 байт. Например, в команде MOVB R1, R5 оба операнда находятся в регистрах. Каждый спецификатор операнда требует 1 байт; поэтому ассемблированная команда будет занимать 3 байт. В то же время в команде MOVB 1,8 (R6) для первого операнда используется литеральный способ адресации (длина спецификатора 1 байт), а для второго - базовая относительная адресация. С учетом величины смещения (8) для спецификатора второго операнда требуется 2 байт; таким образом, вся ассемблированная команда будет занимать 4 байт. В команде MOVB R1, ALPHA для спецификатора первого операнда требуется 1 байт. Для второго операнда используется адресация относительно счетчика команд. В зависимости от величины смещения его спецификатор может занимать от 2 до 5 байт. Таким образом, зависимости от адреса, назначенного метке ALPHA, данная команда может занимать от 4 до 7 байт. Это означает, что ассемблер ЭВМ VAX должен во время первого просмотра выполнять значительно более сложную работу, чем, скажем, ассемблер System/370. Во время первого просмотра ассемблер VAX должен анализировать не только коды операций, но и операнды команд. Таблица кодов операций также должна иметь более сложную структуру, так как в ней необходимо иметь информацию о том, какие способы адресации допустимы для каждого из операндов. Язык ассемблера VAX обеспечивает возможность организации управляющих секций, которые называются программными секциями (PSECT - Program SECTions). Подобно CSECT языка ассемблера System/370, PSECT используются для двух различных целей - реорганизации программы и разбиения программы на модули. В языке ассемблера VAX обработка PSECT отличается: меньшая часть работы выполняется ассемблером и большая - программой связывания. Если из одной PSECT происходит ссылка на имя, определенное в другой PSECT, и эти секции ассемблируются совместно, то для ссылки используется адресация относительно счетчика команд. Так как взаимное расположение секций относительно друг друга во время ассемблирования неизвестно, то величину смещения вычислить нельзя. Поэтому ассемблер генерирует команды для программы связывания, которые позволят ей определить смещение и занести его в команду. Этот процесс похож на тот, что был описан в разд. 2.3.4 для разрешения ссылок между программными блоками. Более детально мы рассмотрим работу программы связывания в следующей главе. При трансляции PSECT могут задаваться некоторые атрибуты, в том числе атрибут доступа: "только чтение", "только исполнение" и т. п. Кроме того, в каждой PSECT можно указать, разрешено или нет выравнивание элементов данных. (Проблемы выравнивания в VAX похожи на те, что мы обсуждали в связи с System/370.) Программа связывания собирает вместе все сегменты каждой PSECT и все программные секции, имеющие аналогичные атрибуты. Средства операционной системы VAX могут быть использованы для обеспечения защиты PSECT в соответствии с ее атрибутами. Ассемблер VAX предполагает по умолчанию, что все имена, которые были использованы, но не были определены во время ассемблирования, являются внешними ссылками. Никакого явного объявления внешних имен (подобно предложению ЕХТREF УУМ/ДС) не требуется. К несчастью, это приводит к тому, что ошибки типа пропуска символа в имени нельзя обнаружить до момента связывания программы. Однако программист может задать режим работы ассемблера, который отменяет данное правило умолчания и требует явного объявления внешних имен. Дополнительную информацию об ассемблере VAX можно найти в DEC [1982] и DEC [1979]. 2.5.3. Ассемблер ЭВМ CYBER Язык ассемблера ЭВМ CYBER, который называется COMPASS, имеет несколько существенных отличий от других языков. Первое отличие является прямым следствием структуры машины. Дело в том, что размер машинного слова CYBER позволяет разместить в нем несколько различных ассемблированных команд. В общем случае задачей ассемблера является упаковка как можно большего количества последовательно расположенных команд в одно слово объектной программы. Конечно, можно было бы просто размещать по одной машинной команде в слове, а неиспользованное пространство заполнять невыполняемыми командами, но такое решение весьма неэффективно с точки зрения как занимаемого пространства, так и времени исполнения. Для упаковки команд в ассемблере предусмотрен счетчик позиций (position counter). Этот счетчик показывает количество свободных разрядов, оставшихся в текущем слове объектной программы. В начале нового слова в счетчик позиций заносится число 60 (число разрядов слова CYBER). При генерации команды количество разрядов, требуемое для ее размещения, вычитается из счетчика позиций. Если в текущем слове нет места для размещения очередной команды, то оставшиеся разряды (если они есть) заполняются неисполняемыми командами, а для работы берется новое слово объектной программы. В этот момент счетчик размещений и счетчик слов (его мы опишем в этом разделе ниже) увеличиваются на 1, после чего они будут содержать адрес следующего слова. Предыдущее описание представляет собой обычную последовательность действий: команды (или элементы данных) упаковываются в машинное слово до тех пор, пока это возможно, а затем осуществляется переход к следующему слову. Однако в некоторых случаях требуется перейти к следующему слову несмотря на то, что в текущем слове еще достаточно места для размещения следующего элемента объектного кода. Такая необходимость является следствием того, что в CYBER применяется пословная, а не побайтная адресация оперативной памяти. Рассмотрим, например, команду безусловного перехода. Адрес операнда в данной команде должен быть адресом слова. После того как в счетчик команд будет занесен адрес, заданный в команде перехода, следующей командой, выбираемой для выполнения, будет первая команда, упакованная в слово с адресом, равным целевому адресу команды перехода. Это означает, что мы можем непосредственно передать управление только на команду, расположенную в начале слова объектной программы. Процесс размещения команды или элемента данных в начале слова, даже если в предыдущем слове достаточно места, называется проталкиванием (forsing upper). При этом остаток предыдущего слова заполняется неисполняемыми командами, счетчик слов и счетчик размещений увеличиваются на 1, а в счетчик позиций заносится число 60. Ассемблер COMPASS выполняет проталкивание в следующих случаях: 1. Ассемблируемое предложение имеет метку (так как адрес метки указывает на начало слова). 2. Ассемблируемое предложение является предложением резервирования памяти, и в нем не задана упаковка битовых полей в одно слово. 3. Предыдущее предложение является предложением безусловного перехода (в этом случае единственный способ выполнить следующую команду - передать на нее управление). Программист может задать проталкивание для любого предложения, задав в поле метки символ +. Для того чтобы отменить автоматическое проталкивание, достаточно задать в поле метки символ -. Кроме счетчика позиций ассемблер использует в своей работе еще два счетчика счетчик, размещений и счетчик слов. Вместе эти два счетчика служат для того же, что и LOCCTR, описанный в разд. 2.1. Основная функция счетчика слов та же, что и у LOCCTR: счетчик слов вместе со счетчиком позиций указывает на относительное местоположение в объектном коде следующей ассемблируемой команды. Счетчик размещений используется ассемблером для назначения адресов внутренним меткам программы. Ранее для этого мы также использовали LOCCTR. Обычно значения счетчика слов и счетчика размещений совпадают. Однако, если это необходимо, программист может с помощью директив ассемблера изменить значение счетчика размещений. Это может оказаться полезным только в весьма специфической ситуации когда при ассемблировании объектный код размещается в одном месте, а перед исполнением должен быть перемещен в другое. В этом случае программист изменяет значение счетчика размещений так, чтобы оно соответствовало адресу расположения объектного кода на момент исполнения. На рис. 2.25 показан пример такого использования счетчика размещений. Данная программа содержит большой буфер, используемый во время ее исполнения для ввода и вывода. Программа состоит из трех различных разделов, которые выбираются при вызове программы. Однако только один из этих разделов используется при исполнении программы. Исполняемые команды для каждого из трех разделов представлены в исходной программе в виде отдельных частей. Объектный код, полученный в результате трансляции каждой из этих частей, занимает значительный объем оперативной памяти. Если бы объектная программа была загружена вместе с буфером и тремя разделами, то она потребовала бы большого объема памяти. Для экономии места мы можем воспользоваться приемом, показанным на рис. 2.25а. Объектный код для Раздела 1 (наибольший раздел) ассемблируется и загружается обычным образом. В то же время код для Раздела 2 и Раздела 3 загружается в область буфера. Эта область не используется до тех пор, пока не начнется ввод, поэтому первоначальное размещение команд в этой области проблем не вызывает. Если для выполнения будет выбран Раздел 2 или Раздел 3, то соответствующий раздел объектного кода будет перемещен программой из области буфера на место кода Раздела 1 (рис. 2.25б). Теперь требуемый раздел выбирается просто вызовом объектного кода, а буфер может использоваться для ввода данных. Программист должен быть уверен, что при таком перемещении объектного кода метки будут определены правильно. Рассмотрим, например, команду с меткой SКIР2 на рис. 2.25в (строка 11). Поскольку счетчик слов был переустановлен (строка 7), то код Разделов 2 и 3 будет расположен внутри буфера. Если бы программа ассемблировалась обычным образом, то в этом случае значением метки SКIР2 был бы адрес внутри буферной области. Однако фактически во время исполнения команды этого раздела будут расположены в памяти, общей для всех трех разделов. Поэтому целевой адрес в команде перехода в строке 10 должен быть адресом в этой области, а не в области буфера. Эта проблема решается с помощью задания в качестве нового значения счетчика размещений начального адреса буферной области. Напомним, что в данном ассемблере для присваивания меткам значений используется счетчик размещений, а не счетчик слов. Поэтому метке SKIP2 будет присвоено значение, соответствующее адресу, по которому она будет расположена во время исполнения, а не адресу на момент ассемблирования. Аналогично обрабатывается код Раздела 3. После того как все три раздела будут оттранслированы, первоначальное значение счетчика слов восстанавливается с помощью предложения ORG в строке 18. Предложение в строке 19 устанавливает значение счетчика размещений равным значению счетчика слов, и обычный процесс ассемблирования будет продолжен. В COMPASS предусмотрены программные блоки (их называют подпрограммными блоками - subprogram bloks) и управляющие секции (их называют подпрограммами subprograms). Их обработка в основном схожа с процессом, описанным в разд.2.3, за исключением того, что все литералы размещаются в каждой подпрограмме отдельным блоком. Для каждого подпрограммного блока ассемблер заводит отдельный набор счетчиков (слов, размещений и позиций). Общий код Общий код Исполняемые команды Код Раздела 1 Код Раздела 2 Разделяемая область Код Раздела 2 Код Раздела 3 Код Раздела 3 а б Область буфера Рис. 2.25. Перемещение объектного кода во время исполнения программы. Последнее необычное свойство COMPASS, которое мы обсудим, заключено в самом языке ассемблера. Для большинства ЭВМ код машинной операции однозначно определяется мнемоническим кодом исходного предложения. Для CYBER это не так. Рассмотрим предложения SA1 А2 SA1 B2+TAB SA1 X1+B1 SA1 X1-B1 Первое из них будет ассемблировано в 15-разрядную команду с машинным кодом операции 54; второе - в 30-разрядную команду с кодом операции 51; третье - в 15разрядную команду с кодом операции 53; четвертое недопустимо. При интерпретации мнемонических кодов, подобных SA1 (и большинства других кодов языка ассемблера CYBER), необходимо анализировать операнды. Подобные команды исходной программы известны как синтаксически определяемые команды, поскольку они распознаются по своему синтаксису (т. е. форме предложения). Ассемблер COMPASS обрабатывает такие команды, просматривая мнемонический код операции и операнды и извлекая символьную строку, описывающую синтаксис. Для третьего из рассмотренных выше предложений синтаксический дескриптор будет иметь вид SAX + B. Этот дескриптор определяет команду группы А, операндом которой является сумма регистров X и В. Существуют 22 различных дескриптора подполей, которые могут комбинироваться для получения таких синтаксических дескрипторов. Строка 1 2 OPT1 Исходное предложение IDENT . . SA1 ... КОД РАЗДЕЛА 1 . 3 4 5 6 7 8 9 SKIP1 BUFFER OPT2 10 11 SKIP2 12 13 14 OPT3 15 16 SKIP3 17 18 19 . ZR . . SA6 . . EQ B4,SKIP1 ... ... ВЫХОД BSS 1000 БУФЕР ДЛИНОЙ 1000 СЛОВ ORG BUFFER LOC OPT1 SA2 ... КОД РАЗДЕЛА 2 . . NZ B3,SKIP2 . . SA7 ... . . EQ ... ВЫХОД LOC OPT1 SA6 ... КОД РАЗДЕЛА 3 . . EQ B1,B3,SKIP3 . . SA6 ... . . EQ ... ВЫХОД ORG BUFFER+1000 LOC *0 . . 20 END Рис.2.25в Синтаксически определяемые команды хранятся в таблице кодов операций в соответствии со своими синтаксическими дескрипторами. Для каждого элемента в таблице дополнительно хранится информация, определяющая, как различные части исходного предложения должны размещаться в объектной программе. При обработке предложений исходного языка ассемблер вначале ищет в таблице кодов операций мнемонический код команды. Для некоторых команд такой поиск оказывается успешным. Однако для синтаксически определяемых команд, таких как SA1, он оканчивается безрезультатно. В этом случае ассемблер просматривает исходное предложение и извлекает из него (как было описано ранее) синтаксический дескриптор. За тем выполняется второй просмотр таблицы. Если и этот поиск заканчивается неудачей, то опознание команды невозможно. Дополнительную информацию об ассемблере CYBER можно найти в СDC [1982]. Упражнения Раздел 2.1 1.Воспользуйтесь алгоритмом, приведенным на рис.2.4, для исходной программы на рис.2.1. Полученный вами результат должен быть таким, как показано на рис.2.2 и 2.3. 2. Воспользуйтесь алгоритмом, приведенным на рис.2.4, для следующей программы УУМ: SUM START 4000 FIRST LDX ZERO LDA ZERO LOOP ADD TABLE, X TIX COUNT JLT LOOP STA TOTAL RSUB TABLE RESW 2000 COUNT RESW 1 ZERO WORD 0 TOTAL RESW 1 END FIRST 3. Как уже отмечалось, для некоторых операций в алгоритме на рис.2.4 не дано детального описания. (Например, операция поиска модификатора, X в поле операндов.) Перечислите все, какие только можете, не детализированные операции и подумайте, как можно было бы их реализовать. 4. Алгоритм на рис.2.4 реализован в виде единого блока. В то же время инженерный подход к созданию программного обеспечения требует, чтобы большие программы разбивались на несколько модулей с четко определенными интерфейсами. Предложите набор модулей для реализации этого алгоритма. Укажите функции каждого из модулей и опишите, как каждый из них используется другими частями ассемблера. 5. Многие ассемблеры используют свободный формат записи команд. Метки должны начинаться в первом столбце исходного предложения, но другие поля (код операции, операнды, комментарии) могут начинаться в любом столбце. Друг от друга поля отделяются пробелами. Как надо изменить алгоритм нашего ассемблера для того, чтобы он смог работать в свободном формате? 6. Алгоритм на рис.2.4 позволяет найти некоторые из ошибок в исходной программе. Однако существует много других ошибок, которые могут возникать в процессе ассемблирования программ УУМ. Когда и как каждая из этих ошибок может быть обнаружена и какие действия должен при этом предпринять ассемблер? Раздел 2.2 1.Может ли ассемблер самостоятельно решить, какая из команд должна быть сгенерирована в расширенном формате? (Это позволит снять с программиста заботу о явном указании таких команд с помощью признака +.) 2. Как мы уже говорили, предложение ВASE служит лишь для передачи информации ассемблеру. Программист должен, кроме того, предусмотреть команды наподобие LDB для загрузки соответствующего значения в базовый регистр. Может ли ассемблер автоматически генерировать команду LDB по директиве BASE? Если да, то в чем заключаются плюсы и минусы подобной интерпретации предложения BASE? 3. Сгенерируйте объектный код для каждого предложения следующей программы УУМ/ДС: SUM START 0 FIRST LDX #0 LDA #0 +LDB #TABLE2 LOOP COUNT TABLE TABLE2 TOTAL BASE ADD ADD TIX JLT +STA RSUB TABLE2 TABLE,X TABLE2,X COUNT LOOP TOTAL RESW RESW RESW RESW 1 2000 2000 1 END FIRST 4. Сгенерируйте законченную объектную программу для исходной программы из упр.3. 5. Модифицируйте алгоритм, показанный на рис.2.4, так, чтобы он обрабатывал все допустимые в УУМ/ДС способы адресации. Как отразятся эти изменения на предложенной вами в упр.2.1.4 модульной структуре? 6. Модифицируйте алгоритм, показанный на рис.2.4, так, чтобы он позволял получать перемещаемые программы. Как отразятся эти изменения на предложенной вами в упр.2.1.4 модульной структуре? 7. Формат записи-модификатора хорошо подходит для программ УУМ/ДС потому, что все адресные поля в командах и данных располагаются, начиная с границы полубайта. Как должна была бы выглядеть запись-модификатор, если бы это было бы не так (т. е. если бы адресные поля могли бы начинаться с произвольного места внутри байта и могли бы иметь произвольную длину)? 8. Предположим, что мы сделали программу на рис.2.1 перемещаемой. Эта программа написана для стандартной модели УУМ, поэтому все адреса операндов являются фактическими адресами и имеется только один командный формат. Во время ее загрузки практически каждая команда объектной программы нуждается в модификации адресов операндов. Это потребует большого количества записей-модификаторов (что более чем в два раза увеличит размер объектной программы). Как можно было бы включить информацию, необходимую для перемещения программы, без столь значительного увеличения размера объектной программы? Раздел 2.3 1. Модифицируйте алгоритм, показанный на рис.2.4 так, чтобы он мог обрабатывать литералы. 2. Можно ли было в программе, приведенной на рис.2.9 в строках 135 и 145 использовать литералы? Почему мы в этом случае не стали их использовать? 3. Немного расширив нашу литеральную нотацию, мы могли бы записать команду в строке 55 рис.2.9 в виде LDA =W'3' указав, что литеральный операнд является словом и имеет значение, равное 3. Хорошо ли это? 4. Непосредственные операнды и литералы - это два способа задания значения операнда непосредственно в исходном предложении. Каковы преимущества и недостатки каждого из них? В каких случаях один предпочтительнее другого? 5. Предположим, что мы внесли следующие изменения в программу на рис.2.9: а) Исключили предложение LTORG в строке 93. б) Заменили предложение в строке 45 на + LDA... . в) Заменили операнды в строках 135 и 145 на литералы (и исключили строку 185). Каким будет в этом случае объектный код для строк 45, 135, 145, 215 и 230? Как будет в этом случае выглядеть литеральный пул? Замечание: для выполнения этой работы полная перетрансляция программы не требуется. 6. Предположим, что имена ALPHA и BETA являются метками исходной программы. Объясните, чем различаются две следующие последовательности предложений: a) LDA ALPHA - BETA б) LDA ALPHA SUB BETA 7. Чем различаются нижеприведенные последовательности предложений: а) LDA #3 б) THREE EQU 3 . . . LDA #THREE в) THREE EQU 3 . . . LDA THREE 8. Модифицируйте алгоритм, показанный на рис.2.4 так, чтобы он мог работать с несколькими программными блоками. 9. Модифицируйте алгоритм, показанный на рис.2.4 так, чтобы он мог работать с несколькими управляющими секциями. 10. Предположим, что в ассемблере реализованы все возможности, описанные в разд. 2.3. Как следует в этом случае изменить таблицу имен по сравнению с тем, как она описана в разд. 2.1? 11. Если различные управляющие секции ассемблируются совместного некоторые ссылки между ними могут быть обработаны ассемблером (вместо того чтобы оставлять их загрузчику). Например, в программе, приведенной на рис.2.15, выражение в строке 190 может быть вычислено непосредственно ассемблером, так как его таблица имен содержит всю необходимую информацию. В чем заключаются преимущества и недостатки такой обработки? 12. Пусть в программе, приведенной на рис.2.11, мы использовали только два программных блока - непоименованный блок и CBLKS. Предположим, что элементы данных СDАТА включены в непоименованный блок. Какие надо для этого сделать изменения в исходной программе? Покажите, какая в этом случае будет получена объектная программа. 13. Предположим, что метка LENGTH определена так, как в программе, показанной на рис.2.9. Чем различаются две нижеследующие последовательности предложений? а) LDA LENGTH SUB #1 б) LDA LENGTH - 1 14. В соответствии с определениями имен на рис.2.10 укажите значения, тип и интуитивный смысл (если он есть) для каждого из следующих выражений: а) BUFFER - FIRST б) BUFFER + 4095 в) MAXLEN - 1 г) BUFFER + MAXLEN - 1 д) BUFFER - MAXLEN е) 2 * LENGTH ж) 2 * MAXLEN - 1 з) MAXLEN - BUFFER и) FIRST + BUFFER к) FIRST - BUFFER + BUFEND 15. В программе, приведенной на рис.2.9, строка 107 записана как MAXLEN EQU BUFEND - BUFFER Чем это лучше записи вида MAXLEN EQU 4096 16. Можно ли в программе, приведенной на рис.2.15, заменить строку 190 на MAXLEN EQU BUFEND - BUFFER а строку 133 на + LDT #MAXLEN как мы это сделали на рис.2.9? 17. Ассемблер мог бы просто предполагать, что все ссылки на имена, не определенные в данной управляющей секции, являются внешними ссылками. В этом случае отпадает необходимость в предложении EXTREF. Хорошо ли это? 18. Каким образом ассемблер, допускающий внешние ссылки, мог бы обойтись без предложения EXTDEF? Какие здесь есть преимущества и недостатки ? 19. Ассемблер мог бы автоматически использовать расширенный формат для команд, в операндах которых используются внешние ссылки. В этом случае программист мог бы не задавать в этих командах признак +. В чем заключаются преимущества и недостатки такого подхода? 20. В некоторых системах управляющие секции могут быть скомпонованы из нескольких частей точно так же, как программные блоки. Какие проблемы ставит это перед ассемблером? Как они могут быть разрешены? 21. Предположим, что имена RDREC и COPY определены так, как показано на рис.2.15. В соответствии с нашими правилами выражение RDREC - COPY является недопустимым (т.е. ассемблер и/или загрузчик откажутся его обрабатывать). Предположим, что в силу каких-либо причин программе действительно необходимо значение этого выражения. Как это можно сделать, не меняя правил составления выражений? 22. Мы рассмотрели большое количество директив ассемблера, и еще многие другие директивы могли бы быть реализованы в реальных ассемблерах. Их поиск путем последовательного сравнения может оказаться весьма неэффективным. Как мы могли бы использовать таблицу, например похожую на ОРТАВ, для того, чтобы ускорить распознавание и обработку директив ассемблера? (Указание: ответ может зависеть от языка, на котором написан ассемблер. ) 23. Что кроме листинга исходной программы со сгенерированным объектным кодом могло бы быть полезно для программиста? Предложите несколько вариантов листингов, которые могут быть сгенерированы, и рассмотрите структуры данных или алгоритмы, необходимые для их построения. Раздел 2.4 1. Рассмотрите базовый ассемблер, алгоритм которого показан на рис.2.4. Какие таблицы и подпрограммы следует включить в корневой сегмент этого ассемблера? 2. Какие таблицы и подпрограммы следует включить в корневой сегмент усовершенствованного ассемблера, в котором реализованы возможности, описанные в разд. 2.3? 3. Процесс разрешения нескольких ссылок вперед должен требовать меньших накладных расходов, чем полный второй просмотр исходной программы. Почему же для повышения эффективности все ассемблеры не используют однопросмотровую схему ассемблирования? 4. Предположим, мы хотели бы, чтобы наш ассемблер выдавал таблицу перекрестных ссылок для всех имен, использованных в исходной программе. Для программы, приведенной на рис.2.5, такой листинг мог бы иметь следующий вид: Имя COPY FIRST CLOOP ENDFIL EOF RETADR LENGTH . . . Строка определения 5 10 15 45 80 95 100 Строка использования 255 40 30 45 10,70 12,13,20,60,175,212 Как ассемблер может это сделать? Укажите изменения, которые потребуется при этом внести в алгоритм и таблицы, описанные в разд. 2.1. 5. Может ли однопросмотровый ассемблер создавать перемещаемую объектную программу и обрабатывать внешние ссылки? Опишите требуемые для этого алгоритмы обработки и укажите потенциальные трудности. 6. Как можно в однопросмотровом ассемблере реализовать литералы? 7. Мы рассмотрели однопросмотровые ассемблеры, в которых операндами команд могут быть только одиночные имена. Как сделать так, чтобы однопросмотровый ассемблер мог обрабатывать команды вида JEQ ENDFIL + 3 где имя ENDFIL еще не определено? 8. Опишите алгоритм простого однопросмотрового ассемблера, работающего по схеме загрузка-выполнение. 9. Предположим, что команда, в которой используется ссылка вперед, должна ассемблироваться с помощью адресации относительно счетчика команд. Как можно ее обрабатывать с помощью однопросмотрового ассемблера? 10. Процесс разрешения ссылок вперед в одпопросмотровом ассемблере, создающем объектную программу, весьма похож на процесс связывания, описанный в разд. 2.3.5. Почему мы не ограничились простым использованием записей-модификаторов для разрешения ссылок вперед? 11. Как можно расширить метод, описанный в разд. 2.4.3, для обработки ссылок вперед в предложениях ORG? Глава 3. Загрузчики и программы связывания Как мы видели, объектная программа содержит оттранслированные команды и значения данных, полученные из исходной программы, и, кроме того, определяет адреса в оперативной памяти, куда эти команды и данные должны помещаться. В гл. 2 мы познакомились с тремя следующими процессами: 1. Загрузкой, обеспечивающей размещение программы в оперативной памяти для исполнения. 2. Перемещением, которое позволяет модифицировать объектную программу так, что она может загружаться с адреса, отличного от первоначально заданного (см. разд. 2.2.2). 3. Связыванием, обеспечивающим объединение двух или более раздельно оттранслированных объектных программ и предоставляющим информацию, необходимую для разрешения ссылок между ними (см. разд. 2.3.5). Загрузчик - это системная программа, выполняющая загрузку. Многие загрузчики обеспечивают, кроме того, перемещение и связывание. В некоторых системах функция связывания отделена от функций перемещения и загрузки. Связывание выполняется специальной программой связывания (или редактором связей), перемещение и загрузка загрузчиком. В большинстве случаев трансляторы (т. е. ассемблеры и компиляторы) создают в каждой конкретной системе объектный код в некотором стандартном формате. Таким образом, загрузчик и программа связывания могут использоваться вне зависимости от того, на каком языке была написана исходная программа. В этой главе мы изучим вопросы проектирования и реализации загрузчиков и программ связывания. Для простоты мы часто будем использовать термин "загрузчик" вместо "загрузчик и/или программа связывания". Ввиду того что процессы ассемблирования и загрузки тесно связаны, данная глава по своей структуре похожа на предыдущую. Многие из примеров, использованных нами при изучении ассемблеров, появятся и в этой главе. Во время обсуждения ассемблеров мы рассмотрели некоторые свойства и возможности, которые касаются как ассемблера, так и загрузчика. В этой главе мы вновь встретимся с такими концепциями. Естественно, что теперь нас в основном будут интересовать функции загрузчика, однако важно помнить о тесной связи между трансляцией и загрузкой программ. Как и в предыдущей главе, мы вначале рассмотрим основную функцию изучаемого программного обеспечения - в данном случае загрузку объектной программы в оперативную память для последующего ее исполнения. В разд. 3.1 представлен абсолютный загрузчик (absolute loader). Подобный загрузчик мог бы использоваться для УУМ совместно с ассемблером, описанным в разд. 2.1. В разд. 3.2 разбираются вопросы перемещения и связывания программ с точки зрения загрузчика. Мы обсудим несколько возможных способов представления объектной программы и исследуем, как они связаны со структурой ЭВМ. Мы также рассмотрим связывающий загрузчик (linking loader), представляющий собой наиболее совершенный тип загрузчика, который используется в большинстве современных вычислительных систем. В разд. 3.3 приведены некоторые из наиболее часто встречающихся свойств загрузчика, которые не имеют прямой связи со структурой машины. Как и прежде, нашей целью является не рассмотрение всех возможных свойств, а изучение концепций и технических приемов, наиболее часто применяемых при создании загрузчиков. В разд. 3.4 обсуждаются возможные альтернативные способы реализации функций загрузчика. Мы разберем различные способы выполнения перемещения и связывания, а также преимущества и недостатки каждого из них. С этой точки зрения будут рассмотрены редакторы связей (выполняющие связывание до загрузки) и схема динамического связывания (в которой связывание откладывается до момента исполнения программы). Наконец, в разд.3.5 мы вкратце обсудим некоторые примеры реальных загрузчиков и программ связывания. Мы рассмотрим те же машины, для которых в разд. 2.5 были описаны ассемблеры, и укажем на взаимосвязь, существующую между ассемблером и загрузчиком. Как и ранее, мы сосредоточим наше внимание на тех аспектах системных компонент, которые зависят от особенностей аппаратуры или программного обеспечения. 3.1. Основные функции загрузчика В данном разделе мы обсудим наиболее важные функции загрузчика запись объектной программы в оперативную память и передачу управления на адрес начала ее исполнения. Вероятно, вы уже знакомы с тем, как выполняются эти функции. Данный раздел будет служить отправной точкой для последующего обсуждения более сложных функций загрузчика. Здесь мы рассмотрим абсолютный загрузчик, который может быть использован совместно с ассемблером, описанным в раза. 2.1. Для представления объектной программы будет использоваться формат, описанный в разд. 2.1.1. Пример объект ной программы показан на рис. 3.1а. H_COPY _001000_00107A T_001000_1E_141033_482039_001036_281030_301015_482061_3C1003_00102A_0C1039_0 0102D T_00101E_15_0C1036_482061_081033_4C0000_454F46_000003_000000 T_002039_1E_041030_001030_E0205D_30203F_D8205D_281030_302057_549039_2C205E_ 38203F T_002057_1C_101036_4C0000_F1_001000_041030_E02079_302064_509039_DC2079_2C10 36 T_002073_07_382064_4C0000_05 E_001000 a Адрес 0000 0010 . . . 0FF0 1000 1010 1020 1030 . . . 2030 2040 2050 2060 2070 2080 . . . Содержимое хххххххх xxxxxxxх . . . xxxxxxxх 14103348 20613С10 36482061 000000xx . . . xxxxxxxх 205D3020 392С205Е 00041030 2С103638 xxxxxxxх . . . хххххххх хххххххх . . . . . . хххххххх 20390010 0300102А 0810334С хххххххх . . . . . . хххххххх 3FD8205D 38203F10 Е0207930 20644С00 хххххххх . . . . . . хххххххх хххххххх . . . хххххххх 36281030 0С103900 0000454F хххххххх . . . хх041030 28103030 10364С00 20645090 0005хххх хххххххх . . . хххххххх хххххххх хххххххх 30101548 102D0С10 46000003 хххххххх COPY 001030Е0 20575490 00F10010 39DС2079 хххххххх хххххххх б Рис. 3.1. Загрузка абсолютной программы. Поскольку от нашего загрузчика не требуется выполнения связывания и перемещения программ, его работа весьма проста. Все выполняется за один просмотр. Вначале, для того чтобы удостовериться, что программа, переданная для загрузки, корректна (и что для нее достаточно места в оперативной памяти), просматривается запись-заголовок. Затем последовательно считываются записи тела программы и содержащийся в них объектный код помещается в оперативную память по указанному адресу. И наконец, как только будет прочитана запись-конец, загрузчик передает управление по адресу, заданному в качестве адреса начала исполнения программы. На рис. 3.1б показано, как будет выглядеть программа рис. 3.1а после загрузки. Содержимое областей оперативной памяти, для которых нет записей в теле программы, обозначено как хххх. Это означает, что первоначальное содержимое этих областей памяти не менялось. На рис. 3.2 показан алгоритм рассмотренного нами простого загрузчика. Хотя данный процесс крайне прост, все же есть один аспект, заслуживающий комментария. В нашей объектной программе каждый байт ассемблированного кода дается в шестнадцатеричном символьном представлении. begin прочитать запись-заголовок проверить имя и длину программы прочитать первую запись тела программы while тип записи < > 'Е' do begin {если объектный код задан в символьном виде, то преобразовать его во внутреннее представление} переписать объектный код в заданное место оперативной памяти прочитать следующую запись объектной программы end передать управление по адресу, эаданному в занисе-конец еnd Рис. 3.2. Алгоритм абсолютного загрузчика. Например, машинный код операции для команды STL представляется в виде пары символов 14. Когда они считываются загрузчиком (как часть объектной программы), они будут занимать два байта памяти. Однако в команде, которая загружается для выполнения, этот код операции должен быть записан в одном байте с шестнадцатеричным значением 14. Таким образом, каждая пара байтов объектной программы должна быть упакована во время загрузки в один байт. Очень важно четко понимать, что на рис. 3.1а каждый символ представляет одни байт записи объектной программы. С другой стороны, на рис. 3.1б каждый символ представляет одну шестнадцатеричную цифру (т. е. полбайта). Данный способ представления объектной программы (Прим. - Напомним, что под объектной программой понимается информация, поступающая загрузчику в качестве исходных данных.) неэффективен как с точки зрения занимаемого объема памяти, так и времени выполнения загрузки. Поэтому в большинстве машин объектные программы хранятся в двоичном представлении, в котором каждый байт объектного кода записывается в виде отдельного байта объектной программы. Конечно, в таком представлении каждой байт может содержать любую двоичную величину. Мы должны быть уверены, что в имеющихся в системе соглашениях по работе с файлами и устройствами не предусмотрено использование каких-либо кодов в качестве управляющих символов. Например, соглашение, описанное в разд. 2.1, в котором в качестве признака конца записи используется байт, содержащий шестнадцатеричное значение 00, очевидно, не подходит для представления объектной программы в двоичном виде. Очевидно, что объектная программа, хранимая в двоичном виде, не годится для использования в книге, так как ее трудно воспринимать читателю. Поэтому мы и далее будем пользоваться в наших примерах символьным представлением объектных программ. 3.2. Машинно-зависимые свойства загрузчиков Абсолютный загрузчик, описанный в разд. 2.1, конечно, весьма прост и эффективен, однако данная схема имеет ряд потенциальных недостатков. Одним из наиболее очевидных является то, что от программиста требуется определять фактический адрес начала загрузки программы во время ее ассемблирования. Если мы рассматриваем очень простую ЭВМ с небольшим объемом оперативной памяти (такую, как стандартная модель УУМ), то это не создает особых проблем. В этом случае места достаточно только для выполнения одной программы, и поэтому начальный адрес загрузки известен заранее. Для более совершенных машин, обеспечивающих работу с большим объемом оперативной памяти (подобных УУМ/ДС), ситуация не столь проста. Очень часто желательно одновременно выполнять несколько независимых программ, совместно использующих оперативную память и другие ресурсы. Это означает, что мы не знаем заранее, куда будет загружена программа. Поэтому для того, чтобы обеспечить эффективное разделение ресурсов машины, требуется создавать перемещаемые, а не абсолютные программы. Абсолютные программы неудобны также и для использования в библиотеках стандартных подпрограмм. Количество подпрограмм, содержащееся в большинстве таких библиотек (например, пакеты для научных или математических расчетов), намного больше, чем требуется для каждой отдельно взятой программы. Поэтому для того, чтобы эффективно использовать оперативную память, необходимо иметь возможность выбирать только те подпрограммы, которые действительно нужны. Сделать это, если все подпрограммы должны загружаться с заранее назначенных адресов, весьма непросто. В этом разделе мы рассмотрим более сложный загрузчик. Такой загрузчик вполне подходит для УУМ/ДС и является типичным для большинства современных ЭВМ. Наряду с простой функцией размещения программы в оперативной памяти, описанной в предыдущем разделе, данный загрузчик выполняет также перемещение и связывание программ. Одним из вопросов, которого мы коснемся в данном разделе, будет вопрос о влиянии структуры машины на решения, применяемые при проектировании загрузчика. Необходимость выполнения перемещения программ косвенно связана с переходом на более мощные и большие ЭВМ. Конкретный способ, используемый загрузчиком для реализации перемещения, зависит от структуры ЭВМ. Эта зависимость будет рассмотрена в разд. 3.2.1. Мы обсудим различные технические приемы, которые могут быть использованы для перемещения программ, и укажем область их применения. В разд. 3.2.2 разбирается процесс связывания программ с точки зрения загрузчика. Функция связывания по сравнению с перемещением обладает меньшей степенью машинной зависимости. Однако на практике для реализации обеих этих функций очень часто используется один и тот же механизм. Кроме того, процесс связывания обычно требует перемещения некоторых подпрограмм. (см. приведенный выше пример использования библиотечных подпрограмм.) Поэтому мы будем рассматривать эти два процесса вместе. В разд. 3.2.3 обсуждаются структуры данных и алгоритмы, используемые в типичном связывающем (и перемещающем) загрузчике. Рассматриваемые здесь алгоритмы будут использованы в последующих разделах в качестве отправной точки при изучении более сложных возможностей, предоставляемых загрузчиком. 3.2.1. Перемещение Загрузчики, обеспечивающие перемещение программ, называются перемещающими загрузчиками (relocating loaders) или относительными загрузчиками (relative loaders). С концепцией перемещения программ мы познакомились в разд. 2.2.2. Вам, прежде чем продолжить чтение, следует бегло повторить материал этого раздела. В данном разделе мы обсудим два способа, в которых информация о перемещении задается как составная часть объектной программы. Первый из рассматриваемых способов в основном аналогичен тому, что мы обсуждали в гл. 2. Для описания каждого фрагмента объектного кода, требующего изменения при перемещении, используется специальная запись-модификатор. (Ее формат описан в разд. 2.3.5.) На рис. 3.3 показана программа для УУМ/ДС, которую мы будем использовать для иллюстрации первого способа задания информации о перемещении. Это та же самая программа, что и на рис. 2.6. Строка 5 0000 10 0000 12 0003 13 15 0006 20 000A 25 000D 30 0010 35 0013 40 0017 45 001A 50 001D 55 0020 60 0023 65 0026 70 002A 80 002D 95 0030 100 0033 105 0036 110 115 120 125 1036 130 1038 132 103A 133 103C 135 1040 140 1043 145 1046 150 1049 155 104B 160 104E 165 1051 170 1053 175 1056 180 1059 185 105C 195 200 205 210 105D 212 105F 215 1062 220 1065 225 1068 230 106B 235 106E Адрес Исходное предложение Объектный код COPY START 0 FIRST STL RETADR 17202D LDB #LENGTH 69202D BASE LENGTH CLOOP +JSUB RDREC 4B101036 LDA LENGTH 032026 COMP #0 290000 JEQ ENDFIL 332007 +JSUB WRREC 4B10105D J CLOOP 3F2FEC ENDFIL LDA EOF 032010 STA BUFFER 0F2016 LDA #3 010003 STA LENGTH 0F200D +JSUB WRREC 4B10105D J @RETADR 3E2003 EOF BYTE C"EOF" 454F46 RETADR RESW 1 LENGTH RESW 1 BUFFER RESB 4096 * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * RDREC CLEAR X B410 CLEAR A B400 CLEAR S B440 +LDT #4096 75101000 RLOOP TD INPUT E32019 JEQ RLOOP 332FFA RD INPUT DB2013 COMPR A,S A004 JEQ EXIT 332008 STCH BUFFER,X 57C003 TIXR T B850 JLT RLOOP 3B2FEA EXIT STX LENGTH 134000 RSUB 4F0000 INPUT BYTE X"F1" F1 * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * WRREC CLEAR X B410 LDT LENGTH 774000 WLOOP TD OUTPUT E32011 JEQ WLOOP 332FFA LDCH BUFFER,X 53C003 WD OUTPUT DF2008 TIXR T B850 240 1070 245 1073 250 1076 255 JLT RSUB OUTPUT BYTE END WLOOP 3B2FEF 4F0000 X"05" 05 FIRST Рис.3.3 Пример программы для УУМ/ДС (с рис.2.6). Здесь мы ее воспроизводим исключительно для удобства. Большинство команд данной программы использует относительную или непосредственную адресацию. Единственной частью объектной программы, содержащей фактические адреса, являются команды, в которых используется расширенный командный формат (строки 15, 35 и 65). Поэтому при перемещении программы изменение значений потребуется только для этих величин. На рис. 3.4 показана объектная программа, соответствующая исходному тексту на рис. 3.3. Обратите внимание, что для каждого значения, требующего изменения при перемещении, имеется отдельная запись-модификатор (в данном случае это три ранее упомянутые команды). Каждая запись-модификатор определяет начальный адрес и длину поля, значение которого необходимо изменить, а также тип требуемой модификации. H_COPY _000000_001077 T_000000_1D_17202D_69202D_4B101036_032026_290000_332007_4B10105D_3F2FEC_03 2010 T_00001D_13_0F2016_010003_0F200D_4B10105D_3E2003_454E46 T_001036_1D_B410_B400_B440_75101000_E32019_332FFA_DB2013_A004_332008_57C0 03_B850 T_001053_1D_3B2FEA_134000_4F0000_F1_B410_774000_E32011_332FFA_53CO03_DF20 08_B850 T_001070_07_3B2F3F_4F0000_05 M_000007_05+COPY M_000014_05+COPY M_000027_05+COPY E_000000 Рис. 3.4. Объектная программа, в которой перемещение задается с помощью записеймодификаторов. В данном примере все модификации заключаются в прибавлении значения метки COPY, представляющей собой начальный адрес программы. Алгоритм, используемый загрузчиком для выполнения данной операции, описывается в разд. 3.2.3. Дополнительные примеры определяемого подобным образом перемещения будут приведены в следующем разделе при изучении взаимосвязи между перемещением и связыванием. Записи-модификаторы являются удобным средством для задания информации о перемещении программы. Однако данная схема годится не для всех машин. Рассмотрим, например, программу, приведенную на рис.3.5. Это перемещаемая программа, написанная для стандартной модели УУМ. Ее важным отличием от программы, изображенной на рис. 3.3, является то, что стандартная модель УУМ не имеет относительной адресации. В данной программе адреса всех команд, за исключением RSUB, должны модифицироваться при перемещении. Поэтому для задания информации о перемещении необходимо иметь 31 запись-модификатор, что потребует более чем двукратного увеличения объема памяти, занимаемой объектной программой, по сравнению с программой, показанной на рис. 3.4. Строка 5 0000 10 0000 Адрес Исходное предложение COPY START 0 FIRST STL RETADR Объектный код 140033 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 155 160 165 170 175 180 185 190 195 200 205 210 215 220 225 230 235 240 245 250 255 0003 0006 0009 000C 000F 0012 0015 0018 001B 001E 0021 0024 0027 002A 002D 0030 0033 0036 0039 1039 103C 103F 1042 1045 1048 104B 104E 1051 1054 1057 105A 105D 105E 1061 1064 1067 106A 106D 1070 1073 1076 1079 CLOOP JSUB RDREC 481039 LDA LENGTH 000036 COMP ZERO 280030 JEQ ENDFIL 300015 JSUB WRREC 481061 J CLOOP 3С0003 ENDFIL LDA EOF 00002А STA BUFFER 0С0039 LDA THREE 00002D STA LENGTH 0С0036 JSUB WRREC 481061 LDL RETADR 080033 RSUB 4С0000 EOF BYTE C"EOF" 454F46 THREE WORD 3 000003 ZERO WORD 0 000000 RETADR RESW 1 LENGTH RESW 1 BUFFER RESB 4096 * * ПОДПРОГРАММА ВВОДА ЗАПИСИ НА БУФЕР * RDREC LDX ZERO 040030 LDA ZERO 000030 RLOOP TD INPUT E0105D JEQ RLOOP 30103F RD INPUT D8105D COMP ZERO 280030 JEQ EXIT 301057 STCH BUFFER,X 548039 TIX MAXLEN 2C105E JLT RLOOP 38103F EXIT STX LENGTH 100036 RSUB 4C0000 INPUT BYTE X"F1" F1 MAXLEN WORD 4096 001000 * * ПОДПРОГРАММА ВЫВОДА ЗАПИСИ ИЗ БУФЕРА * WRREC LDX ZERO 040030 WLOOP TD OUTPUT E01079 JEQ WLOOP 301064 LDCH BUFFER,X 508039 WD OUTPUT DC1079 TIX LENGTH 2C0036 JLT WLOOP 381064 RSUB 4C0000 OUTPUT BYTE X"05" 05 END FIRST Рис.3.5. Перемещаемая программа для стандартной модели УУМ. На машинах, где преимущественно используются прямая адресация и фиксированный командный формат, очень часто более эффективным оказывается другой способ задания информации о перемещении. На рис. 3.6 показано, как этот способ может быть применен к нашему примеру. В данном случае записи-модификаторы не используются. Формат записей тела программы то же, что и ранее, за исключением того, что теперь с каждым словом объектного кода связан разряд перемещения. Поскольку все команды УУМ занимают одно слово, это означает, что для каждой потенциальной команды имеется один разряд перемещения. Эти разряды собираются вместе и образуют маску, которая записывается после указателя длины каждой записи тела программы. На рис. 3.6 эта маска (в символьном виде) показана в виде трех шестнадцатеричных цифр (на рисунке они подчеркнуты). Если разряд перемещения установлен в 1, то при перемещении программы начальный адрес программы добавляется к слову, соответствующему этому разряду. Значение разряда перемещения, равное О, показывает, что никаких преобразований при перемещении делать не надо. Если запись тела программы содержит менее 12 слов объектного кода, то разряды, соответствующие неиспользуемым словам, устанавливаются в 0. H_COPY _000000_00107A T_000000_1E_FFC_140033_481039_000036_280030_300015_481061_3C0003_00002A_0C0 039_00002D T_00001E_15_E00 _0C0036_481061_080033_4C0000_454F46_000003_000000 T_001039_1E_FFC_040030_000030_E0105D_30103F_D8105D_280030_301057_548039_2C 105E_38103F T_0010570A_800_100036_4C0000_F1_001000 T_001061_19_FE0_040030_E01079_301064_508039_DCI079_2C0036_381064_4C0000_05 E_000000 Рис. 3.6. Объектная программа, в которой перемещение задается с помощью маски перемещения. Таким образом, в первой записи тела программы маска FFC (представляющая собой строку 111111111100 ) показывает, что все 10 слов объектного кода должны быть модифицированы при перемещении программы. В данных словах находятся команды, соответствующие в исходной программе строкам с 10 по 55 (см. рис. 3.5). Маска Е00 во второй записи тела программы определяет, что модификация требуется для трех первых слов. Оставшаяся часть объектного кода в данной записи представляет собой константы (и команду RSUB), не требующие модификации. Аналогично строятся остальные записи тела программы. Обратите внимание, что объектный код, генерируемый для команды LDX в строке 210, начинает новую запись тела программы, хотя в предыдущей записи достаточно места для его размещения. Это произошло потому, что каждый разряд перемещения связан с 3-байтовым сегментом объектного кода (словом) в записи тела программы. Поэтому каждое значение, требующее модификации при перемещении, должно совпадать с одним из этих 3-байтовых сегментов для того, чтобы ему можно было поставить в соответствие разряд перемещения. Ассемблированная команда LDX действительно требует модификации, поскольку в ней используется прямая адресация. Однако, если бы она была помещена в предыдущую запись тела программы, она не была бы правильно выровнена для соответствия разряду перемещения, так как в строке 185 была сгенерирована 1-байтовая константа. Поэтому данная команда должна начинать в объектной программе новую запись тела программы. Вам следует тщательно изучить оставшуюся часть объектной программы на рис. 3.6 и разобраться с тем, как ассемблер генерирует разряды перемещения и как они используются загрузчиком . Некоторые ЭВМ предоставляют аппаратные средства, облегчающие организацию перемещения программ. Например, в некоторых машинах все ссылки к оперативной памяти рассматриваются как относительные ссылки с базовым адресом, назначаемым пользователем. Преобразование этих относительных адресов в физические производится во время исполнения программы. (Мы обсудим этот процесс позднее - при разборе управления оперативной памятью в гл. 6.) Однако, как мы увидим в следующем разделе, загрузчик тем не менее должен выполнять некоторые функции перемещения подпрограмм для обеспечения их связывания. 3.2.2. Связывание программ С основными концепциями, затрагивающими связывание программ, мы познакомились в разд. 2.3.5. Прежде чем продолжить чтение, вам следует повторить материал этого раздела и еще раз посмотреть приведенные там примеры. В данном разделе мы обсудим более сложные примеры внешних ссылок между программами и изучим взаимосвязь между перемещением и связыванием. В следующем разделе дается описание алгоритма загрузчика, обеспечивающего связывание и перемещение программ. На рис. 2.15 в разд. 2.3.5 была показана программа, состоящая из трех управляющих секций. Эти управляющие секции могут ассемблироваться как вместе (т. е. за одно обращение к ассемблеру)так и независимо друг от друга. В любом случае после ассемблирования они будут представлены в виде отдельных сегментов объектного кода (рис. 2.17). Однако программист, естественно, склонен рассматривать программу в виде логического целого, включающего в себя все связанные управляющие секции. Однако с точки зрения загрузчика никакой программы нет вообще. Имеются только управляющие секции, которые должны связываться, перемещаться и загружаться. Загрузчик никоим образом не может узнать (да это ему и не нужно) о том, какие управляющие секции ассемблировались одновременно. Рассмотрим три (раздельно ассемблированные) программы, изображенные на рис. 3.7. Каждая из них состоит из единственной управляющей секции и содержит списки (LISTA, LISTB, LISTC). Концы этих списков помечены метками ENDA, ENDB, ENDC. Метки в начале и в конце списков являются внешними именами (т. е. они доступны для связывания). Обратите внимание, что каждая программа содержит один и тот же набор ссылок к этим внешним именам. Три из них - операнды команд (предложения с метками REF1-REF3), а остальные - ссылки на элементы данных длиной в одно слово (REF4 REF8). На данном примере мы посмотрим, в чем состоят различия в обработке этих идентичных выражений в указанных трех программах, и подчеркнем взаимосвязь между перемещением и связыванием. Для того чтобы сосредоточиться на данном вопросе, мы сознательно не стали приводить реальные программы. Те части программ, которые не используются в процессах перемещения и связывания, опущены. То же самое относится и к сгенерированному для этих программ объектному коду (см. рис. 3.8 ) Рассмотрим вначале предложение с меткой KEF1. Для первой программы (PROGA) REF1 является просто ссылкой на метку внутри данной программы. Она ассемблируется обычным образом как команда, использующая адресацию относительно счетчика команд. Никаких модификаций для перемещения или связывания не требуется. В PROGB, с другой стороны, тот же самый операнд ссылается на внешнее имя. Ассемблер использует для этой команды расширенный командный формат и заносит в адресное поле значение 000000. Соответствующая ей объектная программа (см. рис. 3.8) содержит запись-модификатор, в котором загрузчику предписывается добавить значение имени LISTA к данному адресному полю в момент связывания программы. Совершенно аналогично эта ссылка обрабатывается для PROGC. Похожим образом обрабатывается ссылка в команде с меткой REF2. В PROGA выражение, используемое в качестве операнда, представляет собой сумму внешней ссылки и константы. Ассемблер запоминает величину константы в адресном поле команды, а запись-модификатор предписывает загрузчику добавить к данному полю значение LISTB. В PROGB то же самое выражение является локальной ссылкой и поэтому обрабатывается обычным образом с использованием адресации относительно счетчика команд. Ни перемещения, ни связывания здесь не требуется. В команде с меткой REF3 используется непосредственный операнд, значением которого является разность между ENDA и LISTA (т.е. длина списка в байтах). В PROGA ассемблер располагает всей необходимой информацией для вычисления этого значения. Однако при ассемблировании PROGB (и PROGC) значения меток неизвестны. В этих программах выражение должно вычисляться как внешняя ссылка (с двумя записямимодификаторами) даже несмотря на то, что в конечном счете результат является абсолютной величиной, не зависящей от места загрузки программы. Адрес Исходное предложение Объектный код 0000 PROGA START 0 EXTDEF LISTA,ENDA EXTREF LISTB,ENDB,LISTC,ENDC . . . 0020 REF1 LDA LISTA 03201D 0023 REF2 +LDT LISTB+4 77100004 0027 REF3 LDX #ENDA-LISTA 050014 . . . 0040 LISTA EQU * . . 0054 ENDA EQU * 0054 REF4 WORD ENDA-LISTA+LISTC 000014 0057 REF5 WORD ENDC-LISTC-10 FFFFF6 005A REF6 WORD ENDC-LISTC+LISTA-1 00003F 005D REF7 WORD ENDA-LISTA-(ENDB-LISTB) 000014 0060 REF8 WORD LISTB-LISTA FFFFC0 END REF1 Адрес Исходное предложение Объектный код 0000 PROGB START 0 EXTDEF LISTB,ENDB EXTREF LISTA,ENDA,LISTC,ENDC . . . 0036 REF1 +LDA LISTA 03100000 003A REF2 LDT LISTB+4 772027 003D REF3 +LDX #ENDA-LISTA 05100000 . . . 0060 LISTB EQU * . . 0070 ENDB EQU * 0070 REF4 WORD 0073 REF5 WORD 0076 REF6 WORD 0079 REF7 WORD 007C REF8 WORD END ENDA-LISTA+LISTC 000000 ENDC-LISTC-10 FFFFF6 ENDC-LISTC+LISTA-1 FFFFFF ENDA-LISTA-(ENDB-LISTB) FFFFF0 LISTB-LISTA 000060 Адрес Исходное предложение Объектный код 0000 PROGC START 0 EXTDEF LISTC,ENDC EXTREF LISTA,ENDA,LISTB,ENDB . . . 0018 REF1 +LDA LISTA 03100000 001C REF2 +LDT LISTB+4 77100004 0020 REF3 +LDX #ENDA-LISTA 05100000 . . . 0030 LISTC EQU * . . 0042 ENDC EQU * 0042 REF4 WORD ENDA-LISTA+LISTC 000030 0045 REF5 WORD ENDC-LISTC-10 000008 0048 REF6 WORD ENDC-LISTC+LISTA-1 000011 004B REF7 WORD ENDA-LISTA-(ENDB-LISTB) 000000 004E REF8 WORD LISTB-LISTA 000000 END Рис.3.7. Модельная программа, иллюстрирующая связывание и перемещение. Оставшиеся ссылки иллюстрируют другие возможные варианты. Общий подход, принятый при ассемблировании, заключается в том, что ассемблер стремиться вычислить как можно большую часть выражения. Оставшиеся термы передаются загрузчику с помощью записей-модификаторов. Для того чтобы разобраться в этом, рассмотрим предложение с меткой REF4. В PROGA ассемблер может вычислить все выражение, за исключением значения LISTC. В результате имеем начальное значение 000014 (шестнадцатеричное) и одну запись-модификатор. Однако в PROGB то же самое выражение не содержит ни одного терма, который может быть вычислен ассемблером. Поэтому объектный код будет содержать начальное значение, равное 000000, и три записи-модификатора. В PROGC ассемблер знает относительное значение LISTC (но не фактический адрес, который неизвестен до тех пор, пока программа не будет загружена в память). Начальное значение данной константы будет содержать относительный адрес LISTC (шестнадцатеричная величина 000030). Записи-модификаторы содержат указания загрузчику прибавить начальный адрес программы (т.е. значение PROGC)прибавить значение ENDA и вычесть значение LISTA. Таким образом, выражение REF4 представляет собой простую внешнюю ссылку в PROGA, более сложную внешнюю ссылку в случае PROGB и комбинацию перемещения с внешней ссылкой в случае PROGC. Вам следует самостоятельно разобраться с оставшимися ссылками ( с REF5 по REF8) и убедиться, что вы правильно поняли, как были сгенерированы объектный код и записи-модификаторы, показанные на рис. 3.8. H_PROGA _000000_000063 D_LISTA _000040_ENDA _000054 R_LISTB _ENDB _LISTC _ENDC * * T_000020_0A_03201D_77100004_050014 * * T_000054_0F_000014_FFFFF6_00005F_000014_FFFFC0 M_000024_05_+LISTB M_000054_06_+LISTC M_000057_06_+ENDC M_000057_06_-LISTC M_00005A_06_+ENDC M_00005A_06_-LISTC M_00005A_06_+PROGA M_00005D_06_-ENDB M_00005D_06_+LISTB M_000060_06_+LISTB M_000060_06_-PROGA E_000020 H_PROGB _000000_00007F D_LISTB _000060_ENDB _000070 R_LISTA _ENDA _LISTC _ENDC * * T_000036_0B_03100000_772027_05100000 * * T_000070_0F_000000_FFFFF6_FFFFFF_FFFFF0_000060 M_000037_05_+LISTA M_00003E_05_+ENDA M_00003E_05_-LISTA M_000070_06_+ENDA M_000070_06_-LISTA M_000070_06_+LISTC M_000073_06_+ENDC M_000073_06_-LISTC M_000076_06_+ENDC M_000076_06_-LISTC M_000076_06_+LISTA M_000079_06_+ENDA M_000079_06_-LISTA M_00007C_06_+PROGB M_00007C_06_-LISTA E H_PROGС _000000_000051 D_LISTС _000030_ENDС _000042 R_LISTA _ENDA _LISTB _ENDB * * T_000018_0C_03100000_77100004_05100000 * * T_000042_0F_000030_000008_000011_000000_000000 M_000019_05_+LISTA M_00001D_05_+LISTB M_000021_05_+ENDA M_000021_05_-LISTA M_000042_06_+ENDA M_000042_06_-LISTA M_000042_06_+PROGC M_000048_06_+LISTA M_00004B_06_+ENDA M_00004B_06_-LISTA M_00004B_06_-ENDB M_00004B_06_+LISTB M_00004E_06_+LISTB M_00004E_06_-LISTA E Рис.3.8. Объектная программа, соответствующая рис.3.7. На рис. 3.9а приведены три рассмотренные нами программы после загрузки и связывания. Программа PROGA была загружена, начиная с адреса 4000. Непосредственно после нее были загружены PROGB и PROGC. Заметим, что в результате (после перемещения связывания) каждое выражение с REF4 по REF8 будет иметь одно и то же значение во всех трех программах. Так и должно было быть, поскольку в исходных программах они имели один и тот же вид. Рассмотрим, например, значение метки REF4 в PROGA. Она расположена по адресу 4054 (этот адрес Адрес Содержимое 0000 хххххххх 3FF0 xxxxxxxx 4000 .....……... 4010 .....……... 4020 03201D77 4030 .....……... 4040 .....……... 4050 ......…….. 4060 000083 ... 4070 .....……... 4080 .....……... 4090 .....……... 40A0 05100014 40B0 .....……... 40C0 .....……... 40D0 …........00 40E0 0083 .….. 40F0 ……........ 4100 40C70510 4110 .....……... 4120 ……........ 4130 000083 хх 4140 хххххххх хххххххх xxxxxxxx ..……...... ..……...... 1040C705 ..……...... ..……...... 00412600 ..……...... ..……...... ..……...... ..……...... .....……... ..……...... ..……...... 41260000 .....……... ...……..... 0014.....… ..……...... 00412600 хххххххх хххххххх хххххххх хххххххх xxxxxxxx xxxxxxxx ……........ ..……...... ……........ ..……...... 0014..….. ....……… ……........ ..……...... ……........ ..……...... 00080040 51000004 ……........ ..……...... ……........ ..……...... ……........ ..……...... ....031040 40772027 ..……...... ……........ ……........ ..……...... ……........ ..……...... 08004051 00000400 ..……...... ……........ ..…..0310 40407710 ..……...... ……........ ……........ ..……...... 00080040 51000004 хххххххх хххххххх хххххххх хххххххх PROGA PROGB PROGC a Объектная программа PROGA HPROGA (REF4) Содержимое памяти 0000 (REF4) T000054 0F 000014 + 4050 004126 M 00005406+LISTC PROGC HPROGC + 4112 DLISTC 000030 (Фактический адрес LISTC) PROGA 004000 PROGB 004063 PROGC 0040E2 б Рис.3.9а. Программа рис. 3.8 после связывания и загрузки. 3.9б. Выполнение операций связывания и перемещения для предложения REF4 из PROGA. вычисляется как сумма начального адреса PROGA и относительного адреса REF4, равного 0054). На рис. 3.9б детально показан процесс вычисления этого значения. Начальное значение (полученное из записи тела программы) равно 000014. К нему прибавляется адрес, назначенный метке LISTC и равный 4112 (начальный адрес PROGC плюс 30). В PROGB значение REF4 расположено по относительному адресу 70 (фактический адрес 4003). К начальному значению (000000) загрузчик прибавляет значения меток ENDA (4054) и LISTC (4112), а затем вычитает значение метки LISTA (4040). В результате будет получено значение 004126, т. е. точно такое же, как и в PROGA. Значение REF4 в PROGC вычисляется аналогично и дает тот же самый результат. Те же самые рассуждения справедливы и для всех других меток с REF5 по REF8. Если одно и то же выражение используется в качестве операнда в различных командах, то после загрузки адресные поля этих команд необязательно должны иметь одинаковые значения. Это происходит потому, что при вычислении адреса операнда для команд, использующих адресацию относительно счетчика команд (или базы), выполняется еще один дополнительный шаг. В данном случае важно, чтобы совпадали целевые адреса команд. Например, в PROGA команда с меткой REF1 использует адресацию относительно счетчика команд. Значение смещения для данной команды равно 010. В момент исполнения этой команды в счетчике команд будет находиться величина 4023 (фактический адрес следующей команды). В результате будет получен целевой адрес 4040. Данная команда не требует какой-либо модификации при перемещении программы, так как счетчик команд всегда будет содержать фактический (а не относительный) адрес следующей команды. Мы можем рассматривать данный процесс как автоматическое выполнение требуемого перемещения в момент исполнения программы путем вычисления целевого адреса. С другой стороны, в программе PROGB команда с меткой REF1 использует расширенный командный формат, в котором фактический адрес задается непосредственно. После связывания значение данного адреса будет равно 4040, т. е. будет совпадать со значением целевого адреса соответствующей команды PROGA. Вам следует тщательно разобраться с остальными предложениями и убедиться, что целевые адреса (в случае REF2 и REF3) и значения констант (для REF5-REF8) одни и те же в каждой из трех программ. Сейчас вас не должно беспокоить, как загрузчик реально выполняет данные вычисления, поскольку необходимые для этого алгоритмы и структуры данных будут обсуждаться в следующем разделе. Важно, однако, чтобы вы поняли, какие именно вычисления надо делать, и могли выполнить их вручную (следуя при этом инструкциям, содержащимся в объектных программах). 3.2.3. Таблицы и алгоритмы связывающего загрузчика Теперь мы готовы к тому, чтобы рассмотреть алгоритм работы связывающего (и перемещающего) загрузчика. Мы будем использовать записи-модификаторы таким образом, чтобы функции связывания и перемещения выполнялись с помощью одного и того же механизма. Как уже упоминалось, данный тип загрузчиков часто применяется для машин (подобных УУМ/ДС)которые используют относительную адресацию и тем самым делают перемещение большинства адресов ненужным. Алгоритм связывающего загрузчика значительно более сложен, чем алгоритм абсолютного загрузчика, который мы рассматривали в разд. 3.1. Входная информация для этого загрузчика состоит из набора объектных программ (т. е. управляющих секций), которые должны быть связаны друг с другом. В управляющих секциях могут использоваться (и это обычная ситуация) внешние ссылки на имена, значения которых во входном потоке еще не были определены. В этом случае требуемое связывание не может быть выполнено до тех пор, пока не будут назначены адреса для всех требуемых имен (т. е. до тех пор, пока не будет прочитана требуемая управляющая секция). Поэтому связывающий загрузчик обычно выполняет два просмотра входного потока точно так же, как это делает ассемблер. В общих чертах эти два просмотра, выполняемые связывающим загрузчиком, имеют много общего с двумя просмотрами, выполняемыми ассемблером: во время первого просмотра назначаются адреса для всех внешних ссылок, а во время второго выполняются фактическая загрузка, перемещение и связывание. Основная структура данных, необходимая для связывающего загрузчика, - это таблица внешних имен (ESTAB). Данная таблица является аналогом SYMTAB ассемблера и используется для хранения имен и адресов всех внешних ссылок для всего набора управляющих секций, загружаемых совместно. Очень часто в этой таблице также запоминается информация о том, какая управляющая секция содержит определение имени. Обычно ESTAB организуется в виде хеш-таблицы. Двумя другими важными переменными являются PROGADDR (адрес загрузки программы) и CSADDR (адрес управляющей секции). Переменная PROGADDR - это адрес начала программы в оперативной памяти, куда должна загружаться связываемая программа. Этот адрес загрузчик получает от операционной системы. (В гл. 6 мы обсудим, как операционная система может определить PROGADDR.) Переменная CSADDR содержит начальный адрес той управляющей секции, которая обрабатывается загрузчиком в данный момент. Этот адрес добавляется ко всем относительным адресам данной управляющей секции для того, чтобы преобразовать их в фактические адреса. Pass 1: begin получить PROGADDR от операционной системы занести в CSADDR значение PROGADDR {для первой управляющей секции} while не конец входного потока do begin прочитать следующую входную запись {запись- заголовок управляющей секции} занести в CSLTH длину управляющей секции поиск имен управляющей секции в ESTAB if нашли then занести признак ошибки ( дважды определенное внешнее имя ) else занести имя управляющей секции в ESTAB со значением CSADDR while тип записи ( ) "Е" do begin прочитать следующую входную запись if тип записи = "D" then for для каждого имени записи do begin поиск имени в ESTAB if нашли then занести признак ошибки ( дважды определенное внешнее имя ) else занести имя в ESTAB со значением ( CSADDR + указанный адрес ) end { for } end { while ( ) "E" } прибавить CSLTH к CSADDR { начальный адрес следующей управляющей секции } end { while не конец } end { Pass 1 } Рис.3.10а. Алгоритм первого просмотра связывающего загрузчика. Сам алгоритм показан на рис.3.10а и 3.10б. В процессе обсуждения этого алгоритма вам, возможно, будет полезно вспомнить примеры загрузки и связывания, рассмотренные в предыдущем разделе (рис. 3.8 и 3.9). Pass 2: begin занести в CSADDR значение PROGADDR занести в EXECADDR значение PROGADDR while не конец входного потока do begin прочитать следующую входную запись { запись- заголовок } занести в CSLTH длину управляющей секции while тип записи ( ) "E" do begin прочитать следующую входную запись if тип записи = "T" then begin { если объектный код представлен в символьном виде, то преобразовать его во внутреннее представление } занести объектный код записи по адресу ( CSADDR + указанный адрес ) end { if "T" } else if тип записи = "М" then begin поиск модифицируемого имени в ESTAB if нашли then прибавить или вычесть значение имени к адресу ( CSADDR + указанный адрес ) else занести признак ошибки ( неопределенное внешнее имя ) end { if "M" } end { while ( ) "E" } if адрес определен { в записе-конец } then занести в EXECADDR значение ( CSADDR + указанный адрес ) прибавить CSLTH к CSADDR end { while не конец } перейти по адресу, заданному в EXECADDR { начать исполнение программы } end { Pass2 } Рис.3.10б. Алгоритм второго просмотра связывающего загрузчика. Во время первого просмотра (рис.3.10а) загрузчик обрабатывает только записьзаголовок и записи-определения управляющих секций. Начальный адрес загрузки связываемой программы (PROGADDR) загрузчик получает от операционной системы. Этот адрес становится начальным адресом (CSADDR) первой управляющей секции входного потока. Имя управляющей секции, полученное из записи-заголовка, записывается в ESTAB и ему присваивается текущее значение CSADDR. Все внешние имена из записей-определителей также заносятся в ESTAB. Значение их адресов получаются путем сложения значения из записи-определения с CSDDR. После того как будет прочитана запись-конец, к CSADDR добавляется длина управляющей секции (CSLTH). (Длина была получена из записи-заголовка.). Таким образом, мы получаем начальный адрес для следующей управляющей секции. После завершения первого просмотра ESTAB содержит все внешние имена, определенные в данном наборе управляющих секций, вместе с назначенными им адресами. Многие загрузчики могут по желанию пользователя выдавать на печать таблицу загрузки, в которой показаны внешние имена и их адреса. Эта информация часто используется для отладки программ. Для примеров, приведенных на рис. 3.8 и 3.9, такая таблица загрузки может выглядеть следующим образом: Управляющая секция Имя Адрес Длина __________________________________________________________ PROGA 4000 0063 LISTA 4040 ENDA 4054 PROGB 4063 007F LISTB 40СЗ ENDB 40D3 PROGC 40E2 0051 LISTC 4112 ENDC 4124 __________________________________________________________ По существу это та же информация, которая содержится в ESTAB в конце первого просмотра. Собственно загрузка, перемещение и связывание программы осуществляются во время второго просмотра (рис.3.10б). Переменная CSADDR используется так же, как и во время первого просмотра. Она всегда содержит фактический начальный адрес загружаемой в данный момент управляющей секции. После того, как считана очередная запись тела программы, содержащийся в ней объектный код помещается по указанному адресу (плюс текущее значение CSADDR). Когда встречается запись-модификатор, то имя, требуемое для модификации, ищется в ESTAB и затем его значение прибавляется к заданному адресу или вычитается из него. Завершает свою работу загрузчик обычно передачей управления на загруженную программу. (В некоторых системах начальный адрес программы просто возвращается операционной системе и пользователь должен затем выполнить отдельную команду для начала выполнения программы.) Запись-конец каждой управляющей секции может содержать адрес первой команды данной секции, с которой должно начинаться ее исполнение. Наш загрузчик рассматривает этот адрес как адрес передачи управления для исполнения программы. Если адрес передачи управления задан более чем в одной управляющей секции, то загрузчик использует последний встретившийся. Если ни одна из управляющих секций не содержит адрес передачи управления, то загрузчик использует в качестве адреса передачи управления начальный адрес программы (т. е. PROGADDR). Это соглашение типично для большинства связывающих загрузчиков. Обычно адрес передачи управления должен помещаться только в записи-конце главной программно не в подпрограммах. В этом случае стартовый адрес будет правильно определяться вне зависимости от порядка следования управляющих секций. (см. пример на рис. 2.17.) Вам следует применить данный алгоритм (вручную) для загрузки и связывания объектной программы, изображенной на распечатке 13. Если для PROGADDR использовать 4000, то полученный результат; должен быть таким, как показано на рис. 3.9. Можно несколько повысить эффективность данного алгоритма, если слегка изменить формат объектной программы. Суть этих изменений заключается в том, что каждой внешней ссылке присваивается специальный ссылочный номер. Эти ссылочные номера используются в записях-модификатоpax вместо имен. Предположим, что имя управляющей секции всегда имеет ссылочный номер 01. Ссылочные номера остальных внешних ссылок определены в записи-ссылке объектной программы данной управляющей секции. На рис.3.11 показано, как будет выглядеть объектная программа, изображенная на рис.3.8, после внесения в нее указанных изменений. Для удобства чтения объектной программы ссылочные номера подчеркнуты. Описанный здесь механизм часто используется в реальных загрузчиках, и это была одна из причин, почему мы включили наше объектное представление записи-ссылки. Вы могли заметить, что в алгоритме на рис. 3.10 эти записи не использовались. H_PROGA _000000_000063 D_LISTA _000040_ENDA _000054 R_02LISTB _03ENDB _04LISTC _05ENDC * * T_000020_0A_03201D_77100004_050014 * * T_000054_0F_000014_FFFFF6_00003F_000014_FFFFC0 M_000024_05_+02 M_000054_06_+04 M_000057_06_+05 M_000057_06_-04 M_00005A_06_+05 M_00005A_06_-04 M_00005A_06_+01 M_00005D_06_-03 M_00005D_06_+02 M_000060_06_+02 M_000060_06_-01 E_000020 H_PROGB _000000_00007F D_LISTB _000060_ENDB _000070 R_02LISTA _03ENDA _04LISTC _05ENDC * * T_000036_0B_03100000_772027_05100000 * * T_000070_0F_000000_FFFFF6_FFFFFF_FFFFF0_000060 M_000037_05_+02 M_00003E_05_+03 M_00003E_05_-02 M_000070_06_+03 M_000070_06_-02 M_000070_06_+04 M_000073_06_+05 M_000073_06_-04 M_000076_06_+05 M_000076_06_-04 M_000076_06_+02 M_000079_06_+03 M_000079_06_-02 M_00007C_06_+01 M_00007C_06_-02 E H_PROGС _000000_000051 D_LISTС _000030_ENDС _000042 R_02LISTA _03ENDA _04LISTB _05ENDB * * T_000018_0C_03100000_77100004_05100000 * * T_000042_0F_000030_000008_000011_000000_000000 M_000019_05_+02 M_00001D_05_+04 M_000021_05_+03 M_000021_05_-02 M_000042_06_+03 M_000042_06_-02 M_000042_06_+01 M_000048_06_+02 M_00004B_06_+03 M_00004B_06_-02 M_00004B_06_-05 M_00004B_06_+04 M_00004E_06_+04 M_00004E_06_-02 E Рис.3.11. Объектная программа, соответствующая рис.3.7, в которой используются ссылочные номера для модификации кодов. ( Для удобства чтения ссылочные номера подчеркнуты.) Основное преимущество использования ссылочных номеров заключается в том, что в этом случае отпадает надобность в многократном просмотре ESTAB для одних и тех же имен. Все внешние ссылки, которые используются в данной управляющей секции, могут быть найдены в ESTAB один раз. Затем, когда это потребуется, значение внешней ссылки можно получить по ее индексу в массиве ссылок. Вам предлагается разработать алгоритм, в котором используется такой механизм, и подумать, какие для этого потребуются дополнительные структуры данных. 3.3. Машинно-независимые свойства загрузчиков В этом разделе мы обсудим некоторые свойства загрузчиков, которые непосредственно не связаны со структурой машины. Загрузка и связывание часто рассматриваются как сервисные функции операционной системы. Взаимодействие программиста с загрузчиком менее тесное, чем, скажем, его взаимодействие с ассемблером во время создания программы. Поэтому большинство загрузчиков имеет лишь незначительные особенности (и меньше различается по своим возможностям, чем ассемблеры) . В разд. 3.3.1 мы обсудим использование автоматического поиска программ в библиотеках для разрешения внешних ссылок. Это средство позволяет программисту использовать стандартные подпрограммы, не включая их явным образом в загружаемую программу. Подключение библиотечных подпрограмм осуществляется автоматически во время связывания. В разд. 3.3.2 рассматриваются некоторые наиболее общеупотребительные дополнительные средства, которые программист может использовать по своему усмотрению во время загрузки и связывания программ. Эти средства включают возможность задания альтернативного источника входной информации, изменение или уничтожение внешних ссылок, а также управление автоматической обработкой внешних ссылок. В разд. 3.3.3 мы более детально изучим концепцию оверлейной загрузки программ, с которой мы познакомились в разд. 2.4.1. Мы обсудим способы задания оверлейной структуры и рассмотрим, как она может быть реализована в процессе выполнения программы. 3.3.1. Автоматический поиск в библиотеках Многие связывающие загрузчики допускают автоматическое включение библиотечных подпрограмм в загружаемую программу. В большинстве случаев для этой цели используется некоторая стандартная библиотека. Другие библиотеки могут подключаться с помощью специальных управляющих директив или передаваться загрузчику в качестве параметров. Этот механизм позволяет программисту использовать подпрограммы из одной или нескольких библиотек (например, математические или статистические подпрограммы) почти так же, как если бы они были составной частью языка программирования. Подпрограмма, вызываемая из загружаемой программы пользователя, автоматически выбирается из библиотеки, связывается с главной программой и загружается в оперативную память. От программиста не требуется ничего, кроме как перечислить имена используемых подпрограмм в исходной программе в списке внешних имен. В некоторых системах данный механизм называется автоматическим библиотечным вызовом (automatic library call). Мы используем термин библиотечный поиск (library search) для того, чтобы не путать его со средствами вызова, которые имеются в большинстве языков программирования. Связывающие загрузчики, поддерживающие библиотечный поиск, должны иметь список всех внешних имен, на которые есть ссылки, но которые не были определены во входном потоке загрузчика. Проще всего это можно сделать, занося каждое имя, содержащееся в записях-определениях, в таблицу внешних имен (ESTAB) еще до того, как оно будет фактически определено. Такие элементы таблицы помечаются специальным маркером, показывающим, что данное внешнее имя еще не определено. Когда встретится определение имени, то соответствующий ему адрес заносится в таблицу и имя становится полностью определенным. В конце первого просмотра все неопределенные имена, оставшиеся в ESTAB, представляют собой неразрешенные внешние ссылки. Для их разрешения загрузчик должен просмотреть все заданные библиотеки и обработать найденные в них подпрограммы точно так же, как если бы они являлись частью входного потока. Заметим, что библиотечные подпрограммы, в свою очередь, могут иметь внешние ссылки. Поэтому поиск в библиотеках надо проводить до тех пор, пока не будут разрешены все внешние ссылки (или пока дальнейшее разрешение ссылок станет невозможным). Если после завершения поиска в библиотеках останутся неразрешенные внешние ссылки, то их следует рассматривать как ошибки. Описанный процесс позволяет программисту замещать стандартные библиотечные подпрограммы своими собственными подпрограммами. Предположим, например, что главная программа ссылается на стандартную подпрограмму SQRT. Обычно подпрограмма с этим именем выбирается из библиотеки и подключается автоматически. Если программист по каким-то соображениям захочет использовать другую версию подпрограммы SQRT, то он может просто включить ее во входной поток. После завершения первого просмотра загрузчика ссылка на SQRT будет определена, и поэтому ее не потребуется искать в библиотеках. Загрузчик обычно работает с библиотеками, которые содержат ассемблированные или откомпилированные версии подпрограмм (т. е. объектные программы). Просмотр библиотек можно осуществлять с помощью сканирования записей-определений всех объектных программ библиотеки, но это крайне неэффективно. Обычно для библиотек используется специальная файловая структура. Она содержит каталог (директорий), в котором перечислены имена всех подпрограмм и имеются указатели на их месторасположения в файле. Если подпрограмма допускает вызов по нескольким именам (используя различные входные точки), то все ее альтернативные имена также присутствуют в каталоге. Конечно, сама объектная программа хранится в единственном экземпляре, а все альтернативные имена указывают на одну и ту же копию программы. Таким образом, фактически библиотечный поиск сводится к просмотру каталога и последующему чтению найденной объектной программы. Некоторые операционные системы могут постоянно хранить каталоги наиболее часто используемых библиотек в оперативной памяти. Это позволяет ускорить процесс поиска, если требуется обработать большое количество внешних ссылок. Мы рассмотрели библиотечный поиск применительно к разрешению ссылок на подпрограммы. Очевидно, что те же технические приемы могут быть одинаково хорошо применимы для разрешения любых других типов внешних ссылок. 3.3.2. Управление процессом загрузки Многие загрузчики допускают задание управляющих параметров, которые позволяют изменять стандартный процесс, описанный в разд. 3.2. В этом разделе мы остановимся на некоторых типичных вариантах управления работой загрузчика и рассмотрим примеры их использования. Для задания управляющих параметров многие загрузчики используют специальный командный язык. Иногда для задания предложений командного языка используется отдельный входной файл. В других случаях эти предложения могут вставляться в основной входной поток между объектными программами. Отдельные системы даже позволяют включать команды управления загрузчиком в исходную программу, а ассемблер или компилятор переносит их в объектную программу. В этом разделе мы будем считать, что параметры, управляющие работой загрузчика, задаются с помощью командного языка, хотя существуют и другие способы их задания. В некоторых системах команды управления загрузчиком являются составной частью языка управления заданиями, и поэтому они обрабатываются операционной системой. В этом случае операционная система собирает все параметры в управляющий блок, который становится доступным загрузчику после начала его работы. Собственно реализация тех или иных возможностей, предоставляемых загрузчиком, конечно, остается той же самой, за исключением самих средств задания управляющих параметров. Одной из типичных возможностей, предоставляемых пользователю, является определение альтернативного источника входного потока. Например, команда INCLUDE имя - программы (имя - библиотеки) может предписывать загрузчику прочитать определенную программу из библиотеки и работать с ней так, как если бы она входила в состав исходного входного потока загрузчика. Другие команды дают пользователю возможность исключить некоторые внешние имена или целые управляющие секции. Кроме того, может разрешаться изменять внешние ссылки во время загрузки и связывания программ. Например, команда DELETE имя - секции может предписывать загрузчику исключить из рассмотрения указанную управляющую секцию входного потока, а команда CHANGE имя1,имя2 может использоваться для замены внешнего имени имя1 на имя2. Ниже приводится пример использования этих команд. Рассмотрим исходную программу.изображеннуюнарис.2.15, и соответствующую ей объектную программу на рис. 2.17. Имеется главная программа (COPY), которая использует две подпрограммы (RDREC и WRREC) . Каждая из программ оформлена в виде отдельной управляющей секции. Если RDREC и WRREC разработаны только для использования в программе COPY, то весьма вероятно, что все три управляющие секции будут ассемблироваться одновременно. Это означает, что все три управляющие секции будут расположены в одном файле или будут включены в одну и ту же библиотеку). Предположим теперь, что в вычислительной системе появился набор сервисных подпрограмм. Две из них, READ и WRITE, предназначены для выполнения тех же самых функций, что и RDREC и WI*REC. Вероятно, желательно будет изменить неходкую программу COPY так, чтобы она могла использовать эти сервисные подпрограммы. В качестве временной меры можно использовать последовательность команд загрузчика, которые позволяют сделать эти изменения без переассемблирования программы. Например, это может быть полезно для того, чтобы протестировать сервисные подпрограммы прежде, чем делать окончательное преобразование COPY. Предположим, что в файл, в котором содержатся объектные программы, показанные на рис. 2.17, включены дополнительно следующие команды загрузчика: INCLUDE READ(UTLIB) INCLUDE WRITE(UTLIB) DELETE RDREC,WRREC CHANGE RDREC,READ CHANGE WRREC,WRITE Эти команды предписывают загрузчику включить в рассмотрение управляющие секции READ и WRITE из библиотеки UTLIB и исключить из рассмотрения управляющие секции RDREC и WRREC. Первая команда CHANGE служит для замены всех ссылок к RDREC на READ. Аналогично ссылки к WRREC заменяются на WRITE. Результат будет в точности таким, как если бы исходная программа COPY была модифицирована для использования READ и WRITE. Вам предлагается самостоятельно подумать над тем, как загрузчик может осуществить обработку команд, обеспечивающих описанные действия. Другая общеупотребительная возможность, предоставляемая загрузчиком, касается автоматического библиотечного поиска (как это было описано в предыдущем разделе). Большинство загрузчиков разрешает пользователю задавать дополнительные библиотеки с помощью команд типа LIBRARY MYLIB Такие пользовательские библиотеки обычно просматриваются раньше стандартных системных библиотек, что позволяет использовать в программе специальные версии стандартных подпрограмм. Загрузчики, выполняющие для разрешения внешних ссылок автоматический библиотечный поиск, часто дают пользователю возможность указать, что некоторые ссылки надо оставить неразрешенными. Предположим, например, что основной функцией некоторой программы являются сбор и запоминание данных. Кроме того, с помощью подпрограмм STDDEV, PLOT и CORREL, которые расположены в библиотеке математической статистики, она может проводить анализ данных. Пользователь может потребовать проведение такого анализа в процессе выполнения программы. Поскольку программа содержит ссылки на эти подпрограммы, то в общем случае они были бы загружены в оперативную память и связаны с главной программ мой. Если известно, что в данном конкретном запуске программ мы выполнение статистического анализа не потребуется, то пользователь может включить в свое задание команду типа NOCALL STDDEV,PLOT,CORREL которая предписывает загрузчику оставить соответствующие внешние ссылки неразрешенными. Это позволяет избежать накладных расходов на загрузку и связывание ненужных подпрограмм и экономит место в оперативной памяти. Кроме того, пользователь может вообще отказаться от автоматического библиотечного поиска. Естественно, что если во время исполнения программа попытается воспользоваться неразрешенными ссылками, то это приведет к ошибке. Данный режим бывает полезен тогда, когда требуется осуществить только связывание программы без ее немедленного выполнения. В этом случае часто бывает желательно отложить разрешение внешних ссылок на более позднее время. В разд. 3.4.1 мы рассмотрим редактор внешних связей, который выполняет эту функцию. Другим общеупотребительным средством является управдел кие информацией, выводимой загрузчиком на печать. В разд. 3.2.3 мы привели пример таблицы загрузки, которая может создаваться в процессе работы загрузчика. С помощью команд пользователь может указать, нужна ему распечатка данной таблицы или нет. Если таблица нужна, то можно указать уровень ее детализации. Например, таблица может включать только имена управляющих секций и назначенные им адреса. Кроме того, она может включать адреса всех внешних имен и даже таблицу перекрестных ссылок, в которой показаны все ссылки на каждое внешнее имя. Очень часто загрузчики предоставляют и другие дополним тельные возможности. Одна из них - определение стартовой точки программы (она заменяет любую информацию о стартовой точке, заданную в объектной программе). Другая - разрешение или запрещение выполнения программы, если в процессе ее загрузки были обнаружены ошибки (например, неразрешенные внешние ссылки). 3.3.3. Оверлейная структура программ В разд. 2.4.1 мы обсуждали реализацию двухпросмотрового ассемблера и простой оверлейной структурой. Такой способ реализации позволяет сократить суммарный объем оперативной памяти, требуемой для ассемблирования, так как для первого и второго просмотров используется одно и то же пространство. В этом разделе мы рассмотрим более сложный пример и обсудим, как осуществляется управление самим оверлейным процессом. В качестве введения в данную тему вам следует обратиться к рис. 2.18 и 2.19 и материалу разд. 2.4.1. На рис. 3.12а показана программа с оверлейной структурой, которую мы будем использовать в качестве примера. Буквами обозначены имена управляющих секций, а линиями - связи между ними по передаче управления. Таким образом, корневая управляющая секция (соответствующая ведущей подпрограмме на рис.2.18) имеет имя А. Управляющая секция А может вызывать секции В, С или D/E; секция В может вызывать F/G или Н и т. д. Обозначение D/E используется для спецификации двух управляющих секций (D и Е), которые тесно связаны между собой и всегда используются совместно. Хотя эти управляющие секции раздельно обрабатываются ассемблером и загрузчиком, однако в оверлейной структуре они рассматриваются как одно целое. Например, D может содержать исполняемые команды, в то время как Е - связанные с ними данные. Другой пример - Е может содержать часто используемые в D подпрограммы. На рис. 3.12в показана длина (в шестнадцатеричном виде) каждой управляющей секции. A B F/G C H D/E I J K a Управляющая секция Длина (в байтах) A B C D E F G H I J K 1000 1800 4000 2800 800 1000 400 800 1000 800 2000 б Рис.3.12. Пример оверлейной программы. Большинство систем, поддерживающих оверлейные программы, требует, чтобы они имели древовидную структуру (т. е. такую, как на рис. 3.12а). Узлы такого дерева называются сегментами. Корневой сегмент загружается в самом начале исполнения программы и остается в памяти до окончания счета. Остальные сегменты загружаются тогда, когда к ним происходит обращение. Предположим, например, что управляющая секция А вызывает В. Этот вызов приведет к автоматической загрузке сегмента, содержащего В (если только он уже не находится в оперативной памяти), *ели позднее А вызовет D, то будет загружен сегмент, содержащий секцию D. Порядок расположения сегментов, находящихся на одном уровне сверленной структуры, значения не имеет. Например, А может сначала вызвать С, затем В, затем D/E, потом вновь С и т. д. Предположим, что в некоторый момент времени исполняется управляющая секция Н. Она была вызвана секцией В, которую в свою очередь вызвала секция А. Таким образом, эти три сегмента являются активными и должны находиться в оперативной памяти. Другие сегменты не могут быть активными, поскольку нет путей, связывающих их с Н. Например, если ранее был вызван сегмент, содержащий К, то он должен был вернуть управление D/E (и затем А) прежде, чем В мог быть вызван из секции А. Данные рассуждения приводят нас к правилу, которое используется в большинстве систем, допускающих оверлейные структуры: если сегмент S находится в оперативной памяти, то все другие сегменты, расположенные на пути, ведущем из S к корню, также должны находиться в оперативной памяти. Соблюдение этого правила упрощает организацию оверлея. Если некоторый сегмент вызывает сегмент, расположенный на более низком уровне древовидной структуры (т. е. находящийся дальше от корня), то может потребоваться загрузка этого сегмента в оперативную память. С другой стороны, если некоторый сегмент ссылается на сегмент, лежащий между ним и корнем, то этот сегмент уже находится в памяти. Поскольку сегменты одного уровня (например, В, С и D/E) могут вызываться только из сегмента вышестоящего уровня, то они не требуются одновременно. Поэтому они могут совместно использовать одну и ту же область оперативной памяти. Если передача управления вызывает загрузку некоторого сегмента, то он замещает в памяти сегмент, который находится на одном с ним уровне (и сегменты ему подчиненные). Таким образом, общий объем оперативной памяти, требуемой для исполнения программы, оказывается меньше. Это и является основной причиной использования оверлейных структур. Оверлейная структура программы описывается при помощи команд загрузчика, похожих на те, что мы рассматривали в предыдущем разделе. На рис. 3.13 приведен набор команд, который задает оверлейную структуру, изображенную на рис. 3.12а. Команда SEGMENT имя - сегмента (управляющая-секция...) определяет сегмент (т. е. узел древовидной структуры). Данная команда задает имя сегмента и описывает список входящих в него управляющих секций. Первый определяемый сегмент - это корень. Два последовательных предложения SEGMENT задают между описываемыми сегментами отношение вида отец - сын. Поэтому на рис. 3.13 первые три предложения описывают самый левый путь *от корня к F/G) структуры на рис. 3.12а. Остальные сегменты, имеющие общего отца, на рис. 3.13 задаются с помощью предложений другого типа. Предложение PARENT имя – сегмента описывает сегмент (ранее определенный), который является отцом следующего за ним сегмента. Таким образом, на рис. 3.13 первое предложение PARENT определяет, что SEG2 является отцом SEG4. Если бы этого предложения не было, то отцом SEG4 считался бы сегмент SEG3. Вам надлежит тщательно изучить остальные определения и убедиться, что они действительно описывают древовидную структуру, изображенную на рис. 3.12а. SEGMENT SEGMENT SEGMENT PARENT SEGMENT PARENT SEGMENT PARENT SEGMENT SEGMENT PARENT SEGMENT PARENT SEGMENT SEG1(A) SEG2(B) SEG3(F,G) SEG2 SEG4(H) SEG1 SEG5(C) SEG1 SEG6(D,E) SEG7(I) SEG6 SEG8(J) SEG6 SEG9(K) Рис.3.13. Определения оверлейной структуры с помощью управляющих предложений. После того, как мы задали оверлейную структуру, определение начальных адресов сегментов становится простым делом, так как каждый сегмент начинается сразу после своего отца. Правило, управляющее размещением сегментов в оперативной памяти, обеспечивает разрешение всех загруженных на данный момент сегментов в областях, смежных с областью, выделенной заданию в начале его работы. На рис. 3.14а показаны длина и относительный адрес начала каждого сегмента рассматриваемой на МП программы. На этом же рисунке показаны фактические начальные адреса (считается, что начальный адрес загрузки равен 8000). Во время выполнения программы в памяти могут присутствовать различные наборы сегментов. На рис. 3.146 показаны три возможных варианта. С остальными вам предлагается разобраться самостоятельно. Позднее мы увидим, что загрузчик может добавить одну или несколько специальных управляющих секций, содержащих информацию для управления оверлейной структурой. Естественно, что длина этих секций должна учитываться при назначении адресов. Как мы видели, загрузчик может назначить фактический адрес загрузки каждому сегменту оверлейной структуры после того, как будет определен начальный адрес. Таким образом, становятся известны адреса всех внешних имен. Это означает, что все операции перемещения и связывания могут выполняться обычным образом, но с одним исключением - отец должен иметь возможность передать управление сегменту, который в этот момент не находится в оперативной памяти. Загрузка корневого сегмента может производиться непосредственно в оперативную память. Остальные сегменты (для которых уже выполнены операции перемещения и связывания) записываются в специальный файл (SEGFILE), создаваемый загрузчиком. Собственно оверлейный процесс (т. е. загрузка сегмента, когда ему передается управление) может осуществляться различными способами. Здесь мы опишем простой механизм, который не требует вмешательства операционной системы. Наряду с ним могут использоваться средства, аналогичные аппарату динамического связывания (см. разд. 3.4.2). Начальный адрес Сегмент Относительный 1 2 3 4 5 6 7 8 9 Фактический Длина 0000 1000 2800 2800 1000 1000 4000 4000 4000 8000 9000 A800 A800 9000 9000 C000 C000 C000 1000 1800 1400 800 4000 3000 1000 800 2000 a 8000 A 9000 A000 A A B D H B000 C000 D000 E E000 б Рис.3.14. Распределение памяти для оверлейной программы. Фактическая загрузка сегментов в процессе исполнения программы осуществляется специальной программой - менеджером оверлея (overlay manager). В нашем примере менеджер оверлея расположен в отдельной управляющей секции с именем OVLMGR. Эта секция автоматически включается загрузчиком в корневой сегмент оверлейной программы. Для управления загрузкой сегментов OVLMGR должен иметь информацию об оверлейной структуре программы. Эта информация хранится в таблице сегментов (SEGTAB), которая создается загрузчиком и включается в корневой сегмент в виде отдельной управляющей секции. Таблица SEGTAB описывает древовидную структуру, определяя уровень расположения каждого из сегментов. Для каждого сегмента она также содержит его начальный адрес загрузки, адрес входной точки и месторасположение сегмента в SEGFILE. (В данной реализации мы полагаем, что сегмент может вызываться только по одной входной точке). Кроме того, для каждого сегмента, за исключением корня в SEGTAB, имеется область передачи управления. Она содержит команды, которые обеспечивают передачу управления на требуемый сегмент. Если сегмент уже загружен в оперативную память, то его область передачи управления содержит простую команду перехода на входную точку сегмента. Если сегмент не загружен, то область передачи управления содержит команды активизации OVLMGR и передачи ему информации, необходимой для загрузки сегмента. В процессе своей работы менеджер оверлея изменяет команды в области передачи управления так, чтобы они всегда соответствовали текущему статусу каждого из сегментов. Обращение одного сегмента к другому, стоящему на следующем уровне древовидной структуры (т. е. обращение, которое может потребовать оверлейную загрузку), преобразуется загрузчиком в обращение к области передачи управления соответствующего сегмента. Область передачи управления всегда присутствует в оперативной памяти, так как SEGTAB расположена в корневом сегменте. Затем команды области передачи управления либо непосредственно передают управление на требуемый сегмент, либо активизируют OVLMGR. В последнем случае OVLMGR загружает требуемый сегмент, обновляет SEGTAB и после этого передает управление сегменту. Иллюстрация описанного процесса дана на рис. 3.15. На рис. 3.156 показано содержимое оперативной памяти на некотором этапе исполнения программы. В данный момент загружены Сегменты 1, 2, 4 и исполняются команды управляющей секции А. Сегменты 2 и 4 были загружены в ответ на ранее выполненные команды вызова. Хотя сейчас управление вернулось Сегменту 1, однако Сегменты 2 и 4 продолжают оставаться в оперативной памяти до тех пор, пока их не заменят другие сегменты. Области передачи управления Сегментов 2 и 4 содержат команды перехода на управляющие секции В и Н. Остальные области передачи управления содержат команды активизации OVLMGR (соответствующие им поля SEGTAB заштрихованы). Если в управляющей секции А сейчас будет выполнена команда вызова секции В (в исходной программе команда +JSUB В), то в результате выполнится переход в область передачи управления Сегмента 2. Как показано на рисунке, это приведет к прямой передачи управления на секцию В. OVLMGR 2 3 4 5 6 7 8 9 SEGTAB Корневой сегмент (SEG1) +JSUB B A а B Сегмент#2 (SEG2) H Сегмент#4 (SEG4) OVLMGR 2 3 4 5 6 7 8 9 SEGTAB Корневой сегмент (SEG1) A +JSUB D B Сегмент#2 (SEG2) H Сегмент#4 (SEG4) б OVLMGR 2 3 4 5 6 7 8 9 SEGTAB Корневой сегмент (SEG1) A D E Сегмент#6 (SEG6) в Рис.3.15. Пример управления оверлеем. Предположим теперь, что А вызывает D. На рис. 3.15 показано, что область передачи управления, соответствующая Сегменту 6, содержит команды активизации OVLMGR. Менеджер оверлея загрузит Сегмент 6 из SEGFILE на выделенное ему место в оперативной памяти. Затем он обновит SEGTAB, указав, что в данный момент Сегмент 6 находится в памяти, а Сегменты 2 и 4 нет. Заметим, что Сегмент 4 должен быть удален из памяти (даже если занятое им пространство не требуется для нового сегмента), так как удален его отец. Теперь для вызова секции D остается передать управление на входную точку Сегмента 6 (см. рис. 3.15в). Возврат управления из вызванного сегмента (например, из D в А) может выполняться обычным образом. (Активизация OVLMGR для этого не требуется.) Например, команда JSUB (рис. 3.156) может занести адрес возврата в регистр L. Менеджер оверлея сохранит это значение. Таким образом, по завершению своей работы управляющая секция D может просто выполнить команду RSUB. Это тот же самый механизм, который используется для программ без оверлейной структуры. В некоторых системах функции OVLMGR выполняются специальным блоком операционной системы, который называется супервизором оверлея (overlay supervisor). В этом случае SEGTAB содержит макрокоманды операционной системы, обеспечивающие загрузку требуемого сегмента. 3.4. Варианты построения загрузчиков В данном разделе мы рассмотрим некоторые общие варианты реализации функций загрузчика, включая перемещение и связывание. Связывающие загрузчики, рассмотренные в разд. 3.2.3, выполняют связывание и перемещение в процессе загрузки. Здесь мы обсудим две альтернативные схемы-редакторы связей, выполняющие связывание перед загрузкой, и динамическое связывание, в котором функции связывания осуществляются во время исполнения программы. В разд. 3.4.1 рассматривается редактор связей, который во многих вычислительных системах используется вместо связывающего загрузчика или в качестве его дополнения. Редактор связей осуществляет связывание и некоторые действия по перемещению, однако подготовленная им программа загружается не в оперативную память, а записывается в файл или в библиотеку. Этот подход позволяет сократить накладные расходы на обработку программы, так как загрузчику остается выполнить лишь очень простые операции настройки относительных адресов. В разд. 3.4.2 мы познакомимся с механизмом динамического связывания. В этой схеме загрузка программ осуществляется в момент первого вызова, и для этого используются средства операционной системы. Откладывая связывание до момента вызова, мы можем получить дополнительную гибкость в организации программ. Однако обычно это ведет к дополнительным по сравнению со связывающим загрузчиком накладным расходам. В разд. 3.4.3 мы обсудим раскручивающие загрузчики (boot-strap loaders). Такие загрузчики могут использоваться для программ, выполняемых без операционной системы или системного загрузчика. Они также могут применяться для загрузки в оперативную память самой операционной системы или системного загрузчика. 3.4.1. Редакторы связей Основное различие между редактором связей и связывающим загрузчиком показано на рис. 3.16. Вначале исходный текст переводится ассемблером или компилятором в объектную программу (которая может состоять из нескольких управляющих секций). Связывающий загрузчик выполняет все операции связывания и перемещения, включая, если это необходимо, и автоматический библиотечный поиск, и загружает подготовленную для исполнения программу непосредственно в оперативную память. С другой стороны, редактор связей подготавливает вариант программы с разрешенными внешними связями (его часто называют загрузочным модулем или исполняемой программой) и записывает его в файл или в библиотеку. Объектные программы Библиотека Связывающий загрузчик Оператив ная память а Объектные программы Библиотека Редактор связей Связанная программа Перемещающий загрузчик Библиотека Оператив ная память б Рис.3.16. Обработка объектной программы с использованием связывающего загрузчика (а) и редактора связей (б). Для загрузки программы, подготовленной редактором связей, может быть использован простой перемещающий загрузчик. Ему остается только прибавить адрес начала загрузки ко всем относительным адресам внутри программы. Редактор связей выполняет размещение всех управляющих секций относительно начала программы. Таким образом, все поля, требующие модификации во время загрузки, получают значения относительно начала программы. Это означает, что загрузка может выполняться за один просмотр без использования таблицы внешних ссылок. По сравнению со связывающим загрузчиком такая загрузка требует намного меньших накладных расходов. Если программа должна выполняться много раз без переассемблирования,. то использование редактора связей позволяет существенно снизить затраты на разрешение внешних связей и библиотечный поиск, так как они в этом случае выполняются только один раз, в то время как связывающий загрузчик выполняет их при каждом запуске программы. Однако иногда программа переассемблируется практически при каждом новом запуске. Такая ситуация характерна при разработке и тестировании программ (например, обработка студенческих заданий). Кроме того, если программа используется довольно редко, то может оказаться нецелесообразным хранить ее ассемблированную версию в библиотеке. В этих случаях связывающий загрузчик является более эффективным, так как он позволяет обойтись без операций записи и чтения загрузочного модуля. Программа, создаваемая редактором связей, обычно записывается в форме, удобной для ее обработки перемещающим загрузчиком. В ней разрешены все внешние связи, а информация о перемещаемых адресах задается с помощью записеймодификаторов или масок перемещения. Несмотря на то что все операции связывания уже выполнены, в загрузочном модуле часто сохраняется информация о внешних ссылках. Это позволяет впоследствии проводить в загрузочном модуле операции по замене управляющих секций, изменению внешних связей и др. Если такую информацию не сохранять, то последующая обработка загрузочного модуля с помощью редактора связей будет невозможна. В этом случае единственное, что можно будет делать с программой, это загружать ее в память и исполнять. Если фактический адрес загрузки программы известен заранее, то редактор связей может выполнить всю необходимую настройку. В результате будет получено представление программы, которое в точности соответствует ее виду во время исполнения. Такая программа ничем не отличается от абсолютной объектной программы. Однако обычно возможность загружать программу в любое место памяти вполне компенсирует те расходы, которые требуются для выполнения настройки относительных адресов во время загрузки программы. Кроме подготовки объектной программы редактор связей может выполнять и много других полезных функций. Рассмотрим, например, программу (PLANNER), которая использует большое количество подпрограмм. Предположим, что в одну из этих подпрограмм (PROJECT). внесены изменения (например, для исправления ошибок или повышения эффективности). После ассемблирования или компилирования новой версии PROJECT мы должны включить ее в PLANNER. Редактор связей позволяет сделать это, не возвращаясь к исходным (раздельным) версиям всех программ. Ниже приведена последовательность команд редактора связей, которая позволяет выполнить эту работу. Используемый здесь язык аналогичен тому, что мы рассматривали в разд. 3.3.2: INCLUDE PLANNER(PROGLIB) DELETE PROJECT (Исключить из старой версии) INCLUDE PROJECT(NEWLIB) Включить новую версию) REPLACE PLANNER(PROGLIB) Редакторы связей могут также использоваться для построения пакетов из подпрограмм или управляющих секций, требующих совместного использования. Такая ситуация характерна для библиотек, обеспечивающих поддержку языков высокого уровня. Например, при реализации языка Фортран обычно использует большое количество подпрограмм, выполняющих форматный ввод-вывод. К ним относятся подпрограммы чтения и записи блоков данных, блокирования и разблокирования записей, а также подпрограммы кодирования и декодирования информации в соответствии с заданными спецификациями оператора FORMAT. Так как эти подпрограммы тесно увязаны между собой, то между ними имеется большое количество перекрестных ссылок. Вместе с тем для обеспечения модульности и облегчения сопровождения подпрограмм желательно, чтобы они были представлены в виде отдельных управляющих секций. Если бы программа, использующая форматный ввод-вывод, связывалась обычным образом, то все перекрестные ссылки между библиотечными подпрограммами выполнялись бы раздельно. Причем практически для любой Фортран-программы набор этих перекрестных ссылок был бы одним и тем же. Естественно, что это привело бы к значительным издержкам. Редактор связей позволяет решить эту проблему путем объединения некоторых подпрограмм в единый пакет. Необходимую для этого информацию можно задать, например, следующим образом: INCLUDE READR(FTNLIB) INCLUDE WRITER(FTNLIB) INCLUDE BLOCK(FTNLIB) INCLUDE DEBLOCK(FTNLIB) INCLUDE INCLUDE . . . SAVE ENCODE(FTNLIB) DECODE(FTNLIB) FTNIO(SUBLIB) В каталоге библиотеки SUBLIB подпрограммы, входящие в загрузочный модуль FTNIO, присутствуют под старыми именами. Поэтому если просматривать библиотеку SUBLIB раньше, чем FTNLIB, то будет найден модуль FTNIO, а не отдельные подпрограммы. Поскольку в FTNIO все внешние связи между подпрограммами уже разрешены, то, следовательно, они не будут обрабатываться при обработке пользовательской программы. В результате повысится эффективность выполнения операции связывания и будут значительно снижены суммарные затраты на работу системы. Редакторы связей часто разрешают пользователю указывать те внешние ссылки, которые не должны обрабатываться с помощью автоматического библиотечного поиска. Предположим, например, что в библиотеке должно храниться 100 Фортран-программ, в которых используются описанные выше подпрограммы ввода-вывода. Если все эти программы хранить с полностью разрешенными внешними связями, то в библиотеке будет записано 100 копий FTNIO. Если библиотечное пространство дорого, то это может оказаться крайне нежелательным. В этом случае пользователь может с помощью команд, похожих на те, что были рассмотрены в разд. 3.3.2, запретить выполнение библиотечного поиска во время работы редактора связей. Тогда будут разрешены только внешние связи между пользовательскими подпрограммами. Впоследствии во время исполнения программы для объединения пользовательской программы с модулем FTNIO можно будет использовать связывающий загрузчик. Поскольку в этом случае потребуется выполнить две отдельные операции связывания, то это приведет к некоторому увеличению накладных расходов, но зато позволит сэкономить библиотечное пространство. Редакторы связей часто обеспечивают выполнение и других операций и команд, подобных тем, что мы рассматривали для связывающих загрузчиков. По сравнению со связывающими загрузчиками редакторы связей в общем случае предоставляют большую гибкость и разнообразие средств управления загрузкой. Однако за это приходится расплачиваться ростом сложности программного обеспечения и увеличением накладных расходов. 3.4.2. Динамическое связывание Редакторы связей выполняют операции связывания до того, как программа загружается для исполнения. Связывающий загрузчик выполняет эти же операции во время загрузки программы. Здесь мы рассмотрим схему загрузки, в которой связывание откладывается до момента исполнения программы. В этом случае загрузка и связывание подпрограммы осуществляются тогда, когда к ней происходит первое обращение. Такая организация процесса загрузки обычно называется динамическим связыванием (dynamic linking) или динамической загрузкой (dynamic loading). По сравнению с ранее обсуждавшимися схемами обработки внешних связей динамическое связывание имеет ряд преимуществ. Предположим, к примеру, что программа содержит подпрограммы, предназначенные для исправления или подробного диагностирования ошибок во входных данных. Если такие ошибки редки, то в большинстве запусков программы эти подпрограммы будут не нужны. Однако если программа полностью связывается до начала исполнения, то эти подпрограммы должны загружаться (и для них должны обрабатываться внешние связи) каждый раз, когда исполняется программа. Динамическое связывание позволяет загружать подпрограммы в том (и только в том) случае, когда они действительно нужны. Если соответствующие программы занимают много места или имеют много внешних связей, то это может дать существенную экономию времени и пространства оперативной памяти. Похожая ситуация возникает и тогда, когда при каждом конкретном запуске программы используется лишь незначительная часть подпрограмм из большого числа потенциально используемых. Причем, какие именно подпрограммы будут использоваться, становится известно только после обработки входного потока. Такая организация программы характерна, например, для интерактивных систем, в которых пользователю разрешается в процессе диалога вызывать .различные библиотечные подпрограммы. В этом случае входные данные вводятся пользователем с клавиатуры, а полученные результаты выводятся на экран терминала. В каждом сеансе работы пользователь может потенциально обратиться к любой библиотечной подпрограмме, однако фактически используется лишь их небольшая часть. Благодаря динамическому связыванию отпадает необходимость загружать при каждом запуске все библиотечные подпрограммы. Более того, динамическое связывание позволяет главной программе вообще ничего не знать о том, с каким набором подпрограмм она должна работать, так как имена подпрограмм могут обрабатываться точно так же, как и другие входные данные. Существуют различные механизмы, позволяющие осуществлять загрузку и связывание в момент вызова подпрограммы. Здесь мы обсудим подход, при котором динамическое связывание осуществляется с помощью запросов к операционной системе. При желании можно рассматривать эти запросы как обращение к резидентной части загрузчика. При динамическом связывании программа вместо того, чтобы выполнять команду JSUB, в которой используется внешнее имя, должна выполнить запрос к операционной системе на загрузку и исполнение подпрограммы. Параметром этого запроса является символическое имя требуемой подпрограммы (рис. 3.17а). Операционная система по своим внутренними таблицам определяет, загружена ли данная подпрограмма в данный момент времени или нет. Если это необходимо, то подпрограмма загружается из пользовательской или, системной библиотеки (рис. 3.176). После этого операционная система вызывает требуемую подпрограмму (рис. 3.17в). Динамический загрузчик (блок операционный системы Динамический загрузчик Загрузка-и-вызов ERRHANDL Библиотека Программа Пользователя Программа пользователя ERRHANDL а б Динамический Динамический загрузчик загрузчик Динамический загрузчик Загрузка-и-вызов ERRHANDL Программа Программа пользователя пользователя Программа пользователя ERRHANDL ERRHANDL ERRHANDL в г д Рис.3.17. Загрузка и вызов подпрограмм с помощью динамического связывания. После завершения своей работы подпрограмма возвращает управление в точку, откуда она была вызвана (т. е. сервисной подпрограмме операционной системы, выполняющей динамическую загрузку). Затем операционная система возвращает управление программе, выдавшей запрос. Этот процесс показан на рис. 3.17г. Очень важно, чтобы возврат из подпрограммы осуществлялся через операционную систему, так как в этом случае ей будет известен момент завершения вызванной подпрограммы. После окончания работы подпрограммы занятая ей оперативная память освобождается и может быть использована для других целей. Однако это необязательно делать немедленно. Иногда желательно сохранить подпрограмму в памяти для последующего использования до тех пор, пока не потребуется память для других процессов. Если подпрограмма все еще находится в памяти, то ее повторный вызов может осуществляться без перезагрузки. В этом случае динамический загрузчик просто может передать на нее управление, как это показано на рис. 3.17д. При динамическом связывании установление соответствия между символическим именем вызываемой подпрограммы и фактическим адресом ее загрузки не делается до тех пор, пока не будет выполнена команда вызова. Другими словами можно сказать, что привязка имени к фактическому адресу откладывается до момента исполнения программы. Как мы уже говорили, это дает нам большую гибкость в организации программ мы. В то же время это приводит к дополнительным накладным расходам, так как операционная система должна участвовать в процессе вызова подпрограмм. В последующих главах мы увидим другие примеры, в которых используется откладывание привязки. Как и в данном случае, такая отложенная привязка дает большие возможности за дополнительную плату. 3.4.3. Раскручивающие загрузчики В нашем обсуждении мы не затрагивали один важный вопрос: как сам загрузчик загружается в оперативную память? Конечно, можно сказать, что он загружается операционной Системой, но тогда возникает вопрос: кто загружает операционную систему? В более общем виде вопрос можно сформулировать так: каким образом можно запустить в работу пустую машину, в оперативной памяти которой нет ни одной программы? В ситуации, когда машина пуста, необходимости в перемещении программ не возникает. Мы можем просто определить произвольный абсолютный адрес первоначальной загрузки программы. Наиболее часто этой программой будет операционная система, которая занимает заранее определенное место в памяти. Это означает, что нам нужны некоторые средства для выполнения функций абсолютного загрузчика. Одно из возможных решений - поручить оператору ввести в оперативную память объектный код абсолютного загрузчика с помощью переключателей на пульте ЭВМ. Некоторые машины требуют именно это. Однако такая процедура слишком неудобна и не застрахована от ошибок, чтобы быть действительно хорошим решением данной проблемы. Другое решение - иметь программу абсолютного загрузчика в качестве резидента, записанного в память, доступную только для чтения (постоянное запоминающее устройство-ПЗУ). Когда возникает некоторый аппаратный сигнал (например, оператор нажимает кнопку <старт системы>), машина начинает выполнять программу, записанную в ПЗУ. В некоторых машинах программа исполняется непосредственно в ПЗУ, в других – она копируется в основную память и исполняется в ней. Однако не все машины имеют ПЗУ.- Кроме того, использование ПЗУ затрудняет модификацию абсолютного загрузчика, если в этом возникает необходимость. Промежуточное решение заключается в том, чтобы иметь встроенные аппаратные средства (или очень короткую программу в ПЗУ), которые позволяют прочитать запись фиксированной длины с некоторого внешнего запоминающего устройства в фиксированное место оперативной памяти. Используемое конкретное устройство обычно можно задать с помощью пультовых переключателей. После того как запись будет прочитана в память, управление передается на ее начало. Считанная запись содержит команды загрузки абсолютного загрузчика. Если команды, требуемые для организации процесса загрузки, не помещаются в одну запись, то первая запись может служить для чтения еще одной записи, а та в свою очередь может прочитать еще и другие записи. Первая запись (или записи) обычно называется раскручивающим загрузчиком (bootstrap loader). Такие загрузчики должны находиться в начале любой объектной программы, которая предназначена для работы на пустой машине. К числу таких программ относятся, например, сами операционные системы и все другие программы, которые должны выполняться без операционной системы. 3.5. Примеры реализации В данном разделе мы кратко познакомимся с редакторами связей и загрузчиками для реальных машин. Поскольку трансляция и загрузка взаимосвязаны, мы в качестве примеров будем использовать те же самые три машины, что и в разд. 2.5. При чтении данного раздела вам следует обращаться к описаниям машин, которые были даны в гл. 1. Как и ранее, мы не ставим перед собой цели дать полное описание рассматриваемых редакторов связей и загрузчиков. Вместо этого особое внимание будет уделено отдельным интересным или необычным возможностям и отличиям данных реализаций от общей модели, рассмотренной нами в предыдущем разделе. Мы также укажем на те свойства редакторов связей и загрузчиков, которые обусловлены особенностями ассемблеров или структуры ЭВМ. 3.5.1. Редактор связей System/370 Формат объектной программы, обрабатываемой редактором связей System/370, очень похож на тот, что мы обсуждали в связи с УУМ/ДС. Для повышения эффективности используется механизм ссылочных номеров, рассмотренный нами в разд. 3.2.3. Выходная программа редактора связей называется загрузочным модулем (load module). Загрузочные модули могут загружаться в оперативную память и исполняться. Они также (обычно) содержат информацию, необходимую для их последующей обработки с помощью редактора связей. Однако пользователь может указать, что некоторый модуль является "нередактируемым", и в этом случае большая часть управляющей информации будет опущена. Это позволяет делать более компактные загрузочные модули. Редактор связей System/370 может выполнять все обсуждавшиеся нами стандартные операции. Управляющие секции можно исключать, заменять или реорганизовывать. Имена, используемые во внешних ссылках, можно заменять или исключать. Для облегчения редактирования обеспечивается автоматическая замена управляющих секций. Если две или более обрабатываемые управляющие секции имеют одинаковые имена, то только первая из них будет включена в загрузочный модуль. Другие будут исключены, и это не рассматривается как ошибка. Для обработки внешних ссылок редактор связей выполняет автоматический библиотечный поиск в системных и пользовательских библиотеках. Однако пользователь может запретить такой поиск для некоторых или даже для всех внешних ссылок. Редактор связей хранит в загрузочном модуле разнообразную информацию, в том числе: дату трансляции (берется из объектной программы) и тип использовавшегося транслятора, а также даты редактирования и модификации (что позволяет иметь историю работы с загрузочными модулями). Когда редактор связей помещает загрузочный модуль в библиотеку, то он заносит его имя в каталог. В каталоге о каждом модуле кроме имени хранится и другая информация, например, допускаем ли модуль повторную обработку редактором связей, имеет ли он оверлейную структуру, является ли он повторно входимым или разделяемым и многие другие атрибуты. Некоторые из этих атрибутов сообщаются пользователем, другие генерируются редактором связей в процессе работы. Редактор связей System/370 поддерживает оверлейные программы, структура которых совпадает с той, что мы рассматривали в разд. 3.3.3, и даже предоставляет более широкие возможности. В каждом сегменте разрешается использовать несколько входных точек. Для этого в каждый сегмент, который может потребовать оверлейную загрузку, автоматически включается таблица входов (entry table). В этой таблице для каждого внешнего имени указан соответствующий ему сегмент. Имеется также и таблица сегментов (она похожа на таблицу сегментов, описанную нами в разд. 3.3.3). В оверлейных программах при, некоторых ограничениях разрешено использовать так называемые исключительные ссылки (exclusive references) - ссылки, которые требуют оверлейного замещения самого вызывающего сегмента. Примером такой ссылки мог бы служить вызов сегмента С из сегмента В в программе на рис. 3.12а. Однако пользоваться этой возможностью рекомендуется только в особых случаях. В стандартном режиме (если не задан соответствующий атрибут) такие ссылки рассматриваются редактором связей как ошибочные. Как уже говорилось в разд. 3.3.3, оверлейная загрузка осуществляется автоматически при вызове сегмента. Кроме того, программа может явным образом потребовать загрузку конкретного сегмента (обратившись к операционной системе). Некоторые версии операционной системы допускают продолжение исполнения' сегментам выдавшего запрос, в то время как производится, загрузка требуемого сегмента. Это позволяет совмещать во времени процессы загрузки и исполнения программы. Естественно, что подобные параллельные операции необходимо тщательно планировать при проектировании оверлейной программы. В System/370 оверлейные программы могут разбиваться на несколько различных областей, что позволяет более эффективно использовать оперативную память. Каждая область содержит древовидную оверлейную структуру. Внутри одной области применяются обычные правила организации оверлея. Однако области не- зависят друг от друга, и поэтому сегмент одной области может свободно обращаться к любому сегменту другой области. Рассмотрим, например, структуру на рис. 3.18а. Это та же самая оверлейная структура, что и на рис. 3.12а, но с добавленными управляющими секциями L, М и N. Каждая из них может вызываться либо из, секции Н, либо из секции J. Эта структура является недопустимой, так как L, М и М размещены в двух различных узлах дерева, что приводит к неоднозначному определению внешних имен. Для того чтобы организовать допустимую оверлейную структуру, расположенную в одной области, нам надо было бы разместить L, М и N в корневом сегменте. В этом случае они были бы доступны как Н, так и J (рис. 3.18б). Однако это означало бы, что все эти три управляющие секции оставались бы в оперативной памяти в течение всего времени исполнения программы. Таким образом, преимущество оверлейной структуры было бы утрачено. Если L, М. и N велики по объему, то это привело бы к значительному увеличению размера занимаемой оперативной памяти. A B F/G C D/E H L M N I J K L M N а A/L/M/N B F/G C H D/E I J K D/E Область 1 J K б A B F/G C H I Область 2 L M N в Рис.3.18. Оверлейная программа, расположенная в нескольких областях. На рис. 3.18в эта проблема решена с помощью размещения оверлейной структуры в нескольких областях. Область 1 содержит ту же самую оверлейную структуру, которая приведена на рис. 3.12а. Область 2 содержит сегменты L, М и N, Эти сегменты подключены к фиктивному корню для того, чтобы подчеркнуть, что они могут друг друга замещать. Это позволяет решить проблему эффективного использования оперативной памяти и в то же время позволяет обращаться к любому сегменту Области 2 как из сегмента Н, так и из сегмента J. Обычно в состав System/370 наряду с редактором связей входит и связывающий загрузчик. Загрузчик предоставляет меньше средств, чем редактор связей. Например, он не поддерживает оверлейные программы и не создает загрузочные модули, которые можно помещать в библиотеку. Однако, поскольку загрузчик является более простым и не создает загрузочные модули, он позволяет сократить примерно на половину время, требуемое на загрузку и связывание программы. Поэтому для повышения эффективности его рекомендуется применять всегда, когда не требуется выполнять специализированные функции редактора связей. Более подробную информацию о редакторе связей Sуstem/370 можно найти в IBM [1978] и IBM [1972а]. 3.5.2. Программа связывания ЭВМ VAX Редактор связей в системе VAX называется программой связывания (linker) и выполняет функции, аналогичные тем, что мы уже обсуждали в этой главе. Формат объектной программы VAX несколько более сложный, чем формат, использовавшийся для УУМ/ДС. Результатом работы программы связывания является одна или несколько блок-секций (image sections). Блок-секция состоит из нескольких программных секций (PSECT), имеющих сходные значения атрибутов (например, таких как защита по записи или исполнению). Работа программы связывания по созданию блок-секций управляется ассемблером или компилятором с помощью специальных команд, включаемых в объектную программу. (Запись-модификатор, являющаяся частью объектной программы УУМ/ДС, представляет собой простой пример команды этого типа.) В качестве рабочей памяти программа связывания использует внутренний стек. Команды объектной программы могут заносить в стек разнообразную информацию, считывать значения из стека и записывать их в блок-секцию, а также выполнять операции над величинами, записанными в стек. Набор команд состоит более чем из 50 различных операций, обеспечивающих самые широкие возможности. На рис. 3.19 приведен простой пример, иллюстрирующий данный способ организации перемещения и связывания. На рис. 3.19а показаны три программные секции, ассемблировавшиеся совместно. По существу это те же программные секции, которые использовались нами в примере на рис. 3.7. Однако здесь показана только та часть, которая необходима для обработки предложения с меткой REF4 (из секции PROGA). На .рис. 3.196 приведен несколько упрощенный вариант команд, обеспечивающих генерацию значения предложения REF4. Первая команда заносит в вершину стека число 14 (шестнадцатеричное), являющееся значением выражения (ENDA-LISTA). Вторая команда заносит в стек базовый адрес секции PROGC (он назначается программой связывания) плюс смещение 30 (шестнадцатеричное). Эта сумма представляет собой значение метки LISTC. Третья команда суммирует два верхних элемента стека и помещает результат в его вершину. Последняя команда запоминает эту величину в качестве следующего слова генерируемой блок-секции. Это как раз то значение, которое должно содержаться в слове с меткой REF4. Программа связывания VAX может генерировать три типа блок-секций. Блоксекции первого типа - это выполняемые секции (executable images), используемые для загрузки и исполнения. Однако их последующая обработка программой связывания невозможна. Второй тип блок-секций – это разделяемые секции (shareable images). Такие секции исполняться не могут, но зато они могут впоследствии обрабатываться программой связывания. Их можно, например, использовать в качестве промежуточного представления для очень больших программ. Разделяемые блок-секции позволяют также различным программам использовать одну и ту же копию некоторого набора команд или областей данных. На магнитном диске и в оперативной памяти присутствует только один экземпляр разделяемой блок-секции, что дает возможность экономить эти ресурсы. Адрес 0000 0040 0054 0000 0000 0030 Исходное положение PSECT LISTA … ENDA … REF4 WORD PSECT PSECT LIST … END PROGA PSECT 1 (PROGA) ENDA-LISTA+LISTC PROGB PSECT 2 (PROGB) PROGC PSECT 3 (PROGC) a Команда Операнд Объектный код и команды Поместить значение в стек Значение=14 перемещения/связывания Поместить в стек базу PSECT PSECT=3{PROGC}, для предложения REF4 в плюс смещение смещение=30 PROGA Сложение Запомнить слово б Рис.3.19. Пример использования механизма перемещения и связывания ЭВМ VAX. Третий тип-это системные блок-секции (system mages). Такая блок-секция предназначена для работы на машине без поддержки операционной системы. (Операционная система VAX сама является системной блок-секцией). Эти блок-секции используются только для специальных целей. По сравнению с другими типами блоксекций системная блок-секция имеет более простую структуру. Программа связывания VAX выполняет обычные функции по перемещению и связыванию программ. В дополнение к этому она делает часть работы, которая в других системах выполняется ассемблером или компилятором. Например, ассемблер VAX не собирает все части программных секций в единую объектную программу. Эта реорганизация осуществляется программой связывания. Аналогично ссылки между различными программными секциями, ассемблируемыми совместно, обрабатываются программой связывания под управлением последовательности команд сгенерированной ассемблером, и в отличие от многих других систем в системе VAX не требуется явного объявления внешних имен. (Это один из примеров того, насколько мощным является используемый в системе VAX способ командного управления связыванием.) Ссылки между раздельно ассемблируемыми программными секциями обрабатываются так, как было описано в разд. 3.2. Используемые имена Ложны быть объявлены глобальными (т. е. внешними). Определения глобальных имен являютсясоставной частью объектного представления и используются программой связывания для разрешения внешних ссылок между программными секциями. Различают два типа внешних ссылок- сильные (strong) и слабые (weak). Сильные ссылки обрабатываются обычным образом. Разрешение слабых ссылок осуществляется только в том случае, если секция, в которой определены соответствующие имена, присутствует во входном потоке программы связывания. Автоматический библиотечный поиск для этих ссылок не делается. Неразрешенные слабые ссылки не рассматриваются как ошибки. Вместо этого им присваивается нулевое значение. Одно из применений таких слабых ссылок тестирование модульных программ. В этом случае может возникнуть желание протестировать часть программы прежде, чем будут написаны все подпрограммы. Если все вызовы к отсутствующим подпрограммам идентифицировать как слабые ссылки, то программу можно будет беспрепятственно загрузить и исполнять. Конечно, если программа попытается вызвать отсутствующую подпрограмму, то в результате будет зафиксирована ошибка. (Редактор связей System/370 и загрузчик ЭВМ CY-BER допускают использование похожих слабых ссылок.) Определения глобальных переменных обычно отсутствуют в исполняемой блоксекции, поскольку эта секция не предназначена для повторной обработки с помощью программы связывания. Однако в случае использования некоторых средств отладки глобальные имена остаются в исполняемой блок-секции и применяются при выводе диагностических сообщений. В разделяемой блок-секции определения некоторых глобальных имен сохраняются для того, чтобы обеспечить возможность связывания этой секции с другими программами. Эти имена, называемые общими (universal) именами, задаются с помощью команд программы связывания. Глобальные имена, не объявленные общими, в разделяемой блок-секции не сохраняются. Программа связывания ЭВМ VAX не поддерживает оверлейные программы. Частично это вызвано тем, что в системе VAX предоставляется виртуальная память большого объемам Разработчики системы сочли, что большой объем виртуальной памяти и применение алгоритмов управления этой памятью делают ненужным использование оверлейных структур. (Напомним, однако, что в System/370 редактор связей поддерживает оверлейные программы и на системах с виртуальной памятью.) Дополнительную информацию по системе VAX вы можете найти в DEC [1982] и DEC [1978]. 3.5.3. Загрузчик ЭВМ CYBER Формат объектной программы, используемый в системе CYBER, отчасти более сложный, чем формат УУМ/ДС. Однако он содержит ту же самую базовую информацию. Для задания информации о связывании используется механизм, сходный с рассмотренными нами записями-модификаторами. Родственный механизм может быть использован для описания требуемых перемещений. Наряду с этим для описания перемещений может использоваться аппарат, похожий на аппарат масок перемещения, рассмотренный нами в разд. 3.2.1. Маски перемещения особенно полезны в системе CYBER, поскольку в ней нет относительной адресации. Это означает, что в общем случае программы CYBER содержат намного больше величин, требующих перемещения, чем аналогичные программы для VAX или System/370. В каждом слове CYBER может храниться не одна команда, а несколько. Поэтому в маске перемещения недостаточно иметь по одному разряду на каждое слово. Поскольку в CYBER адреса оперативной памяти могут присутствовать только в командах, использующих 30-разрядный формат, то имеется пять возможных вариантов размещения перемещаемой величины внутри слова: 1. перемещение не требуется; 2. перемещаемое значение находится в левой половине слова; 3. перемещаемое значение находится в правой половине слова; 4. перемещаемые значения находятся в обеих половинах слова; 5. перемещаемое значение находится в середине 30-разрядного слова. Поэтому, когда используется аппарат масок перемещения, каждому слову сопоставляется 4-разрядное поле. Это поле используется для кодирования перечисленных выше вариантов и для указания операции, которую надо выполнить для модификации перемещаемых значений (либо прибавить базовый адрес к перемещаемому значению, либо вычесть). Загрузчик CYBER поддерживает оверлейные программы, но на их структуру накладываются более жесткие ограничения. Оверлейная структура ограничена максимум тремя уровнями. Сегмент идентифицируется упорядоченной парой целых чисел и может содержать только одну входную точку. Каждый сегмента кроме корневого, загружается по явному запросу. Автоматическая загрузка сегментов не предусмотрена. Прикладные программы могут запросить загрузку сегмента, обратившись к операционной системе. В качестве альтернативы допускается использование небольшого резидентного загрузчика, обеспечивающего оверлейную загрузку. Этот загрузчик может быть включен в корневой сегмент. Имеется также и более мощное средство организации оверлейного процесса, которое называется сегментацией (segmentation). Сегментированная программа также должна иметь древовидную структуру, однако допускается использование более трех уровней и каждый сегмент может иметь несколько входных точек. Разрешается, кроме того использовать оверлейные программы, размещенные в нескольких областях памяти. Такая организация оверлейной структуры похожа на то, что мы рассматривали для System/370 (хотя и используется другая терминология). Программист с помощью командного языка может явно задать древовидную структуру, указав, какие программы должны включаться в соответствующие сегменты. Если это не сделано, то загрузчик распределяет программы между сегментами, исходя из анализа существующих между ними внешних связей. В дополнение к стандартным функциям загрузчик CYBER позволяет создавать так называемые капсулы (capsules). Капсула состоит из одной или нескольких объектных программ, связанных между собой и записанных в специальном формате, позволяющем осуществлять быструю загрузку. Для того чтобы загрузить капсулу в оперативную память, прикладные программы могут вызвать быстрый динамический загрузчик (Fast Dynamic Loader - FDL) . Загрузка с помощью FDL более эффективна, чем другие методы загрузки, и требует меньших ресурсов оперативной памяти, однако она накладывает более жесткие ограничения и сложнее в использовании. Дополнительную информацию о загрузчике CYBER можно найти в СDС [1982б]. УПРАЖНЕНИЯ Раздел 3.1 1. Определите двоичный формат объектной программы для УУМ и напишите абсолютный загрузчик (на языке ассемблера УУМ), позволяющий загружать программы в этом формате. 2. Опишите метод преобразования объектной программы, формат которой использует символьное представление ассемблированного кода (см. рис. 3.1а) в машинное представление. Как бы вы реализовали его на языке ассемблера УУМ? 3. В . чем заключаются преимущества и недостатки реализации загрузчика на языке высокого уровня, например таком, как Паскаль? Какие проблемы возникают в этом случае и как их можно преодолеть? Раздел 3.2 1. Модифицируйте алгоритм на рис. 3.10 таким образом, чтобы он выполнял настройку относительных адресов при помощи масок перемещения. Связывание попрежнему выполняется с помощью записей-модификаторов. 2. Предположим, что в некоторой ЭВМ. в основном используется прямая адресация, но имеются различные командные форматы. Какие проблемы возникают в связи с этим, если для настройки относительных адресов использовать маски перемещения? Как их можно разрешить? 3. Примените алгоритм, показанный на рис. 3.10, для связывания и загрузки объектной программы на рис. 3.8. Сравните ваш результат с тем, что изображено на рис. 3.9. 4. Предположим, что PROGA, PROGB и PROGC те же, что и на рис. 3.9. Покажите, как изменится объектная программа (включая записи тела программы и записимодификаторы), если в каждую программу добавить следующие предложения: REF9 WORD LISTC REF10 WORD LISTB-3 REF11 WORD LISTA+LISTB REF12 WORD ENDC-LISTC-100 REF13 WORD LISTA-LISTB-ENDA+ENDB 5. Примените алгоритм, показанный на рис. 3.10, для связывания и загрузки модифицированной объектной программы, сгенерированной вами в упр.4. 6. Включите в алгоритм на рис. 3.10 средства для обнаружения в выражениях некорректного использования внешних имен. (Список правил см. в разд. 2.3.5.) Какие проблемы возникают при проведении подобных проверок? 7. Модифицируйте алгоритм -на рис. 3.10 так, чтобы в нем использовались номера ссылок, как это было описано в разд. 3.2.3. 8. В некоторых загрузчиках используется косвенная схема связывания. Для того чтобы использовать подобную схему в УУМ/ЯС, .ассемблер должен был бы сгенерировать список указателей для всех имен, перечисленных в предположениях EXTREF. (Один указатель на каждое внешнее имя.) С помощью записей-модификаторов загрузчику было бы предписано занести значения адресов соответствующих внешних имен в эти указатели. Тогда внешние ссылки можно было бы осуществлять посредством косвенной адресации через эти указатели. Так, например, команда вида LDA XYZ (где XYZ - внешняя ссылка) ассемблировалась бы так, как если бы она имела вид LDA @PXYZ где PXYZ - указатель на XYZ. В чем преимущества и недостатки этого метода? 9. Предложите проект однопросмотрового связывающего загрузчика. Какие ограничения (если они нужны) требуется наложить в этом случае? В чем преимущества и недостатки такого однопросмотрового загрузчика? 10. Некоторые языки программирования разрешают размещать данные в общих областях памяти (имеются в виду области типа COMMON языка Фортран). Исходная программа может содержать несколько общих областей (с разными именами). Мы можем рассматривать каждую общую область как отдельную управляющую секцию. При связывании и загрузке программ общим областям с одинаковыми именами присваивается один и тот же начальный адрес. (Эти общие области в разных программах могут иметь различную длину.) Таким образом, устанавливается соответствие между переменными общих, областей, объявленными в различных программах. Любые данные, записанные в общую область одной программой, становятся доступными для других. Как мог бы загрузчик обрабатывать такие общие области? (Предложите способ модификации алгоритма на рис. 3.10 для обеспечения работы с общими областями.) Раздел 3.3. 1. Модифицируйте алгоритм на рис. 3.10 так, чтобы он для разрешения внешних ссылок обеспечивал автоматический библиотечный поиск. Вы можете считать, что доступ к библиотекам осуществляется с помощью сервисных процедур операционной системы. 2. Модифицируйте алгоритм на рис. 3.10 так, чтобы он позволял обрабатывать команды CHANGE, DELETE и INCLUDE, описанные в разд. 3.3.2. Если потребуется, вы можете ввести необходимые ограничения на их не пользование. 3. Предположим, что загрузчик готовит листинг, который включает на только адреса, присвоенные внешним именам, но и таблицу перекрестные; ссылок между загружаемыми управляющими секциями. Какая информации может быть полезна в таком листинге? Коротко опишите способ реализации этой возможности и дайте определения необходимых для этого структур данных. 4. Рассмотрим оверлейную структуру, показанную на рис. 3.12а. Предположим, что подпрограмма Н может быть вызвана не только из В но и из С. Как это можно реализовать в оверлейной программе? (Считаем, что правила оверлея, определенные в разд. 3.3.3, не меняются.) 5. Опишите алгоритм назначения начальных адресов сегментам оверлейной структуры, принимая во внимание объем памяти, необходимый для размещения SEGTAB и OVLMGR. 6. Во время выполнения оверлейной программы существует много различных наборов сегментов, которые могут находиться в памяти в различные моменты времени. На рис. 3.14б показаны три возможных варианта для программы на рис. 3.12а. Приведите другие варианты, используя аналогичную форму представления. 7. Дайте краткое описание процедуры управления оверлеем, которую мы назвали OVLMGR. Определите необходимые для нее структуры данных. 8. Предположим, что мы хотим разрешить иметь в сегментах оверлейной программы более чем одну входную точку. Например, в программе на рис. 3.12а мы хотели бы непосредственно из сегмента А обращаться либо к входу D, либо к входу Е. При этом независимо от того, по какому входу будет сделан вызов, должен загружаться весь сегмент D/E. Каким образом может быть реализована такая возможность загрузчиком и (или) менеджером оверлея? 9. В описанном нами оверлейном процессе только передачи управления к другому сегменту вызывают оверлейную загрузку. Как можно было бы реализовать оверлейный процесс так, чтобы ссылка на данные также приводила к оверлейной загрузке? Вы можете ввести любые, необходимые по вашему мнению, ограничения. Раздел 3.4 1. Определите формат модуля, подходящий для представления связанной программы, генерируемой редактором связей. Будем считать, что связанная программа не предназначена для повторной обработки с помощью редактора связей. Опишите алгоритм перемещающего загрузчика, который можно использовать для загрузки модулей данного формата. 2. Рассмотрим следующие варианты хранения, связывания и исполнения пользовательской программы. а) Хранится только исходная программа; при каждом запуске программы она переассемблируется и загружается связывающим загрузчиком. б) Хранятся исходная программа и ее объектное представление; при каждом запуске программы она загружается с помощью связывающего загрузчика. в) Хранятся исходная программа и вариант программы, подготовленный редактором связей, в котором оставлены неразрешенными ссылки на библиотечные подпрограммы; при каждом запуске программы она загружается с помощью связывающего загрузчика. г) Хранятся исходная программа и вариант программы, подготовленный редактором связей, в котором разрешены все внешние ссылки; при каждом запуске программы она загружается с помощью связывающего загрузчика. д) Хранятся исходная программа и вариант программы, подготовленный редактором связей, в котором разрешены все внешние ссылки и выполнена настройка относительных адресов; при каждом запуске программы она загружается с помощью абсолютного загрузчика. Укажите условия, при которых целесообразно использовать каждый из этих вариантов. Предполагается, что в исходной программе не делается никаких изменений. 3. Динамическое связывание и оверлейная загрузка имеют много общих черт. Сопоставьте эти два подхода с точки зрения эффективности, простоты использования и других важных, на ваш взгляд, факторов. В каких случаях каждый из этих подходов предпочтительнее другого? 4. Динамическое связывание, как оно было описано в разд. 3.4.2,. работает только для передачи управления. Как его можно приспособить для того, чтобы оно обеспечивало динамическую загрузку и при ссылках на данные? 5. Предположим, что подпрограмма, загруженная с помощью динамической загрузки, должна оставаться в оперативной памяти до окончания главной программы. Предложите средства повышения эффективности динамического связывания за счет того, что операционная система будет использоваться только для загрузки программы, но не для передачи на нее управления. 6. Предположим, что требуется удалить из памяти динамически загруженную подпрограмму (для повторного использования пространства памяти). Будет ли предложенный вами в упр.5 метод работать в этом случае? Какие возникают здесь проблемы и как они могут быть разрешены? 7. Предположим, что нажатие на пульте УУМ/ДС кнопки "старт системы" вызывает чтение с вводного устройства 128-разрядной записи, которая размещается в оперативной памяти, начиная с адреса 0000. После того как запись прочитана, управление автоматически передается по адресу 0000. Какие команды должны находиться в первой раскручивающей записи для того, чтобы можно было загрузить следующую за ней абсолютную объектную программу? Для раскручивающей записи и объектной программы вы можете выбрать любой удобный вам формат. ГЛАВА 4. 4.1. Основные функции макропроцессоров В этом разделе мы рассмотрим общие для всех макропроцессоров функции. В разд. 4.1.1 обсуждаются макроопределения, процессы макровызова и макрогенерации с учетом параметров. В иллюстрирующих примерах используется язык ассемблера УУМ/ДС. В разд. 4.1.2 описываются однопросмотровый алгоритм работы простого макропроцессора и необходимые для его работы структуры данных. В последующих разделах этой главы обсуждаются вопросы реализации некоторых дополнительных возможностей. 4.1.1. Макроопределения и макрорасширения На рис. 4.1 изображен пример программы для УУМ/ДС, в которой используются макроинструкции. Эта программа выполняет те же функции и имеет ту же логику, что и фрагмент программы, изображенной на рис. 2.5. Изменен только способ нумерации предложений. В этой программе определены и используются две макроинструкции RDBUFF и WRBUFF. Функции и логика макроопределения RDBUFF те же, что и у подпрограммы RDREG, изображенной на рис. 2.5; макроопределение WRBUFF аналогично подпрограмме WRREG. Эти макроопределения находятся в исходной программе непосредственно после предложения START. В них используются две новые директивы ассемблера - MACRO и MEND. Первое предложение MACRO (строка 10) идентифицирует начало макроопределения. Текст в поле метки (RDBUFF) 5 COPY START 0 КОПИРОВАНИЕ ФАЙЛА 10 RDBUFF MACRO &INDEV,&BUFADR,&RECLTH 15 . 20 . МАКРОС ДЛЯ ЧТЕНИЯ В БУФЕР 25 . 30 CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА 35 CLEAR А 40 CLEAR S 45 +LDT #4096 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ 50 TD =X'&INDEV' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА 55 JEQ *-3 ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ 60 RD =X'&INDEV' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 65 COMPR A,S ПАРОВЕРКА НА КОНЕЦ ЗАПИСИ 70 JEQ *+11 ВЫХОД ИЗ ЦИКЛА ЕСЛИ КОНЕЦ ЗАПИСИ 75 STCH &BUFADR,X ЗАПИСЬ СИМВОЛА В БУФЕР 80 TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 85 JLT *-19 ДЛИНЫ ЗАПИСИ 90 STX &RECLTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ 95 MEND 100 WRBUFF MACRO &OUTDEV,&BUFADR,&RECLTH 105 . 110 . МАКРОС ДЛЯ ЗАПИСИ ИЗ БУФЕРА 115 . 120 CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА 125 LDT &RECLTH 130 LDCH &BUFADR,X ВЫБОРКА СИМВОЛА ИЗ БУФЕРА 135 TD =X'&OUTDEV' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА 140 JEQ *-3 ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ 145 WD =X'&OUTDEV' ЗАПИСЬ СИМВОЛА 150 TIXR T ЦИКЛ ПОКА ВСЕ СИМВОЛЫ НЕ БУДУТ 155 JLT *-14 ЗАПИСАНЫ 160 MEND 165 . 170 . ОСНОВНАЯ ПРОГРАММА 175 . 180 FIRST STL RETADR СОХРАНЕНИЕ АДРЕСА ВОЗВРАТА 190 CLOOP RDBUFF F1,BUFFER,LENGTH ЧТЕНИЕ ЗАПИСИ В БУФЕР 195 LDA LENGTH ПРОВЕРКА НА КОНЕЦ ФАЙЛА 200 COMP #0 205 JEQ ENDFIL ВЫХОД ПО КОНЦУ ФАЙЛА 210 WRBUFF 05, BUFFER,LENGTH ЗАПИСЬ ВЫХОДНОЙ ЗАПИСИ 215 J CLOOP ЦИКЛ 220 ENDFIL WRBUFF 05,EOF,THREE ЗАНЕСЕНИЕ МАРКЕРА КОНЦА ФАЙЛА 225 J @RETADR 230 EOF BYTE C'EOF' 235 240 245 250 255 THREE RETADR LENGTH BUFFER WORD RESW RESW RESB END 3 1 1 4096 FIRST ДЛИНА ЗАПИСИ БУФЕР ДЛИНОЙ 4096 БАЙТ Рис.4.1. Использование макросов в программе для УУМ/ДС. (RDBUFF) является ?????? ????? ????????????????. В поле операнда находятся имена формальных параметров макроопределения. В нашем макроязыке имена всех формальных параметров начинаются спецсимволом & (амперсанд), что позволяет осуществлять замену формальных параметров на фактические в процессе макрогенерации. Имя и параметры макроса определяют шаблон или прототип используемой программистом макроинструкции. После директивы MACRO следуют предложения, составляющие тело макроопределения (строки с 15 по 90). Именно эти предложения и будут порождены в процессе макрогенерации. Директива ассемблера MEND (строка 95) является признаком конца макроопределения. Определение макроса WRBUFF (строки с 100 по 160) имеет аналогичную структуру. Основная программа начинается со строки 180. Предложение в строке 190 является предложением макроинициализации. Оно определяет имя макроинструкции, которая должна быть инициализирована, и аргументы (фактические параметры), которые должны быть использованы в процессе макрогенерации при порождении макрорасширения. (Предложение макроинициализации часто называют также предложением макровызова или просто макровызовом. Для того чтобы избежать путаницы с предложениями вызова процедур и подпрограмм, мы предпочитаем использовать термин "макроинициализация" . Как мы увидим, процесс макроинициализации совершенно отличен от процесса вызова подпрограммы.) Читатель должен сравнить логику головной программы на рис. 4.1 с логикой программы на рис. 2.5, помня о схожести функций RDBUFF и RDREG, WRBUFF и WRREG. Программа на рис. 4.1 может быть подана на вход макропроцессора. На рис. 4.2 изображена результирующая программа. Определения макроинструкций в ней отсутствуют, поскольку после порождения макрорасширений они уже не нужны. Каждое предложение макроинициализации превратилось в предложения видоизмененного тела макроопределения, в котором формальные параметры, описанные в макропрототипе (заголовке макроопределения), заменены аргументами предложения макроинициализации. Между аргументами и параметрами имеется позиционное соответствие: первый аргумент в предложении макроинициализации соответствует первому параметру в макропрототипе и т. д. Например, при расширении предложения макроинициализации в строке 190 параметр &INDEV всюду, где он встречается в теле макроопределения, заменен на аргумент FI. Аналогично &BUFADR заменен на BUFFER и &RECLTH заменен на LENGTH. Строки 190а -1901 представляют собой полное расширение инструкции макроинициализации в строке 190. Строки комментариев внутри тела макроопределения удалены, однако комментарии внутри предложений языка остались. Обратите внимание, 5 COPY START 0 КОПИРОВАНИЕ ФАЙЛА 180 FIRST STL RETADR ЗАПОМИНАНИЕ АДРЕСА ВОЗВРАТА 190 .CLOOP RDBUFF F1,BUFFER,LENGTH ЧТЕНИЕ ЗАПИСИ В БУФЕР 190а CLOOP CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА 190b CLEAR А 190c CLEAR S 190d +LDT #4096 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ 190e TD =X'F1' ПРОВЕРКА ВХОДНОГО УСТРОЙСТВА 190f JEQ *-3 ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ 190g RD =X'F1' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 190h COMPR A,S ПРОВЕРКА ОКОНЧАНИЯ ЗАПИСИ 190i JEQ *+11 ВЫХОД ПО КОНЦУ ЗАПИСИ 190j STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР 190k TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 190l JLT *-19 ДЛИНЫ ЗАПИСИ 190m STX LENGTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ 195 LDA LENGTH ПРОВЕРКА НА КОНЕЦ ФАЙЛА 200 COMP #0 205 JEQ ENDFIL ВЫХОД ПО КОНЦУ ФАЙЛА 210 WRBUFF 05, BUFFER,LENGTH ЗАПИСЬ БУФЕРА 210a CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА 210b LDT LENGTH 210c LDCH BUFFER,X ЧТЕНИЕ СИМВОЛА ИЗ БУФЕРА 210d TD =X'05' ПРОВЕРКА ВЫХОДНОГО УСТРОЙСТВА 210e JEQ *-3 ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ 210f WD =X'05' ЗАПИСЬ СИМВОЛА 210g TIXR T ЦИКЛ ПОКА ВСЕ СИМВОЛЫ НЕ БУДУТ 210h JLT *-14 ЗАПИСАНЫ 215 J CLOOP ЦИКЛ 220 .ENDFIL WRBUFF 05,EOF,THREE ЗАНЕСЕНИЕ МАРКЕРА КОНЦА ФАЙЛА 220a ENDFIL CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА 220b LDT THREE 220c LDCH EOF,X ЧТЕНИЕ СИМВОЛА ИЗ БУФЕРА 220d TD =X'05' ПРОВЕРКА ВХОДНОГО УСТРОЙСТВА 220e JEQ *-3 ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ 220f WD =X'05' ЗАПИСЬ СИМВОЛА 220g TIXR T ЦИКЛ ПОКА ВСЕ СИМВОЛЫ НЕ БУДУТ 220h JLT *-14 ЗАПИСАНЫ 225 J @RETADR 230 EOF BYTE C'EOF' 235 THREE WORD 3 240 RETADR RESW 1 245 LENGTH RESW 1 ДЛИНА ЗАПИСИ 250 BUFFER RESB 4096 БУФЕР ДЛИНОЙ 4096 БАЙТ 255 END FIRST Рис.4.2. Программа на рис.4.1. с макрорасширениями. что сами предложения макроинициализации вставлены как строки комментария. Они служат для напоминания об исходных предложениях, написанных программистом. Метка предложения макроинициализации (CLOOP) оставлена в качестве метки первого предложения сгенерированного макрорасширения. Это позволяет программисту использовать макроинструкции аналогично другим инструкциям языка ассемблера. Расширение предложений макроинициализации в строках 210 и 220 осуществляется аналогично. Заметьте, что в двух макроинициализациях макроса WRBUFF заданы разные аргументы и в результате получены различные расширения. После обработки макропроцессором расширенный файл (рис. 4.2) может быть использован в качестве входного файла ассемблера. Бывшие предложения макроинициализации будут рассматриваться как комментарии, а сгенерированные предложения макрорасширения будут обрабатываться ассемблером в точности также, как если бы они были написаны непосредственно программистом. Сравнение расширенной программы на рис. 4.2 с программой на рис. 2.5 показывает наиболее существенные различия между макроинициализацией и вызовом подпрограммы. В программе на рис. 4.2 предложения из тела макроса WRBUFF сгенерированы дважды: строки 210а-210h и 220а-220h. В программе на рис. 2.5 соответствующие предложения имеются лишь в единственном экземпляре в подпрограмме WRREG (строки 210-240). Это является общим правилом. Предложения, составляющие расширение макроса, генерируются (и соответственно ассемблируются) каждый раз, когда соответствующий макрос инициализируется. Предложения подпрограммы имеются лишь в единственном экземпляре независимо от того, сколько раз эта подпрограмма вызывается. Заметьте также, что наши макроинструкции были написаны таким образом, что тело макроопределения не содержало меток. На рис. 4.1 в строке 140 находится предложение "JEQ*-3", в строке 155-предложение "JLT*-14". Соответствующими предложениями в подпрограмме WRREC (рис. 2.5) являются "JEQ WLOOP" и "JLT WLOOP", где WLOOP - метка TD инструкции, проверяющей состояние устройства вывода. Если бы эта метка была в строке 135 тела макроопределения, она была бы сгенерирована дважды: в строках 210Ь и 220d (рис. 4.2). Это привело бы к ошибке (дважды определенная метка) во время работы ассемблера. Для того чтобы избежать дублирования меток, мы исключили их из тел наших макроопределений. Использование предложений, подобных "JLT *-14", считается признаком плохого стиля программирования. Их применение отчасти может быть оправданно внутри тела макроопределения, однако и это можно считать плохим стилем, увеличивающим вероятность появления ошибок. В разд. 4.2.2 мы обсудим, как можно избежать подобных конструкций 4.1.2. Макропроцессор. Таблицы и логика Легко себе представить двухпросмотровый макропроцессор, в котором все макроопределения обрабатываются при первом просмотре, а все предложения макроинициализации - при втором. Однако такой двухпросмотровый макропроцессор не допускает наличия в теле одного 1 2 3 4 5 6 MACROS MACRO RDBUFF MACRO * * * MEND WRBUFF MACRO * * * MEND * * * MEND {стандартная версия макросов для УУМ} &INDEV,&BUFADR,&RECLTH {стандартная версия для УУМ} {конец RDBUFF} &OUTDEV,&BUFADR,&RECLTH {стандартная версия для УУМ} {конец WRBUFF} {конец MACROS} a 1 2 3 4 5 6 MACROS MACRO RDBUFF MACRO * * * MEND WRBUFF MACRO * * * MEND * * * MEND Рис.4.3. Пример макроопределения. {версия макросов для УУМ/ДС} &INDEV,&BUFADR,&RECLTH {версия для УУМ/ДС} {конец RDBUFF} &OUTDEV,&BUFADR,&RECLTH {версия для УУМ/ДС} {конец WRBUFF} {конец MACROS} б использования макроопределения внутри тела другого макроопределения определений других макросов (поскольку все макроопределения должны быть обработаны при первом просмотре до выполнения любых макрорасширений). Подобного рода определение одних макросов с помощью других может быть иногда полезно. Рассмотрим в качестве примера два макроопределения на рис. 4.3. Тело первого макроса (MACROS) содержит предложения, определяющие RDBUFF, WRBUFF и другие макроинструкции для УУМ (рис. 4.3а). Тело другого макроопределения (MACROX) содержит определения тех же макросов для УУМ/ДС (рис. 4.36). Программа, предназначенная для использования на стандартной УУМ, может инициализировать MACROS для определения необходимых ей макросов. Программа для УУМ/ДС может инициализировать MACROX для определения тех же макросов в расширенной ДС версии. Таким образом, одна и та же программа сможет работать как на стандартной УУМ, так и на УУМ/ДС (используя все ее дополнительные возможности). Единственное, что для этого надо сделать, - это инициализировать один из макросов: либо MACROS, либо MACROX. Важно понять, что само описание макроопределений MACROS или MACROX не определяет RDBUFF и других макросов. Они становятся доступными для использования только после завершения обработки предложений макроинициализации макросов MACROS или MACROX . Однопросмотровый макропроцессор, который может переключаться с обработки макроопределений на выполнение макрорасширений и наоборот, способен обрабатывать макроопределения, подобные изображенным на рис. 4.3. В этом разделе мы рассмотрим алгоритм работы и структуры данных такого макропроцессора. Поскольку макропроцессор предполагается однопросмотровым, любые макроопределения должны появляться в исходной программе прежде соответствующих предложений макроинициализации. Это ограничение практически не оборачивается какими-либо неудобствами для программиста, поскольку появление предложения макроинициализации до соответствующего макроопределения смутит любого знакомящегося с такой программой. В нашем макропроцессоре используются три основные структуры данных. Сами макроопределения находятся в таблице макроопределений (DEFTAB), которая содержит макропрототипы и предложения, составляющие тело макроопределений (с некоторыми модификациями). Строки комментариев, содержащиеся внутри макроопределений, отсутствуют в таблице DEFTAB, поскольку они не нужны на этапе макрогенерации. Для повышения эффективности процесса подстановки аргументов ссылки на формальные параметры макроопределений преобразованы в их порядковые номера. Имена макросов содержатся в таблице NAMTAB, которая, по существу, является таблицей указателей на DEFTAB. Для каждого макроопределения таблица NAMTAB содержит указатели на начало и конец макроопределения, содержащегося в DEFTAB. Третья структура данных - это таблица аргументов (ARG-ТАВ), заполняемая на этапе обработки предложений макроинициализации. Как только такое предложение распознано, его аргументы записываются в таблицу ARGTAB в соответствии с их номерами в списке аргументов. В процессе макрогенерации аргументы из таблицы ARGTAB заменяют собой соответствующие формальные параметры в теле макроопределения. NAMTAB . . . . . RDBUFF . . . DEFTAB . . . RDBUFF &INDEV,&BUFADR,&RECLTH CLEAR X CLEAR А CLEAR S +LDT #4096 TD =X'?l JEQ *-3 RD =X'?l' COMPR A, JEQ *+11 STCH ?2,X TIXR T JLT *-19 STX ?3 MEND . . ARGTAB 1 F1 2 BUFFER 3 LENGTH б Рис.4.4. Содержимое таблиц макропроцессора для программы на рис. 4.1.: а фрагменты таблиц NAMTAB и DEFTAB, связанные с макроопределением RDBUFF, б содержимое таблицы ARGTAB для макроинициализации RDBUFF в строке 190. На рис. 4.4 показаны фрагменты содержимого этих таблиц на этапе обработки программы, изображенной на рис. 4.1. Рис. 4..4а содержит макроопределение RDBUFF, записанное в таблицу DEFTAB, на начало и конец которого смотрят соответствующие указатели из таблицы NAMTAB. Обратите внимание на позиционную нотацию, использованную для обозначения параметров: параметр &INDF превращен в ?1 (указывая на то, что это первый параметр в прототипе), параметр &BUFADR преобразован в ?2 и т.д. На рис. 4.46 изображена таблица ARGTAB в том виде, в котором она окажется во время расширения макроса RDBUFF в строке 190. Для этой макроинициализации первым аргументом будет FI, вторым - BUFFER и т. д. Такая запись параметров делает процесс их замены на соответствующие аргументы существенно более эффективным. Как только встречается конструкция ?n в строке таблицы DEFTAB, за счет простых индексных операций можно выбрать соответствующий аргумент из таблицы ARGTAB. Собственно алгоритм работы макропроцессора представлен на рис. 4.5. Процедура DEFINE, которая вызывается, как только распознано начало макроопределения, формирует соответствующие строки таблиц DEFTAB и NAMTAB. Процедура EXPAND записывает аргументы в таблицу ARGTAB и осуществляет расширение предложений макроинициализации. Процедура GETLINE, которая вызывается в нескольких местах алгоритма, выбирает очередную строку для обработки. Это можете быть строка из таблицы DEFTAB (очередная строка тела макроопределения) или очередная строка входного файла в зависимости от того, какое значение имеет булевская переменная EXPANDING. Одна из особенностей этого алгоритма заслуживает дальнейших пояснений. Это обработка макроопределений внутри тела других макроопределений (как на рис. 4.3). После того как заголовок макроопределения записан в таблицу DEFTAB, было бы естественно продолжат ь заполнять эту таблицу до тех пор, пока не встретилась директива MEND. Этого, однако, нельзя делать для примера, изображенного на рис. 4.3, поскольку директива MEND в строке 3 (конец макроопределения RDBUFF) была бы интерпретирована как конец макроопределения MACROS. Для того чтобы обойти эту трудность, наша процедура DEFINE содержит счетчик LEVEL. Каждый раз, когда встречается MACRO, величина счетчика LEVEL увеличивается на 1; по каждой директиве MEND его величина уменьшается на 1. Нулевое значение этого счетчика означает, что встретилась директива MEND, соответствующая исходной директиве MACRO. Этот процесс во многом аналогичен анализу открывающихся и закрывающихся скобок при обработке арифметических выражений. Для того чтобы убедиться, что вы понимаете работу этого алгоритма, вы можете применить его вручную к программе на рис. 4.1. Результат должен быть тот же, что и на рис. 4.2. Большинство макропроцессоров допускает использование стандартных системных библиотек, содержащих наиболее часто используемые макроопределения. В этом случае нет необходимости иметь тела этих макроопределений в исходной программе - они выбираются из библиотеки тогда, когда это нужно в процессе работы макропроцессора. В результате использование макропроцессора становится существенно более удобным. Соответствующие макроопределения выбираются из библиотеки тогда, когда это нужно в процессе работы макропроцессора.. Одним из упражнений, приведенном в конце этой главы, является дополнение алгоритма, изображенного на рис. 4.5, такого рода возможностью. begin {макропроцессор} EXPANDING := FALSE while OPCODE <> 'END' do begin GETLINE PROCESSLINE end {while} end {макропроцессор} procedure PROCESSLINE begin поиск OPCODE в NAMTAB if нашли then EXPAND else if OPCODE = 'MACRO' then DEFINE else записать исходную строку в файл макрорасширения end {PROCESSLINE} procedure DEFINE begin записать имя макроопределения в NAMTAB записать макропрототип в DEFTAB LEVEL := 1 while LEVEL > 0 do begin GETLINE if это не строка-комментарий then begin заменить вхождение имени к-го параметра на ?к занести строку в DEFTAB if OPCODE = 'MACRO' then LEVEL := LEVEL + 1 else if OPCODE = 'MEND' then LEVEL := LEVEL - 1 end {если не строка-комментарий} end {while} записать в NAMETAB указатели на начало и конец макроопределения end {DEFINE} procedure EXPAND begin EXPANDING := TRUE взять первую строку макроопределения {заголовок} из DEFTAB записать аргументы макроинициалазации в ARGTAB записать предложение макроинициализации как комментарий while не конец макроопределения do begin GETLINE PROCESSLINE end {while} EXPANDING := FALSE end {EXPAND} procedure GETLINE begin if EXPANDING then begin взять следующую строку макроопределения из DEFTAB заменить выражения ?к на аргументы из ARGTAB end {if} else прочитать очередную строку входного файла end {CETLINE} Рис.4.5. Алгоритм работы однопросмотрового макропроцессора. 4.2. Машинно-независимые особенности макропроцессора В этом разделе мы обсудим некоторые расширения описанных выше базовых функций макропроцессора. Как мы уже отмечали, эти расширения не связаны непосредственно с архитектурой машины, для которой создавался макропроцессор. В разд. 4.2.1 описан метод конкатенации параметров макроинструкции с другими строками символов. В разд. 4.2.2 обсуждается один из методов генерации уникальных меток, который позволяет избежать частого использования относительной адресации в теле макроопределения. В разд. 4.2.3 вводится и иллюстрируется на нескольких примерах важное понятие условного макрорасширения (условной макрогенерации). Возможность изменения макрорасширения путем использования управляющих предложений делает макроинструкции существенно более мощным и полезным для программиста средством. В разд. 4.2.4 обсуждается определение и использование ключевых параметров макроинструкций. 4.2.1. Конкатенация макропараметров Большинство макропроцессоров допускает конкатенацию параметров с другими строками символов. Предположим, например, что программа содержит набор переменных с именами ХА1, ,ХА2, ХАЗ, ..., другой набор переменных с именами ХВ1, ХВ2, ХВЗ, ...и т. д. Если над подобными наборами переменных необходимо осуществить одинаковые действия, программист может записать их в виде макроопределения. Его параметр должен так определить необходимый набор переменных (например, А, В и т. д.), чтобы макропроцессор смог, используя значения этого параметра, сконструировать в процессе макрогенерадии все необходимые имена (ХА1, ХВ1 и т. д.). Предположим, что такой параметр назван &ID. Тело макроопределения может содержать предложение вида LDA X&IDI в котором параметр &ID должен быть сконкатенирован со строкой, состоящей из символа X, расположенной до параметра, и со строкой, состоящей из символа 1, расположенной после параметра. Проблема состоит в том, что конец параметра никак специально не обозначен (его начало легко может быть идентифицировано по символу &). Таким образом, рассматриваемое предложение может быть интерпретировано также как строка символов Х, за которой следует параметр &IDI. В данном конкретном случае макропроцессор сможет интерпретировать это предложение правильно. ОДнако если макроопределение содержит в качестве параметров и &ID, и &IDI, то ситуация становится в принципе неразрешимой. 1 2 3 4 5 6 SUM MACRO LDA X&ID->1 ADD X&ID->2 ADD X&ID->3 STA X&ID->S MEND &11 a SUM LDA ADD ADD STA A XA1 XA2 XA3 XAS б SUM LDA ADD ADD STA Рис.4.6. макропараметров. . BETA ХBETA1 XBETA2 XBETA3 XBETAS в Конкатинация Большинство макропроцессоров решает эту проблему введением специального оператора конкатенации. В макроязыке УУМ этот оператор записывается как ->. Предыдущее предложение на этом макроязыке дожно будет иметь вид LDA X&ID->1 где конец параметра &ID четко определен. Макропроцессор уничтожает все вхождения символа, обозначающего оператор конкатенации, сразу же после завершения подстановки параметров. Таким образом, символ -> не появится в порожденном тексте макрорасширения. На рис.4.6а изображено макроопределение, в котором используется оператор конкатенации. На рис.4.6б и 4.6в изображены предложения макроинициализации и соответствующие им тексты макрорасширения. Для того чтобы убедиться в правильном понимании работы оператора конкатенации, вы дожны проследить весь процесс порождения этих макрорасширений. Вы можете также подумать о том, каким образом оператор конкатенации мог бы обрабатываться в алгоритме работы макропроцессора, подобном изображенному на рис.4.5. 4.2.2. Генерация уникальных меток Как мы уже говорили в разд.4.1, тело макроопределения, вообще говоря, не должно содержать обычных меток. Это приводит к необходимости использования в исходной программе относительной адресации. Рассмотрим в качестве примера определение WRBUFF на рис.4.1. Если бы команда TD в строке 135 была помечена, эта метка оказалась бы дважды определенной (для каждой инициализации WRBUFF). Это, естественно, не позволило бы ассемблеру получить готовую программу. 25 RDBUFF MACRO &INDEV,&BUFADR,&RECLTH 30 CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА 35 CLEAR А 40 CLEAR S 45 +LDT #4096 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ 50 $LOOP TD =X'&INDEV' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА 55 JEQ $LOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ 60 RD =X'&INDEV' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 65 COMPR A,S ПАРОВЕРКА НА КОНЕЦ ЗАПИСИ 70 JEQ $EXIT ВЫХОД ИЗ ЦИКЛА ЕСЛИ КОНЕЦ ЗАПИСИ 75 STCH &BUFADR,X ЗАПИСЬ СИМВОЛА В БУФЕР 80 TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 85 JLT $LOOP ДЛИНЫ ЗАПИСИ 90 $EXIT STX &RECLTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ 95 MEND а 30 35 40 45 50 $AALOOP 55 60 65 70 ЗАПИСИ 75 80 МАКСИМАЛЬНОЙ 85 90 95 $AAEXIT RDBUFF F1,BUFFER,LENGTH CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА CLEAR А CLEAR S +LDT #4096 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ TD =X'F1' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА JEQ $AALOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ RD =X'F1' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А COMPR A,S ПАРОВЕРКА НА КОНЕЦ ЗАПИСИ JEQ $AAEXIT ВЫХОД ИЗ ЦИКЛА ЕСЛИ КОНЕЦ STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ JLT STX MEND $AALOOP ДЛИНЫ ЗАПИСИ &RECLTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ б Рис.4.7. Генерация уникальных меток для макрогенерации. Поскольку строку 135 в этом макроопределении нельзя пометить, в командах перехода на строки 140 и 155 появляются относительные адреса *-3 и *-14. Использование относительной адресации в исходной программе еще может быть приемлемым для коротких переходов типа JEQ *-3. Однако для переходов к далеко расположенным командам подобная запись является неудобной, увеличивает вероятность появления ошибок, трудна для понимания. Многие макропроцессоры решают эту проблему за счет макрогенерации меток специального вида. Рис. 4.7 иллюстрирует один из способов порождения уникальных меток. Макроопределение RDBUFF изображено на рис. 4.7а. Метки, используемые внутри тела макроопределения, начинаются специальным символом $. На рис. 4.76 изображены предложения макроиницализации и соответствующие им тексты макрорасширений. Все идентификаторы, начинающиеся символом $, модифицированы путем замены символа $ на символы $АА. При обработке других предложений макроинциализа-ции символ $ будет заменен на $ХХ, где XX-двухсимвольный алфавитно-цифровой счетчик количества обработанных предложений макроинициализации. При обработке первого встретившегося в программе предложения макроинициализации XX будет иметь значение АА. При обработке последующих предложений макроинициализации XX будет иметь значения АВ, АС и т. д. (Если для счетчика XX используются только латинские буквы и цифры, то подобный двухсимвольный счетчик позволяет обработать в одной программе до 1296 предложений макроинициализации.) Таким образом, в макрорасширениях, соответствующих различным предложениям макроинициализации, метки будут различаться. Другие примеры будут изображены на рис. 4.8 и 4.10. Язык ассемблера УУМ допускает появление символа $ в индентификаторах. Однако программисты предупреждаются о том, что этот символ не следует использовать в их программах. Это позволяет избежать всяких конфликтов между идентификаторами программиста и идентификаторами, порожденными макропроцессором. 4.2.3. Условные макрорасширения Во всех рассмотренных нами примерах использования мак-роинструкций каждая инициализация некоторого макроса расширялась всегда в одну и ту же последовательность предложений. Эти предложения могли различаться за счет подстановки разных параметров. Однако их форма и последовательность всегда были неизменными. Даже такие простые макросредства могут быть весьма полезны. Но большинство макропроцессоров может изменять также последовательность порожденных в процессе макрогенерации предложений (в зависимости от значений аргументов в предложении макроинициализации). Подобная возможность существенно увеличивает мощь и гибкость макроязыка. В этом разделе мы рассмотрим типичный набор предложений условной макрогенерации. Другие примеры содержатся в разд. 4.4, посвященном описанию некоторых макропроцессоров. В связи с обсуждаемыми в этом разделе возможностями часто используется термин "условное ассемблирование". Существуют, однако, приложения макропроцессоров, никак не связанные с программированием на языке ассемблера. Поэтому мы предпочитаем термин "условная макрогенерация". На рис. 4.8 приведен пример использования одного из предложений условной макрогенсрации. На рис, 4,8а изображено макроопределение RDBUFF логика и функции которого уже обсуждались. Это макроопределение содержит два дополнительных параметра: &EOR, который определяет шестнадцате-ричный код, являющийся признаком конца записи, и &MAXLTH, который определяет максимальную длину записи, которая может быть прочитана. (Как мы увидим, любой из этих параметров или оба сразу могут быть опущены при инициализации макроса RDBUFF.) Предложения в строках 44-48 этого макроопределения являются примером простой условной структуры периода макрогенерации. В предложении IF вычисляется булевское выражение, являющееся его операндом. Если значение этого выражения есть TRUE, то порождаются предложения, следующие за предложением IF до тех пор, пока не встретится предложение ELSE. В противном случае эти предложения опускаются и порождаются предложения, следующие за ELSE. Предложение ENDIF завершает условную конструкцию, начатую предложением IF. (Как обычно, альтернатива ELSE может быть опущена.) Таким образом, если значением параметра &MAXLTH является пустая строка (это означает, что соответствующий аргумент был опущен в предложении макроинициализации), то будет порождено предложение в строке 45. В противном случае будет порождено предложение в строке 47. Аналогичная конструкция содержится в строках 26-28. Обратите внимание, что предложение, следующее за IF, не является строкой, которая должна быть порождена в процессе макрогенерации. Это предложение является директивой макропроцессора (SET), которая присваивает значение 1 переменной &EORCK. Подобные переменные называются переменными периода макрогенерации. Они позволяют хранить нужные значения в период порождения текста макрорасширения. Любой идентификатор, начинающийся символом & и не являющийся параметром макроопределения, является переменной периода макрогенерации. Считается, что начальное значение всех таких переменных равно нулю. Таким образом, если в нашем примере в предложении макроинициализации будет задан аргумент, соответствующий параметру &EOR, то значение параметра &EOR не является пустой строкой, и переменной &EORCK будет присвоено значение 1. В противном случае она сохранит свое исходное значение нуль. Значение этой переменной периода макрогенерации используется в условных структурах в строках с 38 по 43 и с 63 по 73. В предыдущем примере переменная периода макрогенерации &EORCK использовалась для запоминания результата анализа параметра &EOR (строка 26). В предложениях IF (строки 38 и 63) можно было бы, конечно, вместо использования переменной &EORCK повторить исходное сравнение, в .результате которого это значение было получено. 25 RDBUFF MACRO &INDEV,&BUFADR,&RECLTH/&EOR,&MAXLTH 26 27 &EORCK 28 30 35 38 40 42 44 45 46 47 48 50 $LOOP УСТРОЙСТВА 55 60 63 65 70 75 80 МАКСИМАЛЬНОЙ 85 90 $EXIT 95 IF (&EOR NE ' ') SET 1 ENDIF CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА CLEAR А IF (&EORCK EQ 1) LDCH =X'&EOR' УСТАНОВКА СИМВОЛА EOR RMO A,S IF (&MAXLTH EQ ' ') +LDT #4096 МАКСИМАЛЬНАЯ ДЛИНА ЗАПИСИ = 4096 ELSE +LDT #&MAXLTH УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ ENDIF TD =X'&INDEV' ПРОВЕРКА ГОТОВНОСТИ JEQ RD IF COMPR JEQ STCH JLT STX MEND $LOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ =X'&INDEV' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А (&EORCK EQ 1) A,S ПАРОВЕРКА НА КОНЕЦ ЗАПИСИ $EXIT ВЫХОД ИЗ ЦИКЛА ЕСЛИ КОНЕЦ ЗАПИСИ &BUFADR,X ЗАПИСЬ СИМВОЛА В БУФЕР TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ $LOOP ДЛИНЫ ЗАПИСИ &RECLTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ а 30 35 40 42 47 50 $AALOOP 55 60 65 70 ЗАПИСИ 75 80 МАКСИМАЛЬНОЙ 85 90 $AAEXIT RDBUFF F3,BUF,RECL,04,2048 CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА CLEAR А LDCH =X'&EOR' УСТАНОВКА СИМВОЛА EOR RMO A,S +LDT #2048 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ TD =X'F3' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА JEQ $AALOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ RD =X'F3' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А COMPR A,S ПАРОВЕРКА НА КОНЕЦ ЗАПИСИ JEQ $AAEXIT ВЫХОД ИЗ ЦИКЛА ЕСЛИ КОНЕЦ STCH BUF,X ЗАПИСЬ СИМВОЛА В БУФЕР TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ JLT STX $AALOOP ДЛИНЫ ЗАПИСИ &RECL СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ б 30 35 47 RDBUFF 0E,BUFFER,LENGTH,,80 CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА CLEAR А +LDT #80 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ 50 $ABLOOP TD 55 JEQ 60 RD 75 STCH 80 МАКСИМАЛЬНОЙ 85 JLT 90 $ABEXIT STX =X'0E' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА $ABLOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ =X'0E' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ $ABLOOP LENGTH ДЛИНЫ ЗАПИСИ СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ в 30 35 40 42 47 50 $ACLOOP 55 60 65 70 ЗАПИСИ 75 80 МАКСИМАЛЬНОЙ 85 90 $ACEXIT CLEAR CLEAR LDCH RMO +LDT TD JEQ RD COMPR JEQ RDBUFF F1,BUFF,RLENG,04 X ОЧИСТКА СЧЕТЧИКА ЦИКЛА А =X'04' УСТАНОВКА СИМВОЛА EOR A,S #4096 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ =X'F1' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА $ACLOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ =X'F1' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А A,S ПАРОВЕРКА НА КОНЕЦ ЗАПИСИ $ACEXIT ВЫХОД ИЗ ЦИКЛА ЕСЛИ КОНЕЦ STCH BUF,X ЗАПИСЬ СИМВОЛА В БУФЕР TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ JLT STX $ACLOOP RLENG ДЛИНЫ ЗАПИСИ СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ г Рис. 4.8. Использование условных предложений периода макрогенерации Однако использование переменной периода макрогенерации оправданно здесь хотя бы уже потому, что оно указывает на то, что в обоих предложениях IF используются одинаковые условия. Кроме того, проверка значения переменной может оказаться быстрее, чем повторение исходного сравнения, особенно если оно связано с вычислением сложных булевских выражений. На рис. 4.86-г изображены макрорасширения, порожденные тремя различными макроинициализациями, иллюстрирующие работу предложений IF во фрагменте на рис. 4.8а. Вы должны внимательно разобраться с этими примерами и убедиться, что понимаете, каким образом было получено данное макрорасширение, исходя из текста макроопределения и предложения макроинициализации. Реализация только что описанных средств условной макрогенерации достаточно проста. Макропроцессор должен вести таблицу имен переменных периода макрогенерации, содержащую их значения. Эта таблица должна пополняться либо модифицироваться при обработке предложений SET. Когда бы ни понадобилось текущее значение переменной периода макрогенерации, оно берется из этой таблицы. Когда в процессе макрогенерации встречается предложение IF, вычисляется соответствующее булевское выражение. Если оно истинно, то макропроцессор продолжает обрабатывать строки из таблицы DEFTAB до тех пор, пока не встретит предложение ELSE или ENDIF. Если встретилось предложение ELSE, то макропроцессор пропускает строки в таблице DEFTAB до предложения ENDIF. После предложения ENDIF макропроцессор продолжает работать в обычном режиме. Если же значение булевского выражения предложения IF ложно, то макропроцессор пропускает строки в таблице DEFTAB до тех пор, пока не встретит ELSE или ENDIF. Далее макропроцессор продолжает работать, как обычно. Предложенная выше реализация не допускает вложенных конструкций IF. Вы можете подумать о том, как она должна быть изменена, чтобы обрабатывать подобные структуры (см. упр. 4.2.8). Чрезвычайно важно понять, что проверка истинности булевских выражений в предложениях IF осуществляется на этапе макрогенерации. К началу работы ассемблера все подобные решения уже приняты. Образовалась только одна последовательность предложений готовой программы (например, предложения на рис. 4.8в), а все директивы условной макрогенерации исключены. Таким образом, предложения IF периода макрогенерации дают программисту удобные средства записи различных вариантов своей программы. Эти средства существенно отличны от предложений, подобных COMPR (или предложений IF в языках высокого уровня), которые осуществляют свои проверки во время выполнения программы. То же относится и к присваиванию значений переменным периода макрогенерации и другим директивам условной макрогенерации, которые мы еще обсудим. Условная конструкция периода макрогенерации IF-ELSE- ENDIF является механизмом, позволяющим однократно перенести или пропустить некоторые строки из тела макроопределения в результирующую программу. На рис. 4.9а приведен пример условных предложений период а макрогенерации другого типа. На нем изображено модифицированное определение макроса RDBLJFF, назначение и функции которого те же, что и раньше. Но с помощью этого макроса программист может определить целый список признаков конца записи. Например, в предложении макроинициализации на рис. 4.96 параметру EOR соответствует список (00, 03, 04). Любой из этих кодов является признаком конца записи. Для упрощения макроопределения параметр &MAXLTH удален; максимальная длина записи всегда полагается равной 4096. В макроопределении на рис. 4.9а используется циклическая конструкция WHILE периода макрогенерации. Предложение WHILE указывает на то, что последующие строки тела макроопределения до ближайшего предложения ENDW должны многократно переноситься в результирующую программу до тех пор, пока некоторое условие является истинным. Как и раньше, проверка этого условия и зацикливание осуществляются в период макрогенерации. В проверяемых условиях могут использоваться переменные периода макрогенерации и аргументы, но не могут быть использованы значения, вычисляемые в период выполнения программы. 25 RDBUFF 27 &EORCK 30 35 45 50 $LOOP УСТРОЙСТВА 55 60 63 &CTR 64 65 70 71 &CTR 73 MACRO &INDEV,&BUFADR,&RECLTH,&EOR SET %NITEMS(&EOR) CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА CLEAR А +LDT #4096 МАКСИМАЛЬНАЯ ДЛИНА ЗАПИСИ = 4096 TD =X'&INDEV' ПРОВЕРКА ГОТОВНОСТИ JEQ RD SET WHILE COMP JEQ SET ENDW $LOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ =X'&INDEV' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 1 (&CTR LE &EORCT) =X'0000&EOR[&CTR]' $EXIT &CTR+1 75 80 МАКСИМАЛЬНОЙ 85 90 $EXIT 95 STCH JLT STX MEND &BUFADR,X ЗАПИСЬ СИМВОЛА В БУФЕР TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ $LOOP ДЛИНЫ ЗАПИСИ &RECLTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ а RDBUFF F2,BUFFER,LENGTH,(00,03,04) 30 CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА 35 CLEAR А 45 +LDT #4096 МАКСИМАЛЬНАЯ ДЛИНА ЗАПИСИ = 4096 50 $AALOOP TD =X'F2' ПРОВЕРКА ГОТОВНОСТИ УСТРОЙСТВА 55 JEQ $AALOOP ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ 60 RD =X'F2' ЧТЕНИЕ СИМВОЛА В РЕГИСТР А 65 COMP =X'000000' 70 JEQ $AAEXIT 65 COMP =X'000003' 70 JEQ $AAEXIT 65 COMP =X'000004' 70 JEQ $AAEXIT 75 STCH BUFFER,X ЗАПИСЬ СИМВОЛА В БУФЕР 80 TIXR T ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ 85 JLT $AALOOP ДЛИНЫ ЗАПИСИ 90 $EXIT STX LENGTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ б Рис.4.9. Использование предложений условного перехода периода макрогенерации. Примером использования конструкции WHILE-ENDW являются строки 63-73 на рис. 4.9а. Переменной периода макрогенерации &EORCT предварительно (строка 27) было присвоено но значение %NITEMS(&EOR). %М1ТЕМS - это макропроцессорная функция, значением которой является количество элементов в списке, задаваемом в качестве аргумента этой функции. Например, если значение аргумента, соответствующее параметру EOR, есть (00, 03, 04), то значение %NITEMS(&EOR) будет равно 3. Переменная периода макрогенерации &CTR используется для подсчета того, сколько раз сгенерированы (перенесены из тела макроопределения в результирующую программу) строки, следующие за предложением WHILE. Начальное значение &CTR полагается равным 1 (строка 63) и далее увеличивается на 1 при каждом проходе по циклу (строка 71). Само предложение WHILE говорит о необходимости повтора цикла периода макрогенерации до тех пор, пока значение переменной &CTR меньше или равно значению переменной &EORCT. Это означает, что строки 65-70 будут сгенерированы для каждого элемента списка. Переменная &CTR используется в качестве индекса для выборки соответствующего элемента списка. На первой итерации выражение &EOR [&CTR] в строке 65 будет иметь значение 00, на второй итерации - значение 03 и т.д. На рис. 4.96 изображено расширение предложения макроинициализации с использованием макроопределения на рис. 4.9а. Вы должны тщательно проанализировать этот пример для того, чтобы убедиться, что вы правильно понимаете смысл предложения WHILE. Реализация цикла WHILE периода макрогенерации также достаточно проста. Когда макропроцессор встречает предложение WHILE, он вычисляет соответствующее булевское выражение. Если оно ложно, то макропроцессор пропускает строки в таблице DEFTAB до предложения ENDW и продолжает далее работать, как обычно, в режиме безусловной макрогенерации. Если же это выражение истинно, то макропроцессор продолжает обрабатывать строки таблицы DEFTAB обычным образом до предложения ENDW. По предложению ENDW макропроцессор возвращается к предложению WHILE, перевычисляет соответствующее булевское выражение и действует далее в соответствии с этим новым вычисленным значением так, как уже описано. Предложенный метод реализации не допускает вложенных конструкций WHILE. Вы можете подумать над тем, каким алгоритмом могут поддерживаться такие вложенные конструкции (см. упр. 4.2.12). 4.3. Варианты построения макропроцессоров В этом разделе мы обсудим некоторые базовые варианты построения макропроцессоров. Представленный на распечатке 18 алгоритм не сработает, если внутри тела макроопределения встретится предложение макроинициализации. Часто, однако, желательно использовать макросы именно таким образом. В разд. 4.3.1 изучается эта проблема и предлагаются некоторые пути ее решения. Хотя подавляющее большинство макросредств используется в связи с программированием на языке ассемблера, имеются также и другие приложения. В разд. 4.3.2 обсуждается макропроцессор общего назначения, не связанный ни с каким конкретным языком программирования. В разд. 4.4 приведен пример подобного макропроцессора. В разд. 4.3.3 обсуждается другая сторона того же вопроса: интеграция макропроцессора с конкретным ассемблером или компилятором . Мы обсудим возможности кооперации 10 RDBUFF 15 . 20 . 25 . 30 35 40 45 50 $LOOP УСТРОЙСТВА 65 70 ЗАПИСИ 75 80 85 90 $EXIT 95 MACRO &BUFADR, &RECLTH, &INDEF МАКРОС ДЛЯ ЧТЕНИЯ ЗАПИСИ В БУФЕР CLEAR X ОЧИСТКА СЧЕТЧИКА ЦИКЛА CLEAR A CLEAR S +LDT #4096 УСТАНОВКА МАКС. ДЛИНЫ ЗАПИСИ RDCHAR &INDEV ПРОВЕРКА ГОТОВНОСТИ COMPR JЕQ A,S $EXIT ПРОВЕРКА НА КОНЕЦ ЗАПИСИ ВЫХОД ИЗ ЦИКЛА ЕСЛИ КОНЕЦ STCH TIXR JLT SIХ MEND &BUFADR,X ЗАПИСЬ СИМВОЛА В БУФEP Т ЦИКЛ ДО ДОСТИЖЕНИЯ МАКСИМАЛЬНОЙ $LOOP ДЛИНЫ ЗАПИСИ &RECLTH СОХРАНЕНИЕ ДЛИНЫ ЗАПИСИ а. 5 10 15 20 RDCHAR MACRO &IN . . МАКРОС ДЛЯ ЧТЕНИЯ СИМВОЛА В РЕГИСТР А . 25 30 35 40 ТD JEQ RD MEND =X'&IN' ПРОВЕРКА ВХОДНОГО УСТРОЙСТВА *-3 ЦИКЛ ОЖИДАНИЯ ГОТОВНОСТИ =X'&IN' ЧТЕНИЕ СИМВОЛА б RDBUFF ARGTAB: Параметр 1 2 3 4 . . BUFFER,LENGTH,F1 в Значение BUFFER LENGTH F1 (He используется) . . г АRGТAВ: Параметр Значение 1 F1 2 (He . используется) . . . д между макропроцессором и транслятором и укажем на потенциальные преимущества и проблемы, которые возникают при такой интеграции. 4.3.1. Рекурсивная макрогенерация На рис. 4.3 был представлен пример определения одной макроинструкции внутри другой. Мы, однако, не встречались с тем, чтобы внутри тела макроопределения были предложения макроинициализации. На рис. 4.11 представлен пример подобного использования макросов. Макроопределение RDBUFF на рис. 4.11а в основном то же, что и на распечатке 16 . Для наглядности изменен только порядок параметров. В этом примере мы предполагаем, что макроопределение RDCHAR уже описано. Его функцией является чтение одного символа с заданного устройства в регистр А с учетом необходимых проверок готовности устройства. Соответствующее макроопределение изображено на рис. 4.11б. Использование макроса, подобного RDCHAR, весьма удобно при определении макроса RDBUFF. Оно позволяет программисту при написании макроса RDBUFF не заботиться о деталях управления и доступа к устройству. (Макрос RDCHAR мог быть написан в другое время или даже другим программистом.) Использование такого макроса даст еще большие преимущества на более сложной машине, на которой чтение одного символа осуществляется более длинной и сложнойчем в нашем случае, программой. К сожалению, изложенный выше алгоритм работы макропроцессора не позволяет обрабатывать инициализации макросов внутри других макросов. Предположим, например, что алгоритм на распечатке 18 обрабатывает предложение макроинициализации на рис. 4.11в. После того как предложение макроинициализации будет распознанобудет вызвана процедура EXPANDING. Аргументы из предложения макроинициализации будут помещены в таблицу ARGTAB, как это изображено на рис. 4.11г. Булевской переменной EXPANDING будет присвоено значение TRUE, и начнется процесс макрорасширения. Он будет протекать правильно до строки 50, содержащей инициализацию макроса RDCHAR. В этом месте процедура PROCESSLINE обратится к процедуре EXPAND еще раз. Таблица ARGTAB будет преобразована к виду, представленному на рис. 4.11д. Расширение макроса RDCHAR также осуществится правильно, однако после этого возникнут трудности. По концу макроопределения RDCHAR переменной EXPANDING будет присвоено значение FALSE. Таким образом, макропроцессор "забудет", что он находился в середине процесса макрорасширения, когда встретил предложение инициализации макроса RDCHAR. Кроме того, аргументы первой макроиницмализации (RDBUFF) оказались потерянными, поскольку их значения в таблице ARGTAB затерлись новыми аргументами макроса RDCHAR. Основной причиной этих трудностей является рекурсивный вызов процедуры EXPAND. Сначала она будет вызвана при обработке предложения макроинициализации макроса RDBUFF. Далее она вызывает процедуру PROCESSLINE (строка 50), которая делает еще одно обращение к EXPAND до возврата из первого вызова этой процедуры. Те же трудности возникнут и с процедурой PROCESSLINE, поскольку она также вызывается рекурсивно. Например, возникает вопрос, куда передавать управление после конца работы процедуры PROCESSLINE: в основной цикл работы макропроцессора или же в цикл внутри процедуры EXPAND. Эти трудности легко разрешимы, если макропроцессор пишется на языке программирования, допускающем рекурсивные вызовы (таком как Паскаль пли ПЛ/1). Сам компилятор позаботится о том, чтобы значения всех переменных, определенных внутри процедуры, были сохранены при рекурсивном обращении. Компилятор позаботится также и о других деталях обработки рекурсивных вызовов, включая определение адреса возврата из процедуры. (В гл. 5 мы детально обсудим, как компилятор обрабатывает подобные рекурсивные вызовы.) Если же допускающий рекурсию язык программирования недоступен, то программист обязан сам позаботиться о таких вещах, как адрес возврата и значения локальных переменных. В этом случае PROCESSLINE и EXPAND, может быть, вообще не будут оформлены как процедуры. Вместо этого та же логика работы может быть реализована с помощью операторов цикла с хранением значений переменных в стеке. Используемые здесь методы реализации описаны в гл. 5 при обсуждении рекурсии. Пример подобной реализации содержится в работе Донован [ 1972]. 4.3.2. Макропроцессоры общего назначения Наиболее часто макропроцессоры используются при программировании на языке ассемблера. Часто такие макропроцессоры встроены в ассемблер. Специализированные макропроцессоры были созданы также для некоторых языков программирования высокого уровня. (См., например,. Кериган и Плаугер [1976].) Эти специализированные макропроцессоры во многом аналогичны с точки зрения выполняемых функций и используемого подхода. Их различия связаны в основном с особенностями конкретного языка программирования. В этом разделе мы обсудим макропроцессоры общего назначения. Они не зависят ни от какого конкретного языка программирования и могут быть использованы с целым рядом различных языков. Преимущества такого общего подхода к построению макропроцессоров очевидны. Программист не должен изучать специализированные макросредства для каждого компилятора или языка ассемблера. В результате экономится много времени и средств при обучении. Стоимость разработки макропроцессора общего назначения несколько больше по сравнению со специализированными макропроцессорами. Однако эти затраты не будут повторяться для каждого языка. Результатом будет существенное сокращение общих затрат на разработку и сопровождение математического обеспечения. На протяжении ряда лет экономия на сопровождении может превысить первоначальные затраты на разработку математического обеспечения. Несмотря на все эти преимущества, в настоящее время имеется сравнительно мало макропроцессоров общего назначения. Одна из причин подобного положения дел связана с огромным количеством деталей, которые необходимо иметь в виду при программировании на любом реальноиспользуемом языке. Эти многочисленные детали могут быть встроены внутрь специализированного макропроцессора. Макропроцессор же общего назначения должен предоставлять пользователю средства, позволяющие ему самому определить необходимый набор правил, которому должен следовать макропроцессор. При работе с обычными языками программирования встречается несколько ситуаций, в которых обычная подстановка макропараметров не должна осуществляться. Например, комментарии, как правило, игнорируются макропроцессором (по крайней мере при обработке списка параметров). Каждый язык программирования имеет свои собственные средства для идентификации комментариев. Они могут идентифицироваться с помощью ключевых слов (как в Алголе) или жe с помощью специальных символов в начале и в конце комментария (как в Паскале). Некоторые языки, например Фортран, используют, специальный символ, означающий, что вся строка является комментарием. В большинстве языков ассемблера любые символы в строке, следующие за полем операнда, автоматически рассматриваются как комментарии. В некоторых языках используется специальный символ разделитель. Все символы, находящиеся после него в строке, рассматриваются как комментарии. Другие различия между языками программирования связаны со средствами группирования отдельных элементов языка, выражений или предложений. Однако некоторые языки используют другие символы вместо круглых скобок (например, символы [и]). Некоторые языки используют для этих целей ключевые слова begin и end. Более общая проблема состоит в структуре лексем языка программирования, например идентификаторов, констант, операторов, ключевых слов. Языки существенно различаются по ограничениям, которые они накладывают на длину идентификаторов и правила написания констант. В некоторых случаях правила построения подобных лексем различны в разных частях программы (например, в предложении FORMAT языка Фортран или в предложении DATA DIVISION в языке Кобол). В некоторых языках имеются операторы, записываемые несколькими символами, такие как ** в языке Фортран и := в Паскале. Если макропроцессор будет воспринимать их как два независимых символа, а не как один оператор, могут встретиться определенные труднгсти. Даже формат записи предложений исходной программы во входном файле может привести к определенным трудностям. Макропроцессор должен учитывать, являются ли пробелы разделителями или должны полностью игнорироваться, способы записи одного предложения в нескольких строках, специальные соглашения по форматированию предложений, подобные принятым в языках Фортран и Кобол. Другая проблема, которая может возникнуть при разработке макропроцессора общего назначения, связана с синтаксисом предложений макроопределение и макропроцессоров предложения макроинициализации весьма похожи на другие предложения базового языка программирования. (Например, инициализация макроса RDBUFF на рис. 4.1. имеет ту же форму, что и предложение на языке ассемблера УУМ.) Эта схожесть форм имеет целью облегчить написание и чтение программ. Этого трудно достичь для макропроцессора общего назначения, который предназначен для работы с языками программирования, имеющими различный формат предложений. В разд. 4.4.3 мы кратко опишем макропроцессор общего назначения, в котором решены многие из перечисленных выше вопросов. Обсуждение макропроцессоров общего назначения и макропроцессоров для языков программирования высокого уровня содержится в работах Коул [1981], Браун [1974] и Кэмпбел-Келли [1973]. 4.3.3. Макропроцессоры, встроенные в трансляторы Все обсуждавшиеся до сих пор макропроцессоры могут быть названы препроцессорами. Они обрабатывают макроопределения и предложения макроинициализации, генерируя расширен ную версию исходной программы. Эта расширенная программа далее используется в качестве входной для ассемблера или компилятора. В этом разделе мы обсудим другую альтернативу-реализацию функций макропроцессора самим транслятором. Простейший способ достижения подобного объединения состоит в использовании принципа "строка за строкой". В этом случае макропроцессор читает предложение исходной программы и выполняет все описанные выше функции. При этом строки, сгенерированные макропроцессором, передаются на вход транслятора по мере того, как они генерируются (последовательно одна за одной), вместо того, чтобы писать расширенную программу в файл. В этом случае макропроцессор выполняет функции, аналогичные подпрограмме чтения очередной строки для ассемблера или компилятора. Подобный подход имеет несколько преимуществ. Он позволяет избежать дополнительного прохода по программе (записи ее в промежуточный файл и последующего чтения) и в результате может быть более эффективным, чем использование макропроцессора. Некоторые таблицы, требующиеся и макропроцессору, и транслятору, могут быть объединены. Напримертаблица ОВТАВ в ассемблере и таблица NAMTAB в макропроцессоре могут быть реализованы в виде одной таблицы. Кроме того, многие вспомогательные программы и функции могут использоваться как транслятором, так и макропроцессором. Сюда относятся операции сканирования входной строки, поиска по таблицам, преобразование числовых констант из внешнего во внутреннее представление. В рамках этого подхода облегчается привязка диагностических сообщений к предложению исходной программы, вызвавшей ошибку (т. е. к соответствующему предложению макроинициализации). При использовании макропроцессора подобная ошибка может быть отнесена только к соответствующему предложению макрорасширения. Программист будет в этом случае самостоятельно искать исходную причину ошибки. Хотя макропроцессор, реализующий принцип "строка за строкой", и может использовать те же подпрограммы, что и транслятор, по-прежнему его функции и функции транслятора остаются существенно различными. Основной формой связи между ними является передача предложений программы с выхода одного на вход другого. Однако можно себе представить еще более тесное взаимодействие между макропроцессором и ассемблером. Речь идет о макропроцессоре, встроенном в транслятор. Встроенный макропроцессор потенциально может использовать любую информацию об исходной программе, имеющуюся у транслятора. Объем этой информации существенно различается в разных системах. При относительно простом варианте взаимодействия макропроцессор использует такие операции транслятора, как сканирование, обработка констант и т. д. Эти операции в любом случае должны осуществляться компилятором или ассемблером; макропроцессор просто использует их результаты, не вдаваясь в такие детали, как изображение одной операции языка несколькими символами, расположение одного предложения языка на нескольких строках, формат лексем и т. п. Это особенно важно в тех случаях, когда эти правила различаются в разных частях программы (например, внутри операторов FORMAT и символьных констант языка Фортран). Сканирование лексем, о котором только что шла речь, концептуально очень просто. Однако многие существующие языки программирования имеют особенности, доставляющие определенные трудности и на этом этапе. Классическим примером является предложение Фортрана DO 100 I = 1, 20 Это оператор цикла DO, где DO распознано как ключеное слово, 100 метка предложения, 1-имя переменной и т. д. Поскольку пробелы никак не учитываются в предложениях Фортрана (кроме символьных констант), похожее предложение DO 100 I = l имеет совершенно другой смысл. Это оператор присваивания переменной DO100I значения 1. Таким образом, правильная интерпретация термов DO100, I не может быть осуществлена до тех пор, пока не проведен анализ всего предложения. Такая интерпретация будет очень важна, например, в случае, если макропроцессор должен заменить переменную с именем I. Компилятор с Фортрана обязан разбираться с подобными ситуациями. Однако для обычного макропроцессора (нe встроенного в компилятор) это сделать очень трудно. Такой макропроцессор может иметь дело с предложениями исходной программы только как со строками символов, не имея возможности выделить их отдельные элементы. При более тесной связи с транслятором встроенный макропроцессор может обрабатывать макроинструкции, смысл которых зависит от того контекста, в котором они оказались. На пример, с помощью такого макроса можно задать замену имен только переменных или констант определенного типа либо только переменных, являющихся переменными цикла в предложениях DO. Процесс макрогенерации может также зависеть от множества характеристик аргументов предложения макроинициализации. (Примером может служить описание макропроцессора System/370 в разд. 4.5.1.) Макропроцессоры, тесно связанные с транслятором, имеют, конечнои свои недостатки. Они должны быть спроектированы специально для работы с конкретной реализацией некоторого ассемблера или компилятора (а не просто для работы с языком программирования). Стоимость разработки такого макропроцессора должна быть прибавлена к стоимости разработки транслятора, что приводит к более дорогим компонентам математического обеспечения. Кроме того, ассемблер или компилятор становится больше и сложнее, чем это было бы в случае использования макропропроцессора. Размер транслятора может представлять собой проблему, если транслятор предназначен для работы на ЭВМ с ограниченной оперативной памятью. В любом случае увеличиваются накладные расходы на трансляцию. (Некоторые ассемблеры со встроенными макропроцессорами требуют больше времени на обработку одной строки исходной программы, чем некоторые компиляторы на той же самой ЭВМ.) Решение вопроса о типе макропроцессора, который должен использоваться, опирается на данные о предполагаемой частоте и сложности макрогенерации и другие характеристики операционного окружения. 4.4. Примеры реализации В этом разделе мы кратко опишем три реальных макропроцессора. Как и раньше, не будем пытаться охватить все характеристики каждой системы, а сосредоточимся на наиболее интересных особенностях. Первые два примера посвящены макропроцессорам, тесно связанным с ассемблером (для System/370 и ЭВМ серии VAX). Третий пример макропроцессор общего назначения, предназначенный для использования в качестве препроцессора с самыми различными языками программирования. 4.4.1. Макропроцессор System/370 Рассматриваемый в этом разделе макропроцессор Sуstem/370 тесно связан с ассемблером этой системы. Он обеспечивает все функции макропроцессора, которые мы обсуждали, включая обработку макроопределении и макроинициализаций внутри тела макроопределения. В макроииструкциях могут использоваться позиционные или ключевые параметры либо смесь позиционных и ключевых параметров. Управляющие предожения позволяют пользователю задавать различные режимы работы макропроцессора, например разрешить или запретить появление предложений текста макрорасширения в листинге исходной программы. Комментарии в теле макроопределений могут игнорироваться, а могут и остаться в листинге ассемблера в зависимости от способа их записи. Существенное отличие макропроцессора System/370 от рассмотренного нами макропроцессора для УУМ состоит в структуре предложений условной макрогенерации. В макроязыке для System/370 они называются предложениями условного ассемблирования. Хотя эти предложения и предназначены в основном для макрогенерации, они могут появиться также и вне макроопределений. Язык ассемблера System/370, дополненный операторами условного ассемблирования, допускает использование переменных периода ассемблирования, которые аналогичны обсуждавшимся выше переменным периода макрогенерации. Этим переменным могут быть присвоены арифметические, двоичные или символьные значения. Этим трем типам значений соответствуют три типа таких переменных. Переменные периода ассемблирования могут быть как локальными, так и глобальными. На локальные переменные можно ссылаться только внутри того макроопределения или такой подпрограммы на ассемблере, которые содержат их определения. Если локальные переменные, имеющие одно имя, будут определены в нескольких макроопределениях, то транслятор будет рассматривать их как различные переменные. Глобальные переменные могут использоваться в любом месте программы. Так, например, глобальной переменной может быть присвоено значение в момент обработки одного макроса, а использоваться это значение может в другом макросе. Предложения условного ассемблирования, управляющие порождением строк результирующей программы, существенно отличны от условных предложений периода макрогенерацип для УУМ. Базовой управляющей конструкцией является оператор перехода периода ассемблирования. При выполнении такого перехода ассемблер (или макропроцессор) прерывает последовательную обработку предложений программы и в качестве очередного предложения берет то, которое указано в операторе перехода. Эти переходы могут быть как "вниз", так и "вверх" по входному потоку. На рис. 4.12 изображены примеры макроопределения и текста макрорасширения с использованием макроязыка System/370. Это макроопределение предназначено для генерации кода, осуществляющего сложение двух элементов данных и запоминание результата. В зависимости от типа аргументов - целые или с плавающей точкой порождается различный код. Если аргументы имеют какой-либо другой или не совпадающий между собой тип, то генерируется сообщение об ошибке. Форма макроопределения на рис. 4.12а в основном та же, что и в наших предыдущих примерах. Макроопределение начинается предложением MACRO и заканчивается предложением MEND. 1 2 3 4 5 6 7 8 9 10 11 12 13 &NAМE .FLOAT &TYPE .INTGR &.NAME MACRO ADD LCLC AIF AIF AIF AGO ANОР SETC ANOP L&.TYPE A&.TYPE ST&.TYPE &OP1,&OP2,&SUM &TYPE (T'&OP1 NE Т'&ОР2) .MIXTYP (T'&OP1 EQ 'F') .INTGR (T'&OP1 EQ 'E') .FLOAT .TYPERR 'E' 2,&OP1 2,&OP2 2,&SUM 14 15 16 17 18 .MIXTYP .TYPERR MEXIT MNOTE MEXIT MNOTE MEND 'СМЕШАННЫЕ ТИПЫ ОПЕРАНДОВ' 'НЕДОПУСТИМЫЙ ТИП ОПЕРAНДОВ' a LAB ADD I,J,K LAB L A ST 2,I 2,J 2,K б ADD X,Y,Z LE AE STE 2,X 2,Y 2,Z в ADD I,Y,Z *** СМЕШАННЫЕ ТИПЫ ОПЕРАНДОВ г Рис. 4.12. Примеры макроопределения и макрорасширений для System/370. Макропрототип расположен сразу же за предложением MACRO (строка 2).Все макропараметры и переменные периода ассемблирования начинаются символом &. В этом примере используются только позиционные параметры. Предложение LCLC в строке 3 определяет переменную &TIPE строкового типа. Эта переменная определена как локальная в том макроопределении, в котором она описана. По умолчанию ей присваивается начальное значение - пустая строка. Предложение в строке 4 (AIF) является предложением условного перехода периода ассемблирования. Ассемблер вычисляет булевское выражение в скобках и переходит на обработку другого места исходной программы, если это выражение истинно. В противном случае ассемблер продолжает обрабатывать следующую строку исходной программы. В нашем примере адрес перехода задается с помощью специальной метки MIXTYP, которая описана в строке 15. Аргументами булевского выражения в предложении AIF являются типы параметров макроопределения. Тип параметра &OP1 (обозначаемый T'&OP1) есть символ F, если соответствующий параметр является целой переменной длиной в одно слово, и символ Е, если он является числом с плавающей точкой. Таким образомпредложение AIF в строке 4 определяет переход к строке 15, если типы двух параметров различны. Если же их типы совпадают, то ассемблер продолжает работу со строки 5. Другое предложение AIF имеет аналогичную структуру. Предложение AGO в строке 7 является оператором безусловного перехода периода ассемблирования, предложение ANOP (строки 8 и 10) - пустым оператором. Эти предложения используются для описания меток FLOAT и INTGR. В строках 14-17 содержатся макродирективы двух новых типов. Предложение MNOTE вызывает печать сообщения об ошибке в листинге ассемблера. Реакция ассемблера на это сообщение будет такой же, как реакция на собственную ошибку. Предложение MEXIT предписывает макропроцессору закончить процесс порождения текста макрорасширения текущего предложения макроинициализации, несмотря на то что предложение MEND еще не встретилось. На рис. 4.12б-г изображены предложения макроинициализации и соответствующие им макрорасширения. Предполагается, что I, J и К являются целыми переменными длиной в одно слово, а X, Y, Z - переменными с плавающей точкой. Чтобы убедиться, что вы правильно понимаете работу предложений AIF и AGO, вам необходимо проследить по шагам весь процесс макрогенерации, используя макроопределение на рис.4.12а. В рассмотренном примере предложения AIF и AGO обеспечивают действия, эквивалентные конструкции IF-ELSE- ENDIF. Поскольку цикл WHILE также может быть запрограммирован c использованием предложений AIF и AGO, эти средства дают широкоие возможности условного ассемблирования. Однако предложения AIF и AGO труднее реализовать (и, вообще говоря, менее удобно использовать), чем предложения IF и WHILE, используемые для УММ. Предложения условной макрогенерации для УУМ аналогичны управляющим операторам структурированного языка, подобного Паскалю. Предложения условного ассемблирования в System/370 больше похожи на средства неструктурированного языка, такого как Фортран IV. В примере на рис. 4.12 предложение AIF использовалось для проверки типа макроаргументов. Имеется множество других характеристик переменных и аргументов, которые могут использоваться сходным образом. Эти характеристики называются атрибутами соответствующих переменных и аргументов. В предложениях условного ассемблирования допустимо ссылаться на атрибуты объектов, определения которых еще не встречались в исходной программе. В результате ассемблер System/370 должен делать предварительный просмотр всей исходной программы, запоминая атрибуты объектов, которые будут определены лишь в процессе макрогенерации. Макропроцессор System/370 позволяет использовать ряд системных переменных. Можно считать, что они определены заранее и поэтому могут использоваться при макрогенерации. Примерами таких системных переменных могут служить текущая дата, время работы ассемблера, имя текущего управляющего раздела. Одна из системных переменных &SISNDX предназначена для генерации уникальных меток. Значением переменной &SISNDX является четырехразрядное целое число, первоначально равное 0001 и увеличивающееся на 1 при обработке каждого предложения макроинициализации. Используя эту переменную в качестве составной части метки, программист может избежать дважды определенных меток. Например, L&SISNDX может принять значения L0001, L0002 и т.д. при последовательной обработке предложений макроиициализации. Подробная информация о макропроцессоре System/370 содержится в IBM [1979] и IBM [1974]. 4.4.2. Макропроцессор системы VAX Макропроцессор системы VAX также сильно связан с ассемблером этой системы. Настолько сильно, что сам язык ассемблера назван VAX-11 и MACRO. Принципы записи макроопределений и предложений макроинициализаций те же, что и для УУМ, но есть и интересные отличия. Имена параметров макроинструкций не обязаны начинаться с & или какого-либо другого специального символа. В результате процесс сканирования для обнаружения параметров усложняется. Кроме того, чаще должен использоваться оператор конкатенации. Рассмотрим в качестве примера макроопределение на рис. 4.13а. В предложении 3 TSTL R'NUM операндом является символ R, конкатенированный со значением параметра NUM. Для обозначения операции конкатенации в макроязыке системы VAX используется апостроф '. Если бы этот операнд был записан просто как RNUM, то последовательность символов NUM не была бы распознана в качестве макропараметра. Таким образом, оператор конкатенации тут совершенно необходим. Сравните эту ситуацию с примером на рис. 4.12. В строке 13 этого примера не требуется никакого оператора конкатенации при записи терма ST&TYPE, поскольку амперсанд является признаком тогочто &TYPE является параметром. Вообще токаря, макропроцессор мог бы распознать последовательность символов NUM на рис. 4.12 как имя параметра без оператора конкатенации. Однако в этом случае макропроцессор должен был бы иметь средства, позволяющие запретить подстановку параметров (например, мы можем не за хотеть, чтобы NUMBER было превращено в 5BER). Если же макропараметр не сконкатенирован ни с какими другими символами, то апостроф, конечно же, не нужен. Макропроцессор системы VAX также предоставляет средства генерации уникальных локальных меток при макрогенерации. Программист описывает локальную метку, путем включения ее в список параметров макроопределения, добавив предварительно перед именем метки символ ?. Задав соответствующий аргумент в предложении макроинициализации, можно дать этой метке произвольное значение. Если же оно не определено, то ассемблер сгенерирует новую локальную метку. Метки, генерируемые ассемблеромнаходятся в диапазоне З0000$-65535$ Этот процесс проиллюстрирован макроопределением на рис. 4.13апредложениями макроиинциализации и соответствующими макрорасширениями на рис. 4.13б-в. В первом макрорасширении метка L1 заменена на З0000$; во втором - на З0001$. Программист может также определить локальные метки вне тела макроопределения (в предложении макроинициализации). Однако, чтобы не было пересечений с метками, генерируемыми макросредствами, документация ассемблера требует, чтобы метки пользователя не попадали в интервал З0000$ - 65535$. Вы можете сами проследить процесс получения макрорасширения на рис. 4.13, обращая внимание на использование операторов конкатенации и локальных меток. Дальнейшая информация о макропроцессоре системы VAX содержится в DEC [1972] и DEC [1979]. 1 2 3 4 5 6 L1: .МACRO SUB'SIZE'3 TSTL BGEQ MNEGL .ENDM ABSDIF OP1,OP2,SIZE.NUM,?L1 OPl,OP2,R'NUM R'NUM L1 R'NUM,R'NUM ABSDIF а ABSDIF X,Y,L,0 SUBL3 TSTL BGEEQ MNEGL X,Y,R0 R0 30000$ R0,R0 30000$: б Рис. 4.13. Примеры макроопределения и макрорасширений для ЭВМ серии VAX. ABSDIF I,J,W,2 SUBL3 TSTL BGEEQ MNEGL I,J,R2 R2 30001$ R2,R2 30001$: в Рис. 4.13. Примеры макроопределения и макрорасширений для ЭВМ серии VAX. 4.4.3. Макропроцессор общего назначения РМ В этом разделе мы дадим краткое описание макропроцессора общего назначения, имеющего имя РМ (Pattern Matching). Более подробное описание этой системы имеется в Сасс [1979]. Макроопределения и предложения макроинициализации макропроцессора РМ существенно отличаются от рассмотренных ранее. Средства записи макрошаблонов (макропрототипов), со ответствующие по назначению заголовкам макроопределений, имеют множество различных вариантов. Некоторые части шаблона могут быть вообще опущены; могут содержать набор альтернативных конструкций; могут повторяться столько раз, сколько необходимо. Процесс макрогенерации инициализируется при совпадении макрошаблона с некоторым фрагментом входного текста. Предложение макроинициализации (и даже его отдельные аргументы) может быть записано в нескольких строках исходного файла. Язык записи тел макроопределений похож на Алгол. Макропроцессор допускает определение пользователем локальных и глобальных меток периода макрогенерации. Имеются также системные переменные периода макрогенерации, аналогичные тем, с которыми мы встречались при анализе макропро цессора System/370. Одна из таких системных переменных является счетчиком количества обработанных предложений макроинициализации. Значение этой переменной может быть использовано для генерации уникальных меток. Допустимы также предложения условной макрогенерации, которые функционально (но не по способу записи) близки к условным предложениям макроязыка УУМ. Важной особенностью макропроцессора РМ является то, что пользователь может определить ряд языково-зависимых конструкций. Как мы уже говорили в разд. 4.4.2, учет специфики конкретного языка может представлять большие трудности при реализации макропроцессора общего назначения. Оператор skip этого макропроцессора является обобщением идеи комментария. Он выделяет порции входного текста, которые не обрабатываются или должны быть удалены. При этом пользователю предоставлена возможность определения синтаксиса оператора skip. Возможно также потребовать замену вхождений операторов skip на некоторые заданные строки символов. Оператор сору в РМ является обобщением идеи текстовой константыт. е. порции информации входного текста, которая должна быть непосредственно скопирована в выходной текст без анализа макропроцессором. Пользователь имеет возможность определить синтаксис оператора сору. Символы или лексемы, определяющие начало и конец копируемого куска текста, могут быть изменены в выходном файле. В дополнение к операторам skip и сору макропроцессор РМ имеет средства описания количества открывающихся и закрывающихся скобок, лексем, состоящих из нескольких символов, правила записи одного предложения на нескольких строках, а также средства обработки пробелов и признака "конец строки". На рис. 4.14 изображен пример макроопределения и предложения макроннициализации для макропроцессора РМ. Макроопределение на рис. 4.14а разработано для использования с языком Фортран. Предложение макроиинциализации по форме похоже на алгольное предложение for; выходной текст имеет формат Фортрана. Строка 1 определяет шаблон предложения макроинициализации: ключевое слово for, за которым следует параметр id, за которым следует ключевое слово from и т.д. Предложение, заключенное в скобки, определяет альтернативные варианты построения предложения макроиницпализации. Выражение в скобках может либо быть пустым (обозначено символом /), либо содержать конструкцию by. Таким образом, в предложении макроинициализации конструкция by может либо содержаться, либо быть опущена. Спецификация noneg, следующая за параметром body, определяет, что пробелы и символы "конец строки" являются значимыми (т. е. не должны игнорироваться) при обработке 1 macro 'for' id 'from' f ('by' b I /) 'to' t 'do' body:noneg 'od' 2 begin 3 <%id = %f 4 %snum IF (%id .GT. %t) GO TO %(snum+1) 5 %Ьоdi 6 %id = %id+>; 7 if b >< '' then <(%b)> else <i> fi; 8 <%/GО TO %snum 9 %(sunm+1) CONTINUE %/>; 10 snum: = snum+2; 11 end а for I from 0 to n-1 do S: = S + A(I) оd б 9000 9001 I=0 IF (I .GT. N-l) GO TO 9001 S = S + A(I) I = I + 1 GO TO 9000 CONTINUE в Рис.4.14. Примеры макроопределения и макрорасширений для макропроцессора РМ. значений макроаргументов. Тело макроопределения записано в алгольном стиле в строках 2-11. Текст, который должен появиться в макрорасширении, заключен в угловые скобки. В строках 3-6 содержатся четыре строки выходного текста. Внутри выходного текста символ % используется для выделения параметров и имен переменных периода макрогенерации. Так, например, строка 3 говорит о том, что в выходной файл должно быть переписано id, за которым следует знак равенства, за которым следует значение параметра f. Далее строка кончается. Конец строки входного текста порождает конец строки и в выходном тексте. Предполагается, что переменной периода макрогенерации snum ранее уже присвоено начальное значение 9000. Эта переменная используется для генерации меток предложений Фортрана. В результате обработки строки 7 в выходном тексте появится либо значение параметра b, либо 1 в зависимости от того, был ли задан аргумент, соответствующий параметру b. Его значение окажется на той же строке, которая начала образовываться при обработке строки 6 макроопределения. Символы %/которыми начинается строка 8, означают конец строки. Таким образомпредложение GOTO будет расположено в отдельной строке. Оператор присваивания в строке 10 увеличивает значение переменной snum периода макрогенерации таким образом, что в следующем макрорасширении метки оператора Фортрана не будут совпадать с ранее сгенерированными. На рис. 4.14б изображено предложение, которое будет обработано как предложение макроинициализации только что описанного макроопределения. На рис. 4.14в приведен сгенерированный текст. (Детали, определяющие формат выходных фортрановских строк, опущены.) Предполагаетсячто было определено соответствующее макроопределение, заменяющее символы := на символ = в операторе присваивания S:=S+A(I). Подробно проанализируйте этот пример, чтобы лучше понять процессы макроинициализации и макрогенерации, реализованные в макропроцессоре РМ. Упражнения Раздел 4.1 1. Примените приведенный на рис. 4.5 алгоритм для обработки исходной программы на рис. 4.1. Результат должен совпасть с изображенным на рис. 4.2. 2. Предложение макроинициализации является частью исходной программы. Во многих случаях программист не интересуется предложениями макрорасширения. Каким образом за счет взаимодействия макропроцессора и ассемблера можно добиться того, чтобы в листинге ассемблерной программы были только предложения макроинициализации и отсутствовали предложения макрорасширения? 3. Предположим, мы хотим видеть макроопределения внутри ассемблерного листинга. Каким образом макропроцессор и ассемблер могут это реализовать? 4. В большинстве случаев фрагменты комментариев не должны заменяться на значения макроаргументов, даже если они и совпадают с именами макропараметров. Каким образом можно предотвратить замену параметров внутри комментариев? 5. На основании чего программист должен принять решение о реализации некоторой конкретной логической функции с помощью подпрограммы или макроопределения? 6. Напишите алгоритм работы двухпросмотрового макропроцессоракоторый при первом просмотре обрабатывает все макроопределения, а на втором предложения макроинициализации. Можно считать, что вложенные друг в друга макроопределения запрещены и внутри макроопределений не могут встречаться предложения макроипициализации. 7. Модифицируйте приведенный на рис. 4.5 алгоритм таким образомчтобы макропроцессор брал нe описанное программистом макроопределение из библиотеки. 8. Предложите некоторый способ работы с таблицами DEFTAB и NAMTAB. 9. Предположим, что вхождения макропарамегров в таблице DEFTAB не заменены соответствующими позиционными обозначениями ?n. Какие изменения это потребует в алгоритме работы макропроцессора на рис. 4.5? Раздел 4.2 1. Макроопределение на рис. 4.1 содержит несколько предложений, в которых макропараметры сконкатенированы с другими символами (напримерстроки 50 и 75). Почему в этих предложениях нет необходимости использовать оператор конкатенации? 2. Модифицируйте приведенный на рис. 4.5 алгоритм так, чтобы он обрабатырал операторы конкатенации. 3. Модифицируйте приведенный на рис. 4.5 алгоритм так, чтобы предоставить пользователю средства генерации уникальных меток при макрогенерации. 4. Предположим, что мы хотим использовать метки внутри текста макрорасширения, не требуя, чтобы они имели какую-либо специальную форму (например, начинались символом %). Каждая такая метка предполагается описанной только внутри соответствующего макрорасширения, что исключает проблему дважды определенных меток. Каким образом макропроцессор и ассемблер должны взаимодействовать для обеспечения такой возможности? 5. В чем заключается наиболее существенное различие между следующими фрагментами: а) LDA ALPHA СОМP #0 JEQ SKIP LDA #З STA BETA SKIP . . . б) IF (&ALPHA NE 0) &BETA SET 3 ENDIF 6. Получите макрорасширения следующих двух предложений макроинициализации, используя макроопределения, приведенные на рис. 4.8 а) RDBUFF F1,BUFFER,LENGTH,00,1024 б) LOOP RDBUFF F2,BUFFER,LTH 7. Модифицируйте приведенный на рис. 4.5 алгоритм так, чтобы допустить использование операторов присваивания и конструкций IF-ELSE-ENDIF периода макрогенерации. Можно считать, что вложенные конструкции IF отсутствуют. 8. Дополните свой ответ к упр. 7 так, чтобы допустить наличие вложенных предложений IF. 9. В чем наиболее существенная разница между следующими двумя фрагментами: а) LDT #8 CLEAR X LOOP . . . TIXR T JLT LOOP б) &CTR SET 0 WHILE (&CTR LT 8) . . . &CTR SET &CTR+1 ENDW Используя макроопределения на рис. 4.9а. получите текст макрорасширении для следующих предложений макроинициализации: а) RDBUFF F1,BUFFER,LENGTH,(04,12) б) LABEL RDBUFF F1,BUFFER,LENGTH,00 в) RDBUFF F1,BUFFER,LENGTH Какое значение будет иметь функция %NITEMS(&EOR) в последних двух случаях? 11. Дополните свой ответ к упр. 7 так, чтобы включить предложение WHILE. Можете не заботиться о вложенных конструкциях WHILE. 12. Дополните свой ответ к упр. 11 так, чтобы допустить вложенные конструкции WHILE. 13. Переменные периода макрогенерации обычно рассматриваются как локальные для данного макроопределения. Таким образом, значение, присвоенное переменной периода макрогенерации, может использоваться только внутри того же самого макроопределения. В некоторых случаях, однакобыло бы удобно разрешить использование одной и той же переменной периода макрогеперации внутри двух связанных между собой макроопределений. Как это может быть реализовано? 14. Модифицируйте приведенный на рис. 4.5 алгоритм с тем, чтобы включить в него обработку ключевых параметров макроопределений. 15. Некоторые макропроцессоры допускают использование предложений макроинициализации, в которых одни параметры являются ключевыми, а другие - позиционными. Каким образом макропроцессор может обработать такого рода предложения макроинициализации? 16. Каким образом можно было бы задать значения по умолчанию для позиционных параметров? Какие изменения в алгоритме на рис. 4.5 пришлось бы сделать для введения такой возможности? 17. Вспомним макроопределение RDBUFF на рис. 4.8а. Каждое из нижеследующих предложений макроииициализапии этого макроопределения содержит ошибку. Какие из этих ошибок будут выявлены макропроцессором, а какие - ассемблером? а) RDBUFF F3,BUF,RECL,ZZ (неправильное значение для &EOF) б) RDBUFF F3,BUF,RECL,04,2048,01 (слишком много аргументов) в) RDBUFF F3,,RECL,04 (не определено значение для &BUFADR) г) RDBUFF F3,RECL,BUF (неправильный порядок аргументов) 10. Раздел 4.3 1. Предположим, что макропроцессор с логикой работы, аналогичной изображенной на рис. 4.5, должен обеспечивать рекурсивную макрогенерацию. При обсуждении было выявлено, что значения переменной EXPANDING и ARGTAB должны запоминаться при рекурсивных вызовах процедуры EXPAND. Какие другие значения также необходимо запоминать для разных способов реализации алгоритма? 2. Каким образом рекурсивный макропроцессор может быть реализован на языке макропроцессора? 3. Можно ли в нерекурсивном макропроцессоре допустить появление предложений макроипициализацпй внутри макроопределений? Каковы будут преимущества и недостатки такого подхода? Выберите два знакомых вам языка программирования высокого уровня. Какие особенности этих языков будут существенны при реализации макропроцессора для них? 5. Выберите какой-либо знакомый вам язык высокого уровня и язык ассемблера. Какие особенности этих двух языков будут существенны для реализации макропроцессора для них? 6. Опишите алгоритм взаимодействия макропроцессора, реализующего режим "строка в строку", с ассемблером. 7. Перечислите вспомогательные функции и процедуры, которые могли бы быть общими для ассемблера и встроенного макропроцессора. 4. Глава 6. Операционные системы В этой главе будут рассмотрены функции операционных систем и способы их разработки Не пытаясь в единственной главе дать исчерпывающее изложение темы, явившейся предметом многих книг, мы коснемся только наиболее важных идей и решении, иллюстрируя их примерами и снабжая ссылками на соответствующую литер. Способы разработки операционных систем и их назначение достаточно разнообразны. Одни, очень простые, предназначены для обеспечения работы единственного пользователя на персональной ЭВМ Другие, крайне сложные системы, дают возможность одновременной работы многих пользователей, управляют высокоразвитыми аппаратными и программными средствами. В разд 6 1 рассмотрены основные свойства операционных систем, которые присущи практически любому их элементу. Изза большого разнообразия операционных систем перечень этих свойств необычайно сжат Он состоит лишь из нескольких общих функций, которые могут быть взяты едва ли не как определения термина операционная система. Раздел 62 содержит описание некоторых важных свойств машинно-зависимых операционных систем, а разд. 6 3 освещает характеристики машинно-независимых реализации Многие из рассматриваемых в этих разделах функций (к ним относятся, например, распределение системных ресурсов и управление связью между различными пользователями) должны быть реализованы практически в любой операционной системе, поддерживающей одновременную работу многих пользователей. В разд 6 4 коротко излагаются некоторые альтернативные способы построения операционных систем В разд 6 5 описываются несколько реальных операционных систем, дающих представление лишь об отдельных формах и функциях подобного программного обеспечения. 6.1. Основные функции операционных систем. Ниже вкратце рассматриваются основные функции, обычно выполняемые всеми операционными системами. Главная задача операционной системы — упростить общение пользователей с ЭВМ. Системное программное обеспечение является надстройкой над базовыми аппаратными средствами и делает работу пользователя с машиной более удобной. Например, обеспечивая максимальную производительность ЭВМ, операционная система осуществляет достаточно сложный процесс управления ее ресурсами, все нюансы которого скрыты от пользователя. Пользователь RUN P Р Реальная машина Расширенная машина (операционное окружение) Интерфейс пользователя Рис 6.1. Основная концепция операционной системы. ЭВМ. Системное программное обеспечение является надстройкой над базовыми аппаратными средствами и делает работу пользователя с машиной более удобной Например, обеспечивая максимальную производительность ЭВМ, операционная система осуществляет достаточно сложный процесс управления ее ресурсами, все нюансы которого скрыты от пользователя. По сравнению с предыдущими главами приводимое ниже изложение основных свойств намного короче и носит более общий характер. Например, для ассемблеров, которые рассмотрены в гл.2, нам удалось найти общую структуру, не зависящую от машинной реализации. Однако операционные системы для персонального компьютера и для суперЭВМмс большим числом пользователей, за исключением основных подходов, будут сильно различаться. Основные функции операционных систем могут быть изображены, как показано на рис. 6.1. Взаимодействие с программистами, операторами и т.д. осуществляется через интерфейс пользователя, который поддерживается операционной системой. Именно его мы имеем в виду при ответе на вопрос: «Какого характера операционная система?». Если интерфейс предусматривает наличие некоторого языка управления, то, например, запуск программы на счет может быть осуществлен по команде КиМ Р. В разд. 6.1.1 приводится терминология, связанная с операционными системами, и дается их классификация, основанная на предоставляемом ими интерфейсе пользователя. В разд. 6.1.2 коротко описываются некоторые возможные функции интерфейса пользователя. Для выполнения часто встречающихся задач операционные системы предоставляют программам определенный набор услуг. Например, для чтения из файла некоторого набора данных программа Р может использовать стандартную сервисную программу. Последняя вызывается командой типа read(f), с помощью которой задается и имя файла. Всю заботу об осуществлении ввода-вывода, производимого на машинном уровне, возьмет на себя операционная система. Сервисные программы могут рассматриваться как часть операционного окружения задач, находящихся в решении. О нем пойдет речь в разд. 6.1.3. Более подробно некоторые сервисные функции и стандартные программы описаны в разд. 6.2 и 6.3. В данной главе предполагается, что функции операционной системы реализованы только программным обеспечением. Однако многие из них могут быть представлены программно-аппаратными средствами (firmware), состоящими из набора микропрограмм. Дополнительную информацию об этом можно получить в Дейтел [1984]. 6.1.1. Типы операционных систем Очень часто способы классификации операционных систем основываются на типе предоставляемого ими интерфейса пользователя. Многие понятия, связанные с операционными системами, возникают из представления пользователей о системе. В данном разделе вводится терминология, наиболее часто используемая для описания операционных систем. При этом не всегда удается достичь полной ясности в определении типов некоторых систем: они подпадают более чем под одну категорию и их классификации в некоторых пунктах совпадают. Один из способов классификации связан с количеством пользователей, одновременно обслуживаемых системой. Назовем однопрограммной систему, которая обеспечивает работу одного пользователя. Это самый старый тип операционных систем. Сейчас его можно встретить на микрокомпьютерах и персональных ЭВМ. Вероятно, и на гипотетической машине УУМ и:(-за малого объема памяти и нехватки каналов (что сильно затрудняет обслуживание более одного пользователя) следует считать, что используется однопрограммная система. Мультипрограммная система позволяет одновременно выполнять несколько заданий пользователей, управляя при этом распределением процессора между ними. Для того чтобы задания не мешали друг другу, операционная система создает для них соответствующее операционное окружение. Мультипроцессорная система схожа с мультипрограммной с той разницей, что в первой возможно использование более чем одного ЦП. Основная цель мультипрограммирования — увеличение производительности вычислительной системы за счет разделения ее ресурсов между несколькими заданиями. Например, одно задание может занимать процессор, в то время как другое — ожидать завершения операции ввода-вывода (подробнее об этом см. в разд. 6.2). Другой способ классификации операционных систем основан на типе доступа, предоставляемого интерфейсом пользователя. В случае систем с пакетной обработкой в качестве задания выступает последовательность управляющих операторов, записанная на машинных носителях (например, на перфокартах или диске). За исключением смены дисков или лент, осуществляемой операторами, всю заботу о считывании и выполнении заданий берут на себя операционные системы. Порядок, в котором выполняются задания, может быть выбран несколькими способами. Вопросы планирования рассматриваются в разд. 6.3. Диалоговый или интерактивный доступ некоторого числа пользователей обеспечивается системами разделения времени. Операционная система исполняет директивы пользователей по мере того, как они вводятся, стараясь дать ответ на каждую команду пользователя за разумно короткое время. Для обработки внешних сигналов, поступающих, например, с различных датчиков, и быстрого ответа на них используются системы реального времени. К ним относятся операционные системы, работающие на электронно-вычислительных машинах, управляющих процессами, в которых время является критическим параметром (например, ядерная реакция или полет космического корабля). Вообще говоря, мультипрограммные системы пакетной обработки призваны сделать использование ЭВМ более эффективным. Основной задачей систем разделения времени должно считаться обеспечение хорошего времени ответа пользователям, работающим в диалоговом режиме. Возможно, при этом придется примириться с использованием машины с меньшей эффективностью. Системы реального времени должны обеспечивать гарантированное время ответа на внешние события, для которых время является критическим параметром. Довольно часто все эти функции реализуются в одной системе. Например, многие системы пакетной обработки нередко поддерживают интерактивный режим, а другие вдобавок осуществляют я обслуживание процессов реального времени. Дополнительные сведения обо всех этих операционных системах, равно как и подробности об их создании, можно найти в Дейтел [1984]. 6.1.2. Интерфейс пользователя Интерфейс пользователя, предоставляемый операционной системой, предназначен для обеспечения нужд различных групп люден, имеющих дело с ЭВМ. Например, для работы на персональной ЭВМ с простой операционной системой пользователю предоставляется набор команд, посредством которых он может получать доступ к системным программам (трансляторам, редакторам, загрузчикам), осуществлять управление внешними файлами. Подобный командный язык достаточно прост в использовании; обычно имеется возможность в диалоге с машиной получить подсказку или просмотреть меню, содержащее варианты команд. В более сложных системах может существовать несколько различных языков общения с операционной системой. Непрофессиональным программистам предоставляется возможность работать с ЭВМ на простом языке директив (command language). Профессиональными программистами может применяться мощный и сложный язык, часто называемый языком управления заданиями (job control language). Кроме того, обычно существует специальный язык, при помощи которого осуществляется взаимодействие операторов и ЭВМ. Такой интерфейс оператора позволяет запускать и останавливать задания, выяснять их состояние и состояние системных ресурсов, управлять внешними действиями. Например, оператору может быть сообщено о необходимости установить ленту или диск. Разработка интерфейса пользователя обычно не влечет за собой решения сложных технических задач. Между тем его создание чрезвычайно важно, так как он эксплуатируется большинством пользователей. В идеале интерфейс пользователя должен быть разработан таким образом, чтобы отвечать требованиям всевозможных типов пользователей и в то же время учитывать цели и задачи вычислительной системы. Дополнительные сведения об интерфейсе пользователя можно найти в работах Дейтел [1984] и Питерсон [1983]. Для поддержки интерфейса пользователя операционная система должна иметь также стандартные сервисные программы. В случае персональной ЭВМ это могут быть программы для управления вводом с клавиатуры и выводом на дисплей, а в более сложных системах — средства сопряжения с работающими в режиме разделения времени удаленными терминалами, считывателями с перфокарт и печатающими устройствами. Возможно также наличие интерфейса между локальной системой и другими ЭВМ, объединенными в сеть. Как часть интерфейса оператора многие операционные системы ведут накопление статистики активности системы; она может использоваться для анализа производительности и обнаружения ошибок. 6.1.3. Операционное окружение Одной из наиболее важных функций операционной системы является поддержка операционного окружения пользовательских задач. Оно состоит из ряда стандартных сервисных программ, которые могут быть использованы в процессе выполнения задачи и предоставлять средства для управления ресурсами вычислительной системы, выделяя их пользователю по мере надобности. В качестве примера услуг, предоставляемых операционным окружением, рассмотрим функцию ввода-вывода. Почти все операционные системы имеют стандартные программы, помогающие в осуществлении таких операций. Предположим, что программа работает на УУМ под управлением однопрограммной системы. Чтобы прочитать байт без помощи операционной системы, программа должна содержать цикл, в котором анализируется состояние устройства и выполняется команда RD (см. рис. 2.1). Обнаружение и исправление ошибок также возложено на саму программу. Наличие поддержки со стороны операционной системы сильно облегчает задачу. Программа пользователя может просто инициализировать стандартную сервисную программу, задав устройство, которое должно быть использовано. За всеми нюансами, такими как опрос состояния и подсчет переданных бай- тов, проследит операционная система. Она же примет необходимые меры по исправлению ошибок. Стандартная сервисная программа, подобная описанной выше, может восприниматься как расширение базовой машины. Типичная операционная система содержит много подобных программ. Вместе взятые, они составляют расширенную машину, которая и используется во время выполнения программы. Программам не нужно опускаться до уровня базовых аппаратных средств, поскольку все функции и возможности предоставляет расширенная машина. Она проще в использовании, чем реальная. Это касается, например, нюансов выполнения операций ввода-вывода. Есть и другие преимущества. Например, операции ввода-вывода на расширенной машине менее подвержены ошибкам, чем на реальной, так как за обнаружением и исправлением ошибок следит операционная система. Иногда расширенную машину называют виртуальной. Однако термин «виртуальная машина» может иметь и другой смысл. Это двоякое использование термина описывается в разд. 6 4. Операционное окружение мультипрограммных операционных систем содержит также программы, которые управляют ресурсами ЭВМ, выделяя их по необходимости заданиям пользователей. Например, оперативная память распределяется между заданиями, одновременно находящимися в решении, центральный процессор предоставляется заданиям согласно заранее выбранной стратегии. За исключением конкретных запросов операционной системе заданиям пользователя нет необходимости иметь дело с управлением ресурсами. Благодаря операционному окружению каждое задание выполняется как бы на отдельной расширенной машине, хотя в действительности базовая машина может быть распределена между несколькими пользователями. В некоторых системах программы пользователей могут вызывать функции операционной системы, обращаясь непосредственно к фиксированным областям памяти. В документации по операционной системе для пользователя дается описание областей, предназначенных для данных, и входных точек вместе с их реальными адресами. Например, точка входа стандартной сервисной программы ввода-вывода может находиться в памяти по адресу 238. После установки в регистрах требуемых параметров программа пользователя может инициализировать эту сервисную функцию командой JSUB 238. Иногда возможно наличие в памяти одной точки входа для всех сервисных программ, нужный тип обслуживания может быть определен при помощи кода запроса. Способ запроса связи с операционной системой при помощи обращения к фиксированной области памяти используется в микрокомпьютерах и персональных ЭВМ Однако этот метод часто не удобен и является источником ошибок; кроме того, он может предоставить пользователю возможность обойти средства защиты, встроенные в операционную систему В более развитых системах пользователи запрашивают функции опера- ционных систем в основном при помощи специальных машинных команд, таких как вызов супервизора (SVC – Super Visor Call). Выполнение команды SVC вызывает прерывание, в результате которого управление передается сервисной стандартной программе операционной системы. Код, которым сопровождается команда SVC, определяет тип запроса. Обработка прерываний операционной системой рассматривается в разд 6.2.1. Как правило, в машине любое прерывание вызывает перевод ЦП из режима пользователя в режим супервизора В режиме супервизора могут быть использованы все команды и средства машины Многие части операционной системы работают в этом режиме При этом в режиме пользователя недопустимо выполнение некоторых команд. К таким командам могут относиться, например, функции ввода-вывода, установка флагов защиты или переключение ЦП из одного режима в другой. Примеры таких команд будут рассмотрены ниже. Ограничения, накладываемые на использование привилегированных команд, заставляют программу пользователя обращаться к услугам операционного окружения. Таким образом, вместо непосредственного использования функций базового аппаратного обеспечения программы должны иметь дело с интерфейсом расширенной машины. Ограничения также не дают программам пользователей вмешиваться случайно или намеренно в функции управления ресурсами, осуществляемые операционной системой. Привилегированные команды, равно как ч,и режим пользователя/супервизора (или эквивалентный ему), необходимы практически для всех систем, поддерживающих одновременную работу более чем одного пользователя. В разд. 62 и 63 рассматриваются разнообразные функции и услуги, обычно предоставляемые операционным окружением. На этом уровне между операционными системами, которые могут оказаться совершенно различными в интерфейсе пользователя, есть много общего Многие рассматриваемые технические приемы могут быть использованы с некоторыми изменениями почти во всех операционных системах: пакетной обработки, разделения времени, реального времени и т. п. 6.2. Машинно-зависимые свойства операционных систем Одной из наиболее важных функций операционной системы является управление ресурсами ЭВМ, на которой она работает. Многие ресурсы имеют непосредственное отношение к аппаратным устройствам, таким как центральная оперативная память, каналы ввода-вывода и ЦП. Таким образом, многие функции операционной системы тесно связаны с архитектурой машины. Рассмотрим, например, машину УУМ У нее маленькая оперативная память, отсутствуют каналы ввода-вывода, прерывания, нет команд вызова супервизора. Такая машина может быть удобна в качестве персональной ЭВМ; на ней нет смысла работать одновременно нескольким пользователям. Таким образом, операционная система для стандартной машины УУМ будет однопользовательской с простыми средствами общения с пользователем и минимальным набором функций операционного окружения. И если она предоставит какие-то простые возможности, то их вряд ли будет больше тех, что рассматривались в разд. 6.1. ЭВМ УУМ/ДС, наоборот, имеет гораздо большую оперативную память, каналы ввода-вывода и обладает многими другими свойствами, отсутствующими у стандартной машины УУМ. На УУМ/ДС хорошо иметь мультипрограммную операционную систему. Она позволит распределять между несколькими одновременно работающими пользователями доступные им ресурсы расширенной машины, а также лучше использовать усовершенствованные программные средства. Конечно, разделение вычислительной системы между несколькими пользователями создает много проблем, подобных распределению ресурсов. Все они должны быть решены операционной системой. В дополнение к этому операционная система должна осуществлять поддержку более развитых функций аппаратуры, таких как прерывания и канальный ввод-вывод. В данном разделе мы рассмотрим некоторые функции машинно-зависимых частей операционных систем. Для этого будет использована терминология ЭВМ УУМ/ДС; однако некоторые принципы могут быть легко перенесены на другие машины, у которых архитектурные особенности схожи с УУМ/ДС. Мы обсудим в ходе изложения ряд важных свойств аппаратных средств УУМ/ДС. Для упрощения ссылок все эти свойства резюмированы в приложении В. Раздел 6.2.1 знакомит читателя с основными принципами прерываний и их обработки, используемыми на всем протяжении остальной части главы. В разд. 6.2.2 обсуждаются вопросы, связанные с распределением ЦП между несколькими заданиями пользователей, работающими в мультипрограммном режиме. В разд. 6.2.4 и 6.2.5 обсуждаются вопросы разделения центральной памяти между несколькими пользовательскими заданиями. В разд. 6.2.4 дается представление о средствах управления реальной памятью, а в разд. 6.2.5 — о важном понятии — виртуальной памяти. 6.2.1. Обработка прерываний Прерывание (interrupt) — это сигнал, заставляющий ЭВМ менять обычный порядок исполнения потока команд. Возникновение подобных сигналов обусловлено такими событиями, как завершение операций ввода-вывода, истечение заранее заданного интервала времени или попытка деления на нуль. Рис. 6.2 дает представление о последовательности событий, происходящих в ответ на прерывание. Предположим, что в момент поступления от некоторого источника сигнала прерывания программа А находится в решении. В результате управление автоматически передается на блок обработки прерываний (или блок ОП, а также обработчик прерываний), который обычно является частью операционной системы. Этот блок предназначен для выполнения некоторых действий в ответ на условие, вызвавшее прерывание. После завершения обработки управление может быть снова передано в ту точку программы А, где ее выполнение было прервано. В только что описанной последовательности событий возникновение и обработка прерывания могут быть совершенно не Программа А Стандартная программа Обработики прерываний Прерывание Возврат из прерывания Рис. 6 2 Основная концепция обработки прерываний. связаны с программой А. Например, оно может быть вызвано завершением операции ввода-вывода, выданной другой программой. В общем случае невозможно предсказать, когда и по какой причине программа А будет прерватип на. Другими словами, по отношению к ней Класс прерывания прерывания возникают асинхронно. За соI SVC хранением текущего состояния машины во II Программное время прерывания программы А, а также III по таймеру за его восстановлением, когда А будет проIV ввода/вывода должена, следят аппаратные и програмРис.6.3. Типы прерымные средства. Благодаря этому в случае ваний в УУМ/ДС. прерывания ничто, за исключением времени, не влияет на ее выполнение. На самом деле для А даже не существует способа узнать, имело место прерывание или нет. На рис. 63 приведены четыре класса прерываний для ЭВМ УУМ/ДС. SVC-прерывание (класс I) возникает при выполнении ЦП команды вызова супервизора. Эта команда используется программами для вызова функций операционной системы. Программное прерывание (класс II) возникает при появлении некоторой ситуации, такой как деление на нуль, или при попытке выполнить неправильную машинную команду и, возможно, в процессе работы программы. Приложение В содержит полный перечень условий, которые могут вызвать программное прерывание. Прерывание по таймеру (класс III) вызывается интервальным таймером ЦП Этот таймер содержит регистр, которому может быть присвоено определенное начальное значение посредством привилегированной команды STI. Значение этого регистра автоматически уменьшается на 1 после использова- ния каждой миллисекунды времени ЦП. Когда это значение становится равным нулю, происходит прерывание по таймеру. Подобный интервальный таймер используется операционной системой для определения времени, в течение которого программа пользователя может оставаться под управлением машины. Прерывание по вводу-выводу (класс IV) вызывается каналами или устройствами ввода-вывода Причиной многих таких прерываний является нормальное завершение некоторой операции ввода-вывода; однако они могут также оповещать о возникновении различных ошибочных ситуаций. Когда происходит прерывание, состояние ЦП сохраняется, а управление передается стандартной программе обработки прерываний В заключении рассмотрим метод для УУМ/ДС. Как показано на рис 6 4, в машине УУМ/ДС для каждого класса прерываний имеется соответствующая ему рабочая область прерываний. Например, область, соответствующая прерыванию по таймеру, начинается с адреса памяти 160 Когда происходит прерывание по таймеру, содержимое всех регистров сохраняется в этой области (см. рис 6 4а) Затем из двух первых слов области заранее занесенные туда значения загружаются в слово состояния SW и счетчик команд РС. Загрузка и сохранение регистров осуществляются аппаратными средствами машины автоматически. Загрузка счетчика команд новым значением адреса автоматически вызывает передачу управления на соответствующую команду. Этот адрес, заранее сохраненный в рабочей области прерывания, представляет собой начальный адрес стандартной программы обработки прерываний по таймеру. Загрузка слова SW также вызывает определенные изменения в состоянии ЦП. Центральная память . . . . Рабочая область SVC-прерываний 100 103 106 109 10С Новый SW Новый PC Старый SW Старый РС Область сохранения регистров Регистры SW 130 Новый SW PC A X Рабочая область программных прерываний Рабочая область прерывния по таймеру Рабочая область прерывания по вводувыводу 133 136 139 13С Новый PC Старый SW Старый РС Область сохранения регистров 160 163 166 169 16С Новый SW Новый PC Старый SW Старый РС S T Область сохранения регистров 190 193 196 199 19С B F Новый SW Новый PC Старый SW Старый РС Область сохранения регистров . . . . а Рис. 6.4. Операции контекстного переключения, вызванные а – прерыванием по таймеру и б – командой LPS 166. Центральная память . . . . Рабочая область 100 103 106 109 Новый SW Новый PC Старый SW Старый РС Регистры SVC-прерываний 10С Область сохранения регистров SW Рабочая область программных прерываний Рабочая область прерывния по таймеру Рабочая область прерывания по вводувыводу 130 133 136 139 13С Новый SW Новый PC Старый SW Старый РС Область сохранения регистров 160 163 166 169 16С Новый SW Новый PC Старый SW Старый РС A X L B S T Область сохранения регистров 190 193 196 199 19С PC F Новый SW Новый PC Старый SW Старый РС Область сохранения регистров . . . . б Рис.6.4. Продолжение. После выполнения в ответ на запрос на прерывание любого требуемого действия стандартная программа обработки прерываний выполняет команду загрузки состояния процессора (LPS – Load Processor Status), в результате чего управление передается прерванной программе (см рис 646). Команда LPS вызывает загрузку сохраненного содержимого SW, РС и других регистров из соответствующих слов области сохранения, начиная с адреса, указанного в команде. Это приводит к восстановлению содержимого регистров и состояния ЦП, которые были в момент прерывания. Управление затем передается на команду, перед выполнением которой произошло прерывание. Сохранение и восстановление состояния ЦП и содержимого регистров часто называют операцией контекстного переключения. Слово состояния SW содержит часть информации, которая нужна для обработки прерываний. Мы говорим о содержимом SW в УУМ/ДС. Большинство ЭВМ имеет аналогичный регистр, часто называемый словом состояния программы или словом состояния процессора. Разряд ы 0 1 2-5 6-7 8-11 12-15 16-23 Имя поля MOD E IDLE ID CC MAS K Использование O=режим пользователя,1=режим супервизора 0=активен, 1=пассивен Идентификатор процесса Код условия Маска перрываний Не используется Код прерываний ICOD E Рис 6.5. Содержимое слова состояния УУМ/ДС. На рис. 6 5 показано содержимое SW. Первым битом (МОDЕ) задается режим, в котором находится ЦП — пользовательский или супервизора. Обычные программы выполняются в пользовательском режиме (МОDЕ = 0). Когда происходит прерывание, новое загружаемое содержимое SW имеет МODЕ = 1, что автоматически переводит ЦП в режим супервизора, тем самым становится возможным использование привилегированных команд. Перед тем как значение SW будет сохранено, в поле ICODE автоматически устанавливается значение, указывающее на причину прерывания В случае SVC-прерываний ICODE присваивается значение, заданное пользователем командой SVС. Он определяет тип сервисного запроса. При программном прерывании в 1СООЕ отражен тип вызвавшего его условия, например деление на нуль. При прерывании по вводу-выводу в ICODE дан номер канала, породивший прерывание. Дальнейшую информацию о возможных значениях ICODE можно найти в приложении В. В SVC содержится также код состояния СС. Сохранение SW автоматически спасает значение кода состояния прерванного процесса. Об использовании полей IDLE и ID будет рассказано ниже в этой главе. Поле IDLE определяет, выполняет ли ЦП команды или простаивает. В ID содержится 4-битовое значение, идентифицирующее текущую выполняемую программу. Оставшееся поле слова состояния (МАSК) используется для контроля за разрешением прерываний. Это требуется для того, чтобы избежать потери сохраненной ранее информации о состоянии процесса. Предположим, например, что произошло прерывание по вводу-выводу. Значения SW, РС и других регистров будут сохранены в рабочей области прерывания вводавывода, о которой только что было рассказано, а ЦП начнет выполнение обработчика прерываний по вводу-выводу. Если до конца обработки первого прерывания произойдет еще одно, снова будет иметь место контекстное переключение. Однако на этот раз в качестве содержимого регистров, сохраняемого в рабочей области, окажутся значения, используемые обработчиком прерываний. Значения же, сохраненные при первом прерывании, будут утрачены, поэтому вернуть управление программе пользователя, которая в тот момент выполнялась, будет нельзя. Чтобы избежать этого, нужно не допустить наступления прерываний определенного типа, пока первое из них не будет обработано. Это достигается использованием поля МАSК слова состояния. В МАSК каждый бит соответствует некоторому классу прерываний. Если какой-то бит установлен в 1, то прерывания соответствующего класса разрешены, если в 0, то запрещены. В последнем случае говорят, что они маскированы (часто их также называют запрещенными или закрытыми). Однако маскированные прерывания не теряются, потому что сигнал, вызвавший прерывание, сохраняется аппаратурой. Временно задержанное таким способом прерывание называется отложенным. Когда, вследствие того что МА5К сброшена, прерывания соответствующего класса вновь разрешаются, сигнал опознается и происходит прерывание. В УУМ/ДС маскирование прерываний находится под контролем операционной системы и зависит от значения МА5К в SW, которое заранее сохраняется в рабочей области каждого прерывания. Можно запретить все прерывания, установив все биты МА5К в 0. Однако в действительности поступать подобным образом нет необходимости. Каждому классу прерываний на УУМ/ДС присвоен определенный приоритет прерываний. Наивысший приоритет имеют SVC-прерывания (Класс I), затем идут программные (Класс II) и т. д.. Поле МАSК в слове состояния устанавливается в соответствии с классом прерывания так, чтобы все прерывания с равным или более низким приоритетом были запрещены, а с более высоким разрешены. Например, слово состояния, загруженное в результате программного прерывания, будет иметь биты МАSК, соответствующие прерываниям — программным, по таймеру и по вводу-выводу, установленными в 0; эти классы прерываний будут запрещены. Бит в МАSК для SVC-прерываний будет установлен в 1, поэтому они будут разрешены. Когда прерывания по завершении работы обработчика прерываний открываются, среди ожидающих может оказаться более одного класса прерываний (например, по таймеру и по вводу-выводу). В этом случае первым опознается то, что имеет более высокий приоритет. Если используется такая система приоритетов, то программа обработки прерываний сама может быть прервана. Каким образом происходит вложенное прерывание, показано на рис. 6 6. По прерыванию по вводу-выводу состояние программы А, выполняющейся в этот момент на ЦП, сохраняется, а управление передается обработчику прерываний по вводу-выводу. Во время его работы происходит новое прерывание — уже по таймеру, в результате чего управление передается обработчику прерываний по таймеру. По завершении обработки этого прерывания при помощи команды LPS из рабочей области прерывания по таймеру восстанавливается состояние ЦП. В результате управление снова передается обработчику прерываний по вводу-выводу. Так как опять загружено старое значение МАSК, прерывания по таймеру, которые были запрещены, снова открываются. Однако прерывания по вводу-выводу по-преж- Блок ОП по вводу-выводу Программа А Прерывание по вводувыводу Блок ОП по таймеру Прерывание по таймеру LPS 166 LPS 196 Рис. 6.6. Пример вложенного прерывания. нему остаются закрытыми. После завершения обработки прерывания при помощи уже другой LPS восстанавливается состояние ЦП, которое было в момент первого прерывания. Теперь все прерывания открыты, потому что в слове состояния, используемом программой А, все биты МА5К установлены в 1. Ниже мы увидим, как прерывания могут быть использованы при реализации таких функций операционной системы, как планирование процессов, управление вводом-выводом и распределение памяти. 6.2.2. Планирование процессов Процессом (рrocess), или заданием (task), часто называется программа, находящаяся в решении. Другие возможные определения процесса можно найти в Дейтел [1984]. Для выполнения вычислительной работы операционная система выделяет процессам ЦП. В однопрограммной операционной системе присутствует только один пользовательский процесс. Однако в мультипрограммной системе на ресурсы может претендовать много независимых процессов. Планирование процессов — это управление распределением ресурсов ЦП между различными конкурирующими процессами путем передачи им управления согласно некоторой стратегии планирования. Во многих случаях процесс соответствует заданию пользователя. Однако некоторые операционные системы позволяют одному заданию создавать несколько различных процессов, выполняемых одновременно. Вдобавок некоторые системы разрешают одной программе быть разделенной между несколькими независимыми процессами. Дальнейшую информацию по этому вопросу можно найти в Дейтел [1984]. Здесь мы будем считать, что каждому процессу соответствуют только одна программа и одно задание пользователя. Процесс создается, когда выполнение задания пользователя начинается, и уничтожается, когда задание завершается. Во время своего существования процесс может находиться в трех состояниях. Процесс активен (running), когда он использует ЦП для выполнения своих команд. Процесс блокирован (blocred), если его выполнение может быть продолжено только после наступления некоторого ожидаемого им события. Например, процесс может быть блокирован, потому что ему требуется ждать завершения операции ввода-вывода. Процессы, которые не блокированы и не активны, называются находящимися в состоянии готовности (геаdу). Этим процессам будет передано управление после того, как текущий активный процесс его отдаст. На рис. 6 7 показаны возможные переходы из одного состояния в другое. В любой момент времени активным (т. е. использующим ЦП) может быть только один процесс. При передаче управления процессу пользователя операционная система устанавливает интервальный таймер. Тем самым задается квант времени, являющийся максимальным количеством времени ЦП, на которое процесс получает управление. Если это время истекает, процесс переводится из состояния активности в состояние готовности. После этого операционная система, согласно стратегии планирования, выбирает следующий процесс, находящийся в готовности, переводит его в состояние активности и передает ему управление. Выбор процесса и передачу на него управления часто называют диспетчеризацией. Часть операционной системы, выполняющая эту функцию, называется диспетчером (dispatcher). Может оказаться, что активный процесс, не использовав полностью предоставленного ему кванта времени, будет ожидать наступления некоторого события, например завершения операции ввода-вывода. В этом случае активный процесс блокирует- ся, а какой-то новый процесс активизируется. Когда же ожидаемое событие наступает, соответствующий заблокированный процесс переводится в состояние готовности и может снова стать Ожидает события Блокирован Истек квант времени Событие наступило Активен Готов Выбран для активизации Рис. 6.7. Изменение состояний процесса. кандидатом на обслуживание. Операции ожидания события и оповещения о том, что событие наступило, реализуются при помощи сервисных запросов операционной системе (с использованием SVC). Механизм, чаще всего применяемый для установления соответствия между процессами и ожидаемыми ими событиями, рассматривается ниже в данном разделе. Обычно до своего завершения процесс много раз пребывает в состоянии активности, готовности и блокировки. Для того чтобы это никак не повлияло на результаты вычислений, каждый раз, когда процесс теряет активность, его текущее состояние должно быть сохранено. Когда же процесс снова будет активизирован, это состояние должно быть восстановлено. Информация о состоянии каждого процесса хранится операционной системой в блоке состояния процесса (РSВ — Рrocess Status Block). Блок состояния процесса создается, когда процесс входит в решение, и уничтожается, когда процесс завершается. Блок РSВ содержит информацию о том, в каком состоянии процесс находился (активности, готовности или блокировки). В нем имеется область, используемая для сохранения машинных регистров (включая SW и РС) и другой всевозможной информации (например, о системных ресурсах, используемых процессом). Общий вид алгоритма, используемого при диспетчеризации, показан на рис. 68. При передаче управления от одного процесса другому прежде всего необходимо сохранить информацию о состоянии активного процесса. Если процесс был заблокирован, так как он использовал весь свой квант времени, то информация о состоянии может быть найдена в рабочей области прерывания по таймеру. Если процесс отдал управление {выполнив SVC-запрос), потому что ему требуется дождаться наступления некоторого события, информация о состоянии будет храниться в рабочей области SVC-прерываний. Конечно, procedure DISPATCH обновить PSB Активного процесса (если он есть) выбрать следующий Готовый процесс для передачи ему управление if готовый процесс найден then begin пометить выбранный процесс как Активный выделить квант времени, установив командой SТI интервальный таймер командой LPS передать управление вибранному процессу end else командой LPS перевести ЦП в состояние простоя Рис. 6 8. Алгоритм диспетчеризации. может оказаться, что активных процессов нет. Это может случиться, например, если все процессы в системе находятся в заблокированном состоянии. Тогда информацию о состоянии сохранять не надо. После сохранения состояния предыдущего активного процесса диспетчер выбирает для активизации новый процесс. Чтобы задать квант времени, выделяемый выбранному процессу, диспетчер устанавливает интервальный таймер, затем, используя команду LPS для загрузки информации о состоянии, хранимой в PSB данного процесса, осуществляет передачу управления. Если процесса, находящегося в состоянии готовности, нет, то диспетчер для перевода ЦП в состояние простоя (idle), использует LPS, загружая слово состояния с IDLE = 1 (рис. 6.5). Выбор следующего процесса для диспетчеризации осуществляется несколькими способами. В первом из них, называемом круговым (robin round), все процессы считаются равноценными. Диспетчер циклически просматривает все PSB, выбирая следующий процесс из тех, что находятся в состоянии готовности. Каждому активизируемому процессу предоставляется одинаковый квант времени. В более сложных методах диспетчеризации выбор процессов происходит по приоритетам. В некоторых системах приоритеты определены заранее в соответствии с характером заданий пользователей. Задачей таких систем является обеспечение для каждого класса заданий необходимого уровня сервиса. В других системах приоритеты могут назначаться самой операционной системой в целях увеличения производительности всей системы. Приоритеты могут меняться и динамически в зависимости от загрузки и производительности системы. Возможно, что наряду с использованием системы приоритетов различным процессам будут выделены различные кванты времени. Дальнейшее обсуждение этих и более сложных способов диспетчеризации может быть найдено в Дейтел [1984] и Лорин [1981]. procedure MAIT(ESB) ESBFLAG = 1 then (событие уже наступило) возвратить управление запрашивающему процессу командой IPS else begin пометить запрашивающий процесс как Блокированный занести запрашивающий процесс в ESВQUEUE DISPATCH end if а procedure SIGNAL (ESB) ESBFLAG : = 1 {отметить, что событие наступило} for каждый процесс в ESBQUEUE do begin пометить процесс как Готовый убрать процесс из ESBOUEUE end возвратить управление запрашивающему процессу командой LPS 6 Рис.6.9. Алгоритмы обработки WAIT (SVC 0) и SIGNAL (SVC 1) Когда активный процесс достигает точки, в которой ему требуется ждать наступления некоторого события, он сообщает об этом операционной системе при помощи запроса на обслуживание WAIT (SVC 0). О наступлении события, которого могут ожидать и другие процессы, операционной системе сообщается посредством запроса SIGNAL (SVC 1). В разд 6.2.3 будут даны примеры использования WAIT и SIGNAL; здесь же мы коснемся того, как эти запросы соотносятся с функцией планирования процессов. На рис. 6.9 приводится последовательность действий, выполняемых операционной системой в ответ на подобные сервисные запросы. Событие, которое ожидалось или о наступлении которого было сообщено, определяется посредством задания адреса соответствующего блока состояния события (ESB—Event Status Block). Блок состояния события содержит битовый флаг ESBFLAG, в котором отмечается, произошло ли соответствующее событие или нет. Блок ESB содержит также указатель на ESBQUEUE — список процессов, ожидающих в этот момент наступления данного события. Дальнейшая информация о ESB, Примерах их создания и использования приводится в разд. 6.2.3. Запрос WAIT выдается активным процессом и говорит о том, что процесс не может продолжить работу до наступления события, соответствующего ESB. Таким образом, в алгоритме обработки WAIT первым делом исследуется ESBFLAG и, если событие уже наступило, управление возвращается запрашивающему процессу. Если событие еще не наступило, активный процесс переводится в состояние блокировки и заносится в ESBQUEUE. Запрос SIGNAL выдается процессом, обнаружившим наступление некоторого события, соответствующего ESB. Следовательно, установкой ESBFLAG алгоритм для обработки SIGNAL отмечает, что событие наступило. Затем осуществляется сканирование ESBQUEUE, списка ожидающих этого события процессов. Каждый процесс в списке переводится из состояния блокировки в состояние готовности. Затем управление возвращается процессу, сделавшему запрос SIGNAL. Если используемый метод диспетчеризации основан на приоритетах, для обработки SIGNAL применяется несколько другой алгоритм. В таких системах может случиться, что один или более процессов, перешедших в состояние готовности, имеют более высокий приоритет, чем текущий активный процесс. Учитывая это, алгоритм для обработки SIGNAL вместо прямой передачи управления активному процессу должен сначала вызвать диспетчер, который и передаст управление процессу с наивысшим приоритетом, находящемуся в текущий момент в состоянии готовности. Этот способ называется планированием процессов по приоритетам. Он позволяет процессу, перешедшему в состояние готовности, перехватить управление у процесса, активного в ?тот момент, но имеющего меньший приоритет, не ожидая истечения кванта времени этого процесса. 6.2.3. Обслуживание ввода-вывода На типичной мини-ЭВМ, такой как УУМ, ввод-вывод осуществляется побайтно. Например, для чтения данных программа должна иметь цикл, в котором опрашивается состояние устройства ввода-вывода и выполняется ряд команд чтения данных. В подобных системах ЦП участвует в передаче и приеме каждого байта с устройства ввода-вывода. Пример программирования ввода-вывода такого рода показан на рис. 2.1. В более совершенных ЭВМ, таких как УУМ/ДС, для отслеживания всех деталей передачи данных и управления вводом-выводом используются каналы ввода-вывода. На рис. 6.10 приведена типичная для УУМ/ДС конфигурация ввода-вывода. Пусть, например существует 16 каналов, к каждому из которых может быть подключено до 16 устройств. В номер, присвоенный устройству ввода-вывода, входит и номер канала, к которому 00 оно подключено. Например, устройство, обозначенное 20—2F, подключено к Каналу 2. Канал 0 01 02 ЦП Центральная память 10 Канал 1 11 20 Канал 2 21 22 Рис. 6.10. Типичная конфигурация ввода-вывода для УУМ/ДС. Последовательность операций, которые должен выполнить канал, задается канальной программой, состоящей из набора канальных команд. Для осуществления операции ввода-вывода ЦП выполняет команду ввода-вывода START I/O (SIO), в которой задается номер канала и начальный адрес канальной программы. Затем канал выполняет указанную операцию ввода-вывода без дальнейшего вмешательства ЦП. По завершении своей программы канал генерирует прерывание по вводу-выводу. Одновременно могут работать несколько каналов, каждый из которых выполняет свою собственную канальную программу; таким образом, в одно и то же время могут осуществляться несколько различных операций ввода-вывода. Каждый канал работает независимо от ЦП, поэтому, пока выполняются операции ввода, ЦП может продолжать вычисления. Операционная система ЭВМ типа УУМ/ДС вовлекается в процесс ввода-вывода в нескольких случаях. Система должна принять запросы на ввод-вывод от пользовательских программ и сообщить им, когда операции будут выполнены. Она может также управлять работой каналов ввода-вывода и обрабатывать генерируемые ими прерывания. В оставшейся части раздела мы обсудим, как эти функции выполняются, и проиллюстрируем этот процесс несколькими примерами. Программа на УУМ/ДС запрашивает операцию ввода-вывода посредством выполнения команды SVC 2, параметрами которой являются номер канала, начальный адрес канальной программы и адрес блока состояния события, используемый при оповещении о завершении операции ввода-вывода. Если программа должна ждать результаты операции ввода-вывода, то она выполняет команду SVC О (WAIT). Этой командой задается адрес блока состояния события, соответствующего ожидаемой операции ввода-вывода. Таким образом, выполнение операции ввода-вывода имеет вид SVC 2 {запрос операции ввода-вывода} . . . SVC 0 {ожидание результата} В некоторых случаях WAIT может сом операции ввода-вывода. Однако, ввод-вывод могут идти одновременно, следовать сразу за запротак как вычисления и имеется возможность про- должить выполнение программы, пока ожидаются результаты операции ввода-вывода. Более подробно это иллюстрируется программой на рис.6 11. Сначала программа загружает в регистры начальный адрес канальной программы, номер канала и адрес блока состояния события. После этого для запроса операции ввода-вывода программа выполняет команду SVC. Канальная программа для чтения, задаваемая в виде последовательности элементов данных, содержит две канальные команды. Первой командой задается операция чтения, которая должна быть выполнена на подсоединенном к каналу устройстве с номером 1; 256 байт данных должны быть размещены в памяти, начиная с адреса BUFIN. Вторая команда вызывает останов канала и генерирует прерывание по вводу-выводу. Блок состояния события состоит из 3-байтовой области данных. Первым битом ESB является флаг, используемый для индикации, наступило уже соответствующее событие или нет (0 = нет, 1 == да). Остальная часть ESB служит для сохранения указателя на очередь процессов, ожидающих это событие. Если ждущих процессов нет, то значение указателя равно нулю. Так, начальное значение ESB Х'000000' показывает, что соответствующее событие еще не наступило и что ожидающие его процессы отсутствуют. Дальнейшие подробности, касающиеся формата канальных команд PI START 0 . . (инициализация) . LDA #READ АДРЕС КАНАЛЬНОЙ ПРОГРАММЫ LBS #l НОМЕР КАНАЛА LDT #ESB АДРЕС БЛОКА СОСТОЯНИЯ СОБЫТИЯ SVC 2 ВЫДАЧА ЗАПРОСА НА ЧТЕНИЕ LOOP LDA #ESB AДРЕС ESB SVC 0 ОЖИДАНИЕ КОНЦА ЧТЕНИЯ • • {запись данныx в рабочую область ирограммы} . LDА #0 ИНИЦИАЛИЗАЦИЯ ESB STA ESВ LDA #READ LDS #1 LDT #ЕSВ SVC 2 ВЫДАЧА СЛЕДУЮЩЕГО ЗАПРОСА НА ЧТЕНИЕ . • {данные процесса} . J LOOP . . KАНАЛЬНАЯ ПРОГРАММА ДЛЯ ЧТЕНИЯ . ПЕРВАЯ КОМАНДА — READ BYTE X'll' КОМАНДА = READ, УСТРОЙСТВО = I BYTE WORD . BУТЕ . ESB BYTE BUFIN RESB . . . END X'0100’ BUFIN СЧЕТЧИК БАЙТОВ = 256 АДРЕС БУФЕРА ВВОДА ВТОРАЯ КОМАНДА X’000000000000’ ОСТАНОВ КАНАЛА X’000000’ БЛОК состояния СОБЫТИЯ для ЧТЕНИЯ 256 БУФЕРНАЯ ОБЛАСТЬ ДЛЯ ЧТЕНИЯ Рис 611. Пример выполнения ввода-вывода с использованием запросов. УУМ/ДС и блока состояния события, можно найти в приложении В. После выдачи запроса на ввод-вывод программа на рис. 6 11 выполняет команду SVC 0. Регистр А содержит адрес ESB, соответствующего ожидаемому событию. Таким событием в данном случае является только что затребованная операция ввода-вывода. После завершения операции чтения программа заносит введенные данные в рабочую область Затем программа опять инициализирует ESB и запрашивает операцию ввода-вывода для чтения следующих 256 байт данных. Пока эта операция выполняется, программа может обрабатывать уже прочитанные данные, совмещая тем самым функции вычисления и ввода. По завершении обработки управление снова передается на начало основного цикла для ожидания конца следующей операции чтения. Несколько более сложный пример приведен на рис. 6.12. В этом случае программа копирует 4096 байт записей данных с устройства 22 на устройство 14. Имеются две канальные программы: одна для чтения и другая для записи, а также два блока состояния события. В основном цикле программы сначала выдается запрос на чтение, а затем ожидается завершение этого чтения и предыдущей записи. По окончании обеих операций программа создает последовательность вывода и выдает запрос на запись. Однако в этот момент ESB для операции записи имеет начальное значение Х'800000', где первый бит равен 1. Этим отмечается, что соответствующее событие уже наступило и при вызове стандартной программы WAIT операционной системы управление будет сразу возвращено программе пользователя (рис. 6.9). Операции ввода-вывода в программе на рис. 6.12 выполняются независимо друг от друга, так как ими используются разные каналы. Одна операция может быть завершена раньше другой. Возможно также, что обе операции будут выполнены фактически одновременно. Программа в состоянии координировать взаимосвязанные операции ввода-вывода, так как им соответствуют разные ESB. На примере этой программы показано, как каналы могут использоваться для выполнения совме- щенных операций ввода-вывода. Ниже такое совмещение будет рассмотрено подробнее. В программах на рис. 6.11 и 6.12 показано, как осуществляются запросы на ввод-вывод с точки зрения пользователя. Сейчас мы готовы обсудить, как они в действительности обрабатываются машиной и операционной системой. В аппаратуре УУМ/ДС предусмотрено наличие в памяти, соответствующей каналу ввода-вывода, рабочей области канала. В ней содержатся стартовый адрес текущей канальной программы, если она имеется, и адрес ESB, соответствующего текущей операции. Результат работы операции ввода-вывода после ее завершения отображается находящимися в рабочей области канала флагами состояния, такими как нормальное завершение, ошибка ввода-вывода или устройство недоступно. Рабочая область программы содержит также указатель на очередь запросов на ввод-вывод для данного канала, которая поддерживается стандартными программами операционной системы. Дополнительные сведения о том, где располагаются рабочие области каналов УУМ/ДС и что в них находится, содержатся в приложении В. Р2 START 0 . . (инициализация} . LOOP LDA #0 ИНИЦИАЛИЗИРОВАТЬ ESB ДЛЯ ЧТЕНИЯ STA RDESB LDA #READ ВЫДАТЬ ЗАПРОС НА ЧТЕНИЕ С УСТРОЙСТВА 22 LDS #2 LDT #RDESB SVC 2 LDA #RBESB ЖДАТЬ КОНЦА ЧТЕНИЯ SVC 0 LDA #WRESB ЖДАТЬ ЗАВЕРШЕНИЯ ПРЕДЫДУЩЕЙ ЗАПИСИ SVC 0 . . {формирование последовательности внвода} . LDA #0 ИНИЦИАЛИЗИРОВАТЬ ESB ДЛЯ ЧТЕНИЯ STA WRESB LDA #WRITE ВЫДАТЬ ЗАПРОС НА ЧТЕНИЕ С УСТРОЙСТВА 14 LDS #1 LDT #WRESB SVC 2 J LOOP . . КАНАЛЬНАЯ ПРОГРАММА ДЛЯ ЧТЕНИЯ . ПЕРВАЯ КОМАНДА — READ BYTE Х'12' КОМАНДА = READ, УСТРОЙСТВО = 2 BYTE Х'1000' СЧЕТЧИК БАЙТОВ = 4096 WORD BURN АДРЕС БУФЕРА ВВОЗА . ВТОРАЯ КОМАНДА — BYTE X’000000000000’ ОСТАНОВ КАНАЛА . . КАНАЛЬНАЯ ПРОГРАММА ДЛЯ ЗАПИСИ . ПЕРВАЯ КОМАНДА — WRITE BYTE Х'24' КОМАНДА = WRITE, УСТРОЙСТВО = 4 BYTE Х'1000' СЧЕТЧИК БАЙТОВ = 4в96 WORD BUFOUT АДРЕС БУФЕРА ВЫВОДА . ВТОРАЯ КОМАНДА — BYTE X’000000000000’ ОСТАНОВ КАНАЛА . RDESB BYTE X’000000’ БЛОК состояния СОБЫТИЯ для ЧТЕНИЯ WRESB BYTE X'800000' БЛОК СОСТОЯНИЯ СОБЫТИЯ ДЛЯ ЗАПИСИ BUFIN RESB 4096 БУФЕР ВВОДА BUFOUT RESB 4096 БУФЕР ВЫВОДА • END Рис. 6.12. Программа, иллюстрирующая многократный запрос на ввод-вывод. На рис. 6.13 отмечены действия, предпринимаемые операционной системой в ответ на запрос ввода-вывода от программы пользователя. Если требуемый канал занят выполнением другой операции, операционная система ставит запрос в очередь к нему. В противном случае она запускает канал, а текущий запрос сохраняет в рабочей области канала. Возможно также, что управление будет возвращено процессу, затребовавшему ввод-вывод, и он, пока ведется обмен, сможет продолжить работу. procedure IOREQ(CHAN,CP,ESB) опросить канал командой if канал занят поставить (Cp,ESВ) в очередь к else begin сохранить (CP,ESВ) в рабочей области запустить канал командой end возвратить управление запрашивающему процессу командой LPS TIO then каналу канала SIO Рис. 6.13. Алгоритм обработки запроса на ввод-вывод (SVC 2). На рис. 6.14 показаны действия, предпринимаемые операционной системой в ответ на прерывание по вводу-выводу. Номер канала, вызвавшего прерывание, находится в слове состояния, procedure IOINTERRUPT(CHAN) исследовать флаги if нормальное begin состояния в завершение рабочей области операции канала then взять адрес ESB из рабочей области канала командой SVC оповестить через ESB о наступлении события if очередь запросов к каналу не пуста then begin взять из очереди (CP,ESB) для следующего запроса сохранить (CP,ESB) в рабочей области канала, запустить канал командой SIO end (if очередь не пуста) end (if нормальное завершение операции} else выбрать подходящие действия по исправлению ошибок if в момент наступления прерывания ЦП простаивал then DISPATCH else возвратить управление прерванному процессу командой LPS Рис. 6.14. Алгоритм обработки прерывания по вводу-выводу хранящемся в рабочей области данного прерывания. Для определения причины прерывания операционная система исследует флаги состояния в рабочей области канала. Если установлено, что операция ввода-вывода завершилась нормально, обработчик прерывания оповещает об этом операционную систему через заданный при запросе блок состояния события. Это может быть сделано как при помощи SVC-запроса, приводящего к вложенному прерыванию, так и непосредственно вызовом той части операционной системы, которая обрабатывает запросы SIGNAL. В любом случае окончание операции ввода-вывода отмечается в ESB. Каждый ожидающий этого процесс снова переводится в состояние готовности (см. разд. 6.2.2). Затем обработчик прерываний по вводу-выводу просматривает очередь запросов к данному каналу и начинает выполнение следующего запроса (если он есть). Если флагами отмечено какое-то ненормальное состояние, то операционная система осуществляет соответствующие действия по исправлению ошибок. Они, конечно, зависят от типа устройства ввода-вывода и характера обнаруженной ошибки. Например, обработка ошибки четности при работе с магнитофоном обычно сводится к перемотке ленты назад и повторению операции ввода-вывода (некоторое число раз). С другой стороны, когда кончилась бумага на печатающем устройстве, перед попыткой осуществить дальнейшие действия по исправлению ошибок оператору ЭВМ посылается сообщение. Если операционная система обнаружит, что ошибка ввода-вывода неустранима, то она может окончить процесс, выдавший запрос на вводвывод, и послать соответствующее сообщение пользователю. Или, сохранив код ошибки в ESB, она может оповестить об этом запрашивающий процесс, который сам уже может принять решение, продолжить ему выполнение или нет. После окончания работы обработчик прерываний обычно возвращает управление прерванному процессу, восстанавливая его состояние. Однако если ЦП в этот момент простаивал, то должен быть вызван диспетчер. Делается это потому, что в результате обработки прерывания некоторый процесс мог перейти в состояние готовности. Диспетчер будет вызван и в том случае, если использовалось приоритетное планирование процессов (см. разд. 622). 6.2.4. Управление реальной памятью Любая операционная система, поддерживающая одновременную работу более одного пользователя, должна обладать механизмом разделения центральной памяти между совместно выполняющимися процессами. Многие мультипрограммные системы разбивают память на разделы (partitions) с выделением Задание 1 2 3 4 5 6 7 8 Длина (шестнадца теричная) АООО 14000 А800 4000 Е000 В000 С000 D000 Рис.6.16. Задания пользователей для примеров по распределению памяти. каждому процессу своего раздела. Размер и расположение раз делов могут быть либо заранее заданы {разделы фиксированного размера}, либо назначаться динамически в процессе выполнения заданий {разделы переменного размера}. Ниже рассматриваются некоторые способы управления разделением памяти. В примерах будет использована последовательность заданий, приведенная на рис. 6.16. Предполагается, что уровень мультипрограммирования (т. е. количество одновременно выполняющихся заданий) ограничен только числом самих заданий, загружаемых в центральную память. На рис. 6.17 показано разбиение памяти на разделы фиксированного размера. Полный объем доступной памяти ЭВМ предполагается равным 50 000 байт; операционная система занимает первые 10 000 байт. Заметим, что эти и другие размеры и адреса, используемые в данном разделе, даны в шестнадцатеричном виде. Память, не занятая операционной системой, состоит из четырех разделов. Раздел 1 начинается с адреса 10000 сразу за операционной системой и имеет длину 18000 байт. Остальные разделы следуют в таком порядке: Разделы 2 и 3 имеют по 10000 байт каждый, а Раздел 4—длину, 8000 байт. В простой схеме распределения с разделами фиксированного размера каждое входящее задание загружается в наименьший подходящий по объему раздел Если размер раздела превосхо- дит размер задания, то оставшаяся внутри раздела память не используется. Система, имея вначале пустыми все четыре раздела, первым делом загрузит Задание 1 в Раздел 2 Затем Задание 2 будет загружено в единственный достаточно большой для этого Раздел 1. Задания 3 и 4 загружаются в Разделы 3 и 4. После этого все разделы оказываются занятыми, поэтому больше заданий загрузить нельзя. Получившееся в итоге распределение памяти показано на рис 6 17а. Заштрихованные области на диаграмме соответствуют неиспользованной памяти. Однажды загруженное в раздел задание остается там до конца своего выполнения. После того как задание завершится, занимаемый им раздел вновь становится доступным для использования. На рис 6 176 Задание 2 уже окончилось и в Раздел 1 было загружено Задание 5 На этом же рисунке показаны дальнейшие действия, осуществляемые по мере того, как оканчиваются остальные задания Операцио нная система Операцио н1000 ная 0 система Операцио нная система Операцио нная система Задание 2 Задание 5 Задание 5 Задание 5 Задание 1 Задание 6 Задание 6 Задание 3 Задание 3 Задание 7 Задание 4 Задание 4 Задание 4 Раздел 1 2800 0 Задание 1 Раздел 2 3800 0 Задание 3 Раздел 3 4800 0 Раздел 4 Задание 4 а б Оканчивается Задание 2 в г Оканчивается Задание 1 Оканчивается Задание 3 Рис. 6.17. Распределение памяти для заданий рис. 6.16.с использованием разделов фиксированного размера. Заметим, что сами разделы и их расположение остаются фиксированными вне зависимости от размеров занимающих их заданий Начальный выбор величины раздела в схеме с разделами фиксированного размера очень важен. Число больших разделов должно быть достаточным, чтобы длинные задания могли выполняться без слишком большой задержки Однако если больших разделов слишком много, то при выполнении коротких заданий значительное количество памяти расходуется впустую. Использование разделов фиксированного размера наиболее эффективно, когда размеры большинства заданий нахо* дятся в пределах конкретных объемов, а распределение размеров заданий меняется не часто. Это позволяет эффективно использовать доступную память посредством выделения набора разделов ожидаемому множеству заданий. На рис.6.18. демонстрируется выполнение того же множества заданий при использовании разделов переменного размера. Для каждого загружаемого задания создается новый раздел о размерами, соответствующими заданию После окончания задания отведенная ему память освобождается и может быть использована при распределении других разделов. Вначале вся память (кроме той, что отведена операционной системе) не распределена, так как заранее нет определенных разделов. Раздел для Задания 1 создается при его загрузке. Предположим, что этот раздел расположен непосредственно за операционной системой. Затем Заданию 2 отводится раздел, следующий сразу за Заданием 1, и т. д. Объем свободной памяти, оставшейся за Заданием 5, для загрузки очередного задания уже недостаточен. Когда завершается Задание 2, его раздел освобождается, и новый Раздел отводится Заданию 6. Как показано на рис. 6.186, этот новый раздел занимает часть памяти, которая отводилась Заданию 2. Остаток от прежнего раздела Задания 2 остается свободным. Теперь имеются две несмежные свободные области памяти; однако ни одна из них не велика настолько, чтобы вместить еще одно задание. На рис. 6.18в и г показано, как происходит освобождение и распределение памяти по мере того, как оканчиваются другие задания. 1000 0 Операцио нная система Операцио н1000 ная 0 система Задание 1 1А00 0 10000 10000 Операцио нная система Задание 1 1А0 00 1А00 0 Задание 6 Задание 2 Операцио нная система 2500 0 1А00 0 Задание 6 25000 Задание 6 25000 2Е00 0 2Е0 00 2Е000 Задание 7 31000 3880 0 3С80 0 Задание 3 Задание 3 Задание 4 3880 0 3С8 Задание 4 00 Задание 5 Задание 5 4А80 0 а б Задание 3 38800 3С80 0 4А80 0 Оканчивается Задание 2 Оканчивается Задание 1 г Задание 4 3С80 0 Задание 5 4А8 00 в 38800 Задание 4 Задание 5 4А80 0 Оканчивается Задание 3 Рис. 6.18. Распределение памяти для заданий рис. 6.16.с использованием разделов переменного размера. При использовании разделов переменного размера нет надобности выбирать размер раздела заранее. Однако операционной системе, которая отслеживает, какие области памяти уже распределены, а какие свободны, приходится проделывать большую работу. Обычно это делается системой при помощи поддерживаемого ею связаного списка свободных областей. Этот список просматривается при выделении нового раздела, который размещается либо в первой (первое подходящее размещение), либо в наименьшей {наиболее подходящее размещение) подходящей для него свободной области. Когда раздел освобождается, отведенная ему память объединяется со всеми смежными свободными областями и заносится в список. Подробное обсуждение и сравнение алгоритмов распределения памяти можно найти в Стендиш [1980]. Вне зависимости от используемого способа создания разделов необходимо, чтобы операционная система и аппаратные средства обеспечивали защиту памяти. При выполнении задания в одном разделе недопустимо, чтобы оно изменяло ячейки памяти другого раздела или операционной системы. В некоторых системах разрешено чтение данных из любой области памяти, а запись — только внутри отведенного заданию раздела. В других системах и чтение, и запись допускаются только в пределах собственного раздела задания. Для эффективной защиты памяти необходима и аппаратная поддержка. Можно, например, ввести пару граничных регистров, в которых будут содержаться начальный и конечный адреса раздела задания. Эти регистры не доступны непосредственно программам пользователя и могут быть использованы, только если ЦП находится в режиме супервизора. Операционная система устанавливает граничные регистры, когда пользо- вательскому заданию назначается раздел. Во время операций контекстного переключения, аналогичных тем, что вызваны командой LPS или прерыванием, содержимое этих регистров автоматически сохраняется. Таким образом, граничные регистры всегда содержат адреса начала и конца раздела, отведенного текущему активному процессу. При любом обращении к памяти аппаратура автоматически сверяет адрес, по которому идет обращение, с граничными регистрами. Если адрес находится вне раздела текущего задания, обращение к памяти не производится и генерируется программное прерывание. В УУМ/ДС используется и другой способ защиты памяти. Каждому 800-байтовому (в шестнадцатеричной системе счисления) блоку памяти ставится в соответствие 4-битовый ключ защиты памяти. Эти ключи могут быть установлены операционной системой при помощи привилегированной команды SSK (Set Storage Key—Установи ключ памяти). Каждый пользовательский процесс имеет поставленный ему в соответствие 4-битовый идентификатор процесса, который хранится в поле ID слова состояния SW. Когда заданию выделяется некоторый раздел, операционная система присваивает ключам всех блоков памяти внутри раздела значение идентификатора процесса этого задания. При каждом обращении пользовательской программы к памяти аппаратура автоматически сравнивает идентификатор процесса из SW с ключом защиты адресуемого блока памяти. Если значения этих двух полей не совпадают, то обращения к памяти не происходит и генерируется программное прерывание. Однако если ЦП находится в режиме супервизора, то этой проверки не производится; операционная система может обращаться к любой ячейке памяти. Единой проблемой для всех общецелевых способов динамического распределения является фрагментация памяти. Фрагментация имеет место, когда доступная свободная память разбита на несколько несмежных блоков, каждый из которых слишком мал для использования. Рассмотрим, например, рис. 6.18в. Чтобы разместить Задание 7, в целом имеется более чем достаточно свободной памяти; однако из-за того, что нет ни одного свободного блока достаточно большого размера, оно не может быть загружено. На рис. 6.19 показано одно из возможных решений этой проблемы: использование перемещаемых разделов (relocatable partitions). После окончания каждого задания оставшиеся разделы передвигаются как можно дальше к одному концу памяти В результате этого вся доступная свободная память собирается в один общий блок, который больше подходит для распределения новых разделов. Как показано на рис. 6.19, этот способ может привести к более эффективному использованию памяти по сравнению с тем, что достигается с помощью неперемещаемых разделов. Однако копирование заданий из одного места памяти в другое может потребовать значительного количества времени. Этот недостаток часто перевешивает преимущества усовершенствованного использования памяти. При работе с перемещаемыми разделами возникают проблемы и с переместимостью программ. Рассмотрим, например, программу, приведенную на рис. 6.20а. Команда STA имеет расширенный формат, поэтому она в оттранслированном виде содержит истинный адрес 08108. При использовании методов, описанных в гл. 2 и 3, это адресное поле будет отмечено ассемблером. Чтобы получить истинный адрес, по которому загружена программа, значение адресного поля модифицируется загрузчиком. Например, если начальная загрузка РЗ осуществлена по истинному адресу 2ЕООО, то адресное поле команды STA, как показано на рис. 6.206, будет изменено на 36108. Операцио н10000 ная система 10000 Задание 1 Операцио нная система 1000 0 Задание 1 1А00 0 1А00 0 1A80 0 1E80 0 Задание 3 Операцио нная система 1000 0 Задание 3 1400 0 Операцио нная система Задание 4 Задание 5 Задание 4 Задание 2 24800 Задание 5 28800 2Е00 0 Задание 4 2C80 0 Задание 5 Задание 3 36800 38800 3С80 Задание 4 0 2200 0 3780 0 Задание 6 Задание 6 2D00 0 Задание 6 3900 0 Задание 7 Задание 7 Задание 8 41800 4380 0 Задание 5 4600 0 Задание 7 4А80 0 4D80 0 а б Оканчивается Задание 2 в г Оканчивается Задание 1 Оканчивается Задание 3 Рис. 6.19. Распределение памяти для заданий рис. 6.16.с использованием перемещаемых разделов. Предположим теперь, что раздел, содержащий РЗ, будет начинаться не с адреса 2ЕООО, как на рис. 6.19а, а с адреса 1АООО, как на рис. 6.196. Тогда адрес в команде STA будет неверным. Действительно, он будет указывать на ячейку памяти, которая является частью раздела, отведенного другому заданию. Аналогичная проблема возникнет, если РЗ загрузит в базовый регистр адрес некоторой своей части или создаст структуру данных, использующую указатель адреса. После начальной загрузки программы все перемещаемые значения определены ассемблером, что упрощает перемещение программ! Однако во время своего выполнения программа может испол! зовать регистры и ячейки памяти произвольным образом. В о( щем случае операционная система не в состоянии определит] какие значения являются адресами, а какие другими типам данных. Таким образом, перемещение программы во время, вь полнения осуществить намного сложнее, чем после начально загрузки. Применение перемещаемых разделов на практике требует некоторой аппаратной поддержки. Один из обычно используемых для этого способов показан на рис. 6.20в. В этом случае существует специальный регистр перемещения, устанавливаемый операционной системой и содержащий начальный адрес текущей активной программы. Во время операций контекстного переключения содержимое этого регистра автоматически сохраняется или восстанавливается, а при перемещении программы на новое место его значение изменяется операционной системой. При любом обращении программы пользователя к памяти значение, содержащееся в регистре перемещения, автоматически складывается с адресом. Например, команда STA на рис. 6.20в обращается к адресу 08108; однако, учитывая регистр перемещения, истинный адрес, к которому осуществляется обращение, равен 36108. Если начало программы РЗ было перенесено на адрес 1АООО, как показано на рис. 6.20г, то операционная система изменит значение регистра перемещений для РЗ на 1АООО. Таким образом, адрес, к которому действительно обращается команда STA, будет равен 22108. Адре с 0000 Исходный оператор Р3 1840 8108 BUF F2 STAR 0 T . . . BUFF +STA 2 . . . … . Объектный код OF108108 2Е000 36108 2F840 OF13610 8 . . END 36108 а Регистр перемещений б 1A000 Регистр перемещения 2E000 1A00 0 08108 1B84 0 + OF108 108 2E00 0 2F84 0 OF10810 8 08108 22108 + 22108 36108 36108 В г Рис.6.20. Использование регистра перемещений при вычислении адреса. Важно понять, что регистр перемещения управляется операционной системой; пользовательской программе он не доступен. Таким образом, этот регистр в корне отличается от задаваемых программно базовых регистров, имеющихся в УУМ/ДС, System/370 и многих других ЭВМ. Регистр перемещения автоматически включается каждый раз, когда программа обращается к какой-нибудь области памяти; этим достигается тот же эффект, что и при фактической загрузке каждой программы с истинного адреса 00000. И в самом деле, на многих ЭВМ для программы пользователя не существует прямого способа определить ее реальное месторасположение в памяти. Таким образом, этот тип перемещений применим к адресам, находящимся в указателях структур данных, к значениям в базовых регистрах, равно как и к адресам в командах. Заметим также, что подобное автоматическое перемещение, выполняемое аппаратурой, полностью исключает необходимость перемещения программ загрузчиком. В этом разделе были рассмотрены методы распределения программных разделов с заранее определенными размерами. В некоторых операционных системах программам пользователей во время их выполнения разрешается динамически запрашивать дополнительную память. Дополнительно выделяемая память не обязательно должна быть смежной с первоначально назначенным разделом. Подобное динамическое распределение памяти обычно осуществляется методами, схожими с теми, что использовались при управлении памятью для структур данных. Хорошее изложение этих методов можно найти в Стендиш [I980]. 6.3. Машинно-независимые свойства операционных систем В этом разделе будут кратко рассмотрены некоторые общие функции операционных систем, непосредственно не зависящие от архитектуры машины, на которой они функционируют. Эти свойства могут быть реализованы на более высоком уровне по сравнению с рассмотренными ранее. Поэтому они не столь фундаментальны с точки зрения основного назначения операционных систем, как вопросы аппаратной поддержки, рассмотренные в предыдущем разделе. Объем книги не позволяет обсуждать их в деталях. Для читателей, которые захотят больше узнать о вопросах, изложенных ниже, приводятся ссылки на литературу. В разд. 6.2.3 были рассмотрены возможные способы управления операциями ввода-вывода. В разд. 6.3.1 изучается аналогичный вопрос для более высокого уровня: управление логическими файлами и работа с ними. В разд 6.3.2 обсуждается проблема планирования, т. е. выбора из заданий пользователей кандидатов на обслуживание, производимое на более низком уровне, о чем говорилось ранее. В разд. 6.3.3 рассматривается основной объект распределения ресурсов, осуществляемого операционной системой, и возникающие при этом сложности. Наконец, в разд. 6.3.4 вводятся такие важные понятия, как защита и безопасность операционных систем. 6.3.1. Работа с файлами Данный раздел посвящен некоторым функциям, выполняемым типичными операционными системами в процессе управления и работы с файлами. В большинстве систем программы пользователей имеют возможность запрашивать ввод-вывод при помощи средств, описанных в разд. 6.2 (канальных программ и SVC-запросов). Однако это достаточно неудобно, так как программисту должны быть известны коды канальных команд и их форматы, а программе пользователя — номера каналов, устройств и, возможно, реальный адрес требуемой записи, если используется устройство с прямым доступом (например, диск). Кроме того, нужно отслеживать окончание ввода-вывода, выполнять буферизацию и объединение в блоки, о чем будет сказано ниже. Функция управления файлами, осуществляемая операционной системой, является промежуточным звеном между программой пользователя и супервизором ввода-вывода (рис. 6.27). Программа пользователя на логическом уровне с помощью имен файлов, ключей и т. п. делает запросы, например «прочитать следующую запись из файла F». Программа управления файлами реализует метод доступа, транслируя логические запросы в физические запросы на ввод-вывод (т. е. канальные программы), и передает их супервизору, действующему при управлении операцией ввода-вывода (разд. 6.2.3). Чтобы перевести логические программные запросы в канальные программы, система управления файлами должна иметь информацию о расположении и структуре файлов. Эту информацию она получает из структуры данных, называемой катало' гом, и файловых информационных таблиц. В действительности термины, используемые для обозначения подобных структур, так же как и их форматы и содержание, в разных операционных системах различны. Каталог устанавливает соответствие логических имен файлов с их физическим местонахождением и может предоставить о файлах некоторую общую информацию, В файловой информационной таблице содержится дополнительная информация, например об организации файлов, длине записи и форматах, о способе индексирования, если оно имеется. Чтобы начать работу с файлом, система просматривает каталог и определяет местонахождение соответствующей файловой информационной таблицы. Система также может создать буфера для размещения в них блоков файлов, которые будут прочитаны или записаны. Эта процедура называется открытием файла. После окончания работы с файлом буферные и другие рабочие области и указатели уничтожаются. Такая процедура называется закрытием файла. Программа пользователя Каталог Запись Логический запрос («прочитать следующую запись из файла F») Распорядитель файлов Файловая информац ионная Блок таблица Физический запрос (канальная программа) Супервизор ввода-вывода Прерывание по вводу-выводу SiO Канал Рис.6.27. Использование программы управления файлами для ввода-вывода. Одной из наиболее важных функций управления файлами является автоматическое выполнение операций буферизации и объединения в блоки читаемых и записываемых файлов. На рис. 6.28 эти операции иллюстрируются последовательно вводящим файлом. Предполагается, что чтение записей осуществляется программой пользователя последовательно с начала файла и до его конца. Логически файл состоит из записей длиной 1024 байт; физически, однако, файл образован 8192-байтовыми блоками, каждый из которых содержит 8 логических записей. Этот вид объединения записей в блоки обычно осуществляется конкретными типами запоминающих устройств в целях экономии времени обработки и пространства памяти. Подробнее об этом говорится в Лумис [1983]. На рис. 6.28а показана ситуация, сложившаяся после того, как файл был открыт и программа пользователя осуществила свой первый запрос на чтение записи. Система управления файлами уже выдала запрос на ввод-вывод для чтения в буфер В1 первого блока файла и должна ждать завершения этой операции, прежде чем сможет передать требуемую запись пользователю. На рис. 6.286 первый блок прочитан. Он находится в буфере В1 и содержит логические записи с номерами от 1 до 8. Теперь система управления файлами может передать требуемую запись программе пользователя, для чего указатель Р устанавливается на первую логическую запись. Для чтения в буфер В2 второго блока файла система управления файлами выдает второй физический запрос на ввод-вывод. В1 В2 Читать первый блок а В1 1 2 3 4 5 6 Р 7 8 В2 Читать второй блок б В1 1 2 Р 3 4 5 6 7 8 В2 Читать второй блок в В1 1 2 3 4 5 6 Р 7 В2 8 Читать второй блок г В1 В2 Читать третий блок 9 10 11 12 13 14 15 16 Р д Рис. 6.28. Блокирование и буферизация последовательного файла. Когда программа пользователя в следующий раз делает запрос на чтение записи, уже нет необходимости ждать каких-либо действий по вводу-выводу. Система просто передвигает указатели, F' на логическую заиись 2 и возвращает управление пользователю. Эта операция показана на рис. 6.28в. Заметим, что физическая операция ввода-вывода по чтению второю блока в буфер В2 все еще выполняется. Те же действия повторяются и для других логических записей первого блока (рис. 6.28г). Если программа пользователя осуществляет запрос на чтение 9-й записи до завершения операции ввода-вывода для блока, системе управления слайдами снова приходится заставлять программу ждать. После того как второй блок прочитан, указатель Р переводится на первую запись буфера В2. После этого система управления файлами выдает запрос на ввод-вывод для чтения в буфер В1 третьего блока и процесс продолжается подобно тому, как было описано выше. Заметим, что использование двух буферных областей позволяет совмещать работу с одним блоком и чтение другого. Подобный способ, часто называемый двойной буферизацией, широко используется при обмене последовательными файлами. В предыдущем примере программа пользователя осуществляла просто серию запросов на чтение записей, ничего ПС зная о буферизации и деталях выполнения физических запросов на ввод-вывод. Сравните это с программой на рис. 6.11, которая выполняет схожую функцию буферизации, имея дело непосредственно с супервизором ввода-вывода. Ясно, что использование системы управления файлами сильно упрощает программу, облегчает ее написание и, следовательно, уменьшает число возможных ошибок. Не требуется и повторение во многих программах одних и тех же кодов. Программы управления файлами выполняют также много других функций: распределение пространства на внешних запоминающих устройствах, реализацию правил управления доступом к файлам и их использованием. Дальнейшее обсуждение этих вопросов можно найти в Дейтел [1984] и Мэдник [1974]. 6.3.2. Планирование заданий Планирование заданий - это выбор заданий пользователей для выполнения. В однопрограммной системе оно сводится к определению порядка выполнения заданий. В мультипрограммной системе планировщик определяет порядок входа заданий в крещение. На рис. 6.29а показана типичная двухуровневая система планирования в мультипрограммной системе. Задания, поступающие в систему, помещаются в очередь входных заданий, из которой они затем выбираются планировщиком заданий. Выбранные задания становятся активными; это означает, что они начинают принимать участие в операции планирования процессов, описанной в разд. 6.2.2. Двухуровневая процедура вводится в целях ограничения уровня мультипрограммирования, т. е. числа заданий пользователей, между которыми происходит разделение ЦП и других системных ресурсов. Это необходимо для поддержания эффективной работы мультипрограммной системы. Если система пытается выполнять одновременно слишком много заданий, перекрытие при управлении ресурсами становится слишком большим, а количество доступных каждому заданию ресурсов - слишком малым. В результате производительность системы снижается. В только что описанной схеме планировщик заданий используется в качестве инструмента для поддержания требуемого уровня мультипрограммирования. Однако этот идеальный уровень Входная очередь Планировщик заданий Активные задания Диспетчер ЦП а Входна я очередь Планировщик заданий Промежуточный планировщик Активные задания Диспетчер ЦП Приостанов ленные задания б Рис. 6.29. а – Двухуровневая система планирования и б – трехуровневая система планирования. вень может меняться в зависимости от характера выполняющихся заданий. Рассмотрим, например, систему, в которой для управления памятью применяется стратегия размещения страниц, по запросу. Количество заданий, одновременно использующих реальную память, по существу, не ограничено. Выполняться может любое задание, имеющее по крайней мере одну страницу. Однако, когда в памяти у задания отсутствует некоторое критическое число страниц, начинается пробуксовка, и от этого страдает производительность всей системы. К сожалению, трудно заранее определить то количество выделяемых заданию страниц, которое позволит избежать пробуксовки. Вдобавок в течение выполнения программы оно может сильно меняться, поэтому во время работы системы может меняться и требуемый уровень мультипрограммирования. Если входная очередь не пуста, уровень мультипрограммирования достаточно просто можно увеличить, обратившись к планировщику заданий. Труднее бывает его уменьшить, например для того, чтобы остановить пробуксовку. Это обычно достигается при помощи процедуры трехуровневого планирования заданий, которая показана на рис. 6.29б. Планировщик заданий и планировщик процессов (т. е. диспетчер) работают, как и ранее. Однако существует также планировщик промежуточного уровня, управляющий производительностью системы и, если требуется, регулирующий уровень мультипрограммирования. Если он слишком высок, промежуточный планировщик понижает его, приостанавливая или свертывая одно или несколько заданий. Если уровень мультипрограммирования слишком низок, промежуточный планировщик возобновляет работу приостановленных заданий пли обращается к планировщику заданий для того, чтобы активизировать новые. Промежуточные планировщики могут использоваться и для регулирования приоритета диспетчеризации активных заданий, основываясь на наблюдении за ними в процессе выполнения. Вся система планирования обычно базируется на системе приоритетов, предназначенных для достижения определенных целей. Первой целью может быть, например, получение максимальной пропускной способности системы, т. е. выполнение наибольшей вычислительной работы за кратчайшее время. Ясно, что для этого требуется эффективное использование всех системных ресурсов. Другая задача - получение наименьшего среднего времени прохождения, т. е. времени между загрузкой задания пользователем и завершением его выполнения. С системами разделения времени связана задача минимизации предполагаемого времени ответа (времени между нажатием клавиши ENTER на терминале и получением на это ответа системы). Существует и много других задач планирования. Например, мы можем захотеть обеспечить гарантированный уровень обслуживания посредством ограничения максимально возможного времени прохождения и времени ответа. Или справедливости ради предоставить всем одинаковый уровень обслуживания. С другой стороны, исходя из внешних причин, возможно, потребуется предоставить определенным заданиям наивысший приоритет. В некоторых системах у пользователей имеется возможность за большую цену получить более высокий приоритет; в этом случае целью планирования может заработать побольше денег. Первые две задачи, упомянутые выше, - высокая пропускная способность и малое среднее время прохождения и время ответа - обычно принимаются в качестве желательных свойств системы. К сожалению, они часто несовместимы. Рассмотрим, например, систему разделения времени, работающую с большим числом терминалов. Мы можем решить, что обеспечить лучшее время ответа нужно посредством более быстрого переключения управления между различными терминалами пользователей. Для этого при диспетчеризации каждому процессу можно выделять меньший квант времени. Однако это приводит к увеличению частоты операций контекстного переключения, что заставляем оперативную систему чаще принимать решения по распределению ЦП и других ресурсов. Это означает, что накладные расходы операционной системы будут выше, а время, предоставляемое заданиям пользователей, меньше, поэтому пропускная способность в целом уменьшится. Задание I Т1 = 2 Т2 = 5 Задание 2 1 Тср = 3,5 2 3 4 5 Время (в минутах) а Задание 1 Т1 = 4,5 Т2 = 4,3 Задание 2 1 Тср = 4,4 2 3 4 5 Время (в минутах) б Рис. 6.30. Сравнение времени прохождения и пропускной способности для а однопрограммной системы, б - для мультипрограммной системы. С другой стороны, рассмотрим однопрограммную систему пакетной обработки. Каким образом в этой системе выполняются два задания, показано на рис. 6.ЗОа. Обратите внимание на промежутки времени (они показаны пробелами на горизонтальных линиях, соответствующих Заданиям 1 и 2), когда ЦП простаивал. Если оба задания запущены в момент времени 0, то время прохождения Задания 1 (Т1) равно 2 мин, а время прохождения Задания 2 (Т2) - 5 мин. Среднее время прохождения Тср равно 3,5 мин. Рассмотрим теперь мультипрограммную систему, в которой одновременно выполняются два задания, как это показано на рис. 6.306. Заметьте, что разделение ЦП между заданиями происходит таким образом, что полное время простоя уменьшается; это феномен, похожий на тот, что показан на рис. 6.15. Из-за меньшего времени простоя два задания завершаются быстрее: за 4,5 мин вместо 5мин. Это означает, что пропускная способность системы стала больше: тот же самый объем работы был сделан за меньшее время. Однако среднее время прохождения задания стало хуже: 4,4 мин вместо 3,5 мин. Как правило, чаще всего используются две стратегии планирования заданий: первым пришел - первым обслужен (FCFS - First Cotfle-Firsl Served) и кратчайшее задание - первым (SJ F - Shortest Job First). Стратегия FCFS стремится обслуживать все задания одинаково, минимизируя тем самым время прохождения; SJF обеспечивает снижение среднего времени прохождения, так как короткие задания могут вынужденно ожидать обслуживания в течение долгого времени. Примеры этих свойств и обсуждение других стратегий планирования можно найти в Дейтел [1984], Лорип [1981], Мэдник [1974]. 6.3.3. Распределение ресурсов В разд. 6. 2 обсуждалось, каким образом операционная система может управлять такими ресурсами, как центральная память, каналы ввода-вывода и ЦП. Эти ресурсы используются всеми заданиями пользователей; распределение осуществляется системой автоматически. Ниже будет рассмотрена более общая функция распределения ресурсов, выполняемая многими операционными системами. Ее можно использовать для управления распределением таких ресурсов пользователя, как файлы и структуры данных. На рис. 6.31а при помощи двух программ иллюстрируется необходимость применения этой функции. Обе программы используют последовательный стек, созданный какой-то третьей программой. Внешние переменные STACK и ТОР содержат соответственно базовый адрес стека и относительный адрес элемента данных, находящегося в его вершине. Предполагается, что внешние ссылки на переменные STACK и ТОР обрабатываются методами, аналогичными рассмотренным в гл. 3. Программа Р1 добавляет данные в стек; для этого она увеличивает предыдущее значение ТОР на 3, заносит содержимое регистра А в вершину стека, а затем запоминает новое значение ТОР (строки 24-27). Программа Р2 изымает данные; для этого она заносит значение из вершины стека в регистр А и уменьшает затем значение ТОР на 3 (строки 37-40). Для простоты мы не использовали команду, требуемую для обработки пустого стека и переполнения. Если процессы Р1 и Р2 выполняются одновременно, то они могут работать как правильно, так и неправильно. Предположим, например, что текущее значение ТОР равно 12. Если выполнятся команды с номерами 24-27 программы Р1, то в байты. Такая стратегия обслуживания часто называется UFO.-Прим, ред. Гл. 6. Oi-lepallhontime системы 1 2 3 P1 START 0 ЕХТREF STACK,ТОР LDS #3 РЕГИСТР S = КОНСТАНТА 3 . . . +LDX TOP ВЗЯТЬ УКАЗАТЕЛЬ ВЕРШИНЫ СТЕКА ADDR S,X УВЕЛИЧИТЬ ЕГО ЗНАЧЕНИЕ +STA STACK,X ДОБАВИТЬ В СТЕК НОВОЕ ДАННОЕ +STX TOP СОХРАНИТЬ НОВОЕ ЗНАЧЕНИЕ УК-ЛЯ . . . END P2 START 0 EXTREF STACK,TOP LDS #3 РЕГИСТР S = КОНСТАНТА 3 . . . +LDX TOP ВЗЯТЬ УКАЗАТЕЛЬ ВЕРШИНЫ СТЕКА +LDA STACK,X ВЗЯТЬ НОВОЕ ДАННОЕ ИЗ СТЕКА SUBR S,X УМЕНЬШИТЬ ЗНАЧЕНИЕ УКАЗАТЕЛЯ +STX TOP СОХРАНИТЬ НОВОЕ ЗНАЧЕНИЕ УК-ЛЯ . . . END а 24 25 26 27 40 1 2 3 37 38 39 40 75 1 P1 2 3 22 23 24 25 START 0 EXTREF STACK,TOP LDS #3 РЕГИСТР S = КОНСТАНТА 3 . . . LDT +SNAME УСТАН. УКАЗ-ЛЬ НА ИМЯ РЕСУРСА SVC 3 ЗАПРОСИТЬ РЕСУРС +LDX TOP ВЗЯТЬ УКАЗАТЕЛЬ ВЕРШИНЫ СТЕКА ADDR S,X УВЕЛИЧИТЬ ЕГО ЗНАЧЕНИЕ 26 27 28 47 48 +STA STACK,X ДОБАВИТЬ В СТЕК НОВОЕ ДАННОЕ +STX TOP СОХРАНИТЬ НОВОЕ ЗНАЧЕНИЕ УК-ЛЯ SVC 4 ОСВОБОДИТЬ РЕСУРС . . . SNAME BYTE ‘STACK1’ END 1 P2 2 3 35 36 37 38 39 40 41 74 75 START 0 EXTREF STACK,TOP LDS #3 РЕГИСТР S = КОНСТАНТА 3 . . . LDT +STKNM УСТАН. УКАЗ-ЛЬ НА ИМЯ РЕСУРСА SVC 3 ЗАПРОСИТЬ РЕСУРС +LDX TOP ВЗЯТЬ УКАЗАТЕЛЬ ВЕРШИНЫ СТЕКА +LDA STACK,X ВЗЯТЬ НОВОЕ ДАННОЕ ИЗ СТЕКА SUBR S,X УМЕНЬШИТЬ ЗНАЧЕНИЕ УКАЗАТЕЛЯ +STX TOP СОХРАНИТЬ НОВОЕ ЗНАЧЕНИЕ УК-ЛЯ SVC 4 ОСВОБОДИТЬ РЕСУРС . . . STKNM BYTE ‘STACK1’ END б Рис. 6.31. Управление ресурсами и использование сервисных запросов операционной системе. 15-17 стека ею будет занесено новое данное, а значение ТОР станет равно 15. Если затем будут выполнены команды 37-40 программы Р2, то только что добавленное Р1 данное будет считано, а значение ТОР снова станет равным 12. Так выглядит правильная работа Р1 и Р2; оба процесса выполняют требуемые операции над стеком, не влияя друг на друга. Правильная последовательность работы будет и тогда, когда сначала Р2 выполнит строки 37-40, а затем Р1 - строки 24-27. Теперь предположим, что после выполнения строки 24 у Р1 кончается отведенный ей квант времени. Происшедшее в результате этого прерывание от таймера вызовет сохранение значений всех регистров; сохраненное значение регистра X будет равно 12. Предположим теперь, что диспетчер передает управление Р2, которая затем выполняет строки 37-40. Так как значение ТОР программой Р1 еще не было изменено, Р2 возьмет данное из байтов 12-14 стека и сделает значение ТОР равным 9. Когда Р1 снова получит управление ЦП, регистр X все еще будет содержать значение 12. Таким образом, в результате выполнения строк 25-27 в байты 15-17 стека будет добавлено новое данное, а значение ТОР установлено в 15. Последовательность только что рассмотренных событий приведет к неправильной работе Р1 и Р2. Данное, считанное Р2 по-прежнему как бы является частью стека, н получается, что стек содержит на одно данное больше, чем должно быть. Неправильные результаты получаются н в некоторых других случаях. Такие ситуации могут возникать всякий раз, когда два одновременно работающих процесса пытаются обменяться одним и тем же файлом или структурой данных. Избежать ситуаций такого рода можно, монополизировав управление стеком на время выполнения операций модификации каждой из программ. На рис. 6.316 показано типичное решение проблемы, при котором используются запросы на обслуживание операционной системе. Выполнением команды SVC 3 программа Р1 запрашивает монопольное управление стеком. Требуемый ресурс задается регистром Т, указывающим на логическое имя (определяемое пользователем), присвоенное стеку. После добавления в стек нового данного и изменения ТОР Р1 отказывается от монопольного управления, выполнив команду SVC 4. Программа Р2 осуществляет аналогичную последовательность действии. Операционная система в ответ на запрос управления ресурсом проверяет, назначен ли он какому-нибудь другому процессу или нет. Если ресурс свободен, то управление передается запрашивающему процессу. Если ресурс занят, то система переводит запрашивающий процесс в заблокированное состояние, в котором он находится до тех пор, пока ресурс не станет доступным. Пусть, например, в данный момент свободен ресурс STACKI. Если Р1 запросит его (строка 23), то управление операционной системой будет возвращено непосредственно этой программе. Как и раньше, предположим, что сразу после выполнения строки 24 заканчивается квант времени, выделенный Р1. Затем управление ЦП переходит к Р2; однако STACK 1 остается занятым Р1. Таким образом, после запроса Р2 ресурса STACKI в строке 36 программа будет переведена в состояние блокировки. В конце концов Р1 снова получит управление и завершит работу со стеком. Когда Р1 освободит STACKI (строка 28), он будет передан Р2, которая из состояния блокировки перейдет в состояние готовности и после получения управления ЦП сможет выполнить операцию модификации. Читателю следует внимательно просмотреть последовательность этих событий, чтобы увидеть, как с помощью данного метода удается избежать возникновения описанных выше проблем. К сожалению, использование операций запроса и отказа может привести к проблемам другого рода. Рассмотрим, например, программы на рис. 6.32. Первой управление ресурсом RESI запрашивает РЗ; позже она запрашивает ресурс RES2. Программа Р4 использует те же два ресурса, однако RES2 она запрашивает перед RESI. Предположим, что РЗ после запроса получает управление ресурсом RESI; при этом отведенный ей квант времени кончается до того, как она 1 P3 START 0 . . 2 LDT =R1 ЗАПРОС RES1 3 SVC 3 . . 4 LDT =R2 ЗАПРОС RES2 5 SVC 3 . . 6 LDT =R2 ОСВОБОЖДЕНИЕ RES2 7 SVC 4 . . 8 LDT =R1 ОСВОБОЖДЕНИЕ RES1 9 SVC 4 . . 10 11 12 R1 R2 1 P4 BYTE ‘RES1’ BYTE ‘RES2’ END START 0 . . 2 LDT =R2 ЗАПРОС RES2 3 SVC 3 . . 4 LDT =R1 ЗАПРОС RES1 5 SVC 3 . . 6 LDT =R1 ОСВОБОЖДЕНИЕ RES1 7 SVC 4 . . 8 LDT =R2 ОСВОБОЖДЕНИЕ RES2 9 SVC 4 . . 10 R1 BYTE ‘RES1’ 11 R2 BYTE ‘RES2’ 12 END Рис.6.32. Запрос ресурсов, приводящий к потенциальной самоблокировке. сможет затребовать RES2. Затем может быть активизирована Р4. Предположим, что Р4 запрашивает и получает управление ресурсом RES2. Такая последовательность событий ведет к ситуации, в которой дальше не смогут выполняться ни РЗ, ни Р4. Действительно, программа Р4, дойдя до строки 4, затребует управление ресурсом RES1, после чего она будет переведена в состояние блокировки, так как PESI отдан РЗ. Аналогично РЗ, дойдя до строки 4, затребует управление RES2 и тоже будет переведена в состояние блокировки, так как RES2 отдан Р4. Поскольку свободных ресурсов нет, ни один процесс не сможет продолжить работу. Такая ситуация является примером взаимоблокировки (deadlock); она характеризуется тем, что в некотором наборе процессов каждый отдельный процесс постоянно заблокирован, так как необходимые ресурсы заняты другими процессами. Взаимоблокировка снимается освобождением ресурса за счет удаления из решения одного или нескольких вовлеченных в нее заданий. Существует ряд методов, позволяющих избежать взаимоблокировки. Например, система может потребовать, чтобы процесс запрашивал все свои ресурсы одновременно или делал это в определенном порядке (как то: RES1 перед RES2). К сожалению, при этом из-за продолжительной занятости ресурсов снизится работоспособность всей системы. Описание методов нахождения и предотвращения взаимоблокировок можно найти в Дейтел [1984]. Выше рассмотрены частные случаи общих проблем взаимоисключения и синхронизации процессов. Их обсуждение и способы разрешения можно найти в Дейтел [1984] и Холт [1978]. 6.3.4. Защита Во всякой операционной системе, обслуживающей одного или нескольких пользователей, должны существовать средства, обеспечивающие защиту отдельного пользователя от возможных несанкционированных действий других. Например, пользователь должен иметь возможность создавать файлы, которые не могут быть прочитаны или изменены другими пользователями. В целом проблема безопасности и защиты достаточно сломана. В данном разделе будут коротко рассмотрены основные функции защиты, выполняемые типичной операционной системой. Они должны быть совмещены с безопасностью на физическом уровне, с административными процедурами и другими способами контроля, осуществляемыми для обеспечения эффективной защиты. Дополнительную информацию по защите, безопасности и секретности можно найти в Дейтел [1984], Деннинг [1983] и Фернандес [1981]. В большинстве многопользовательских операционных систем существует некоторый вид контроля доступа или авторизации, чаще всего они реализуются на основе матрицы доступа, аналогичной той, что показана на рис. 6.33. В этом примере пользователи обозначены u1, u2 и u3. Права пользователя по отношению к некоторому объекту (файлу или программе) задаются строками в матрице доступа. Так, пользователь u2 может читать файлы fl и f2, писать в файл f2, выполнять программу p2. Однако ему запрещен какой-либо доступ к файлу f3 или программе pl. Права на доступ к вновь созданному объекту обычно определяются его создателем. В некоторых системах пользователям, обладающим определенными правами на доступ, разрешено передать их другим Пользователи u1 u2 u3 f1 R R R,W Файлы f2 f3 R R,W R Программы p1 p2 E E E R,E Рис. 6.33. Пример матрицы доступа. пользователям. Доступ к контролируемым объектам осуществляется при помощи запросов на обслуживание операционной системе. Каждому пользователю, авторизация которого произведена неверно, в доступе будет отказано. В действительности матрица доступа достаточно разрежена. Большинство пользователей имеет доступ к относительно малому числу объектов системы. По этой причине информация, связанная с правами на доступ, часто хранится в виде списка авторизаций для каждого объекта (т. е. списка авторизованных пользователей) или списка возможностей (т. е. списка объектов, к которым возможен доступ). Дальнейшую информацию по реализации модели матрицы доступа можно найти в Деннинг [1983] . Эффективность только что описанного метода зависит, очевидно, от правильной идентификации пользователей. С тем чтобы применить верные правила контроля за доступом, операционная система должна иметь возможность распознавать каждого пользователя. Один из наиболее часто используемых способов идентификации системные пароли. В большинстве систем пользователю для запуска задания необходимо ввести секретный пароль или зарегистрироваться с помощью интерактивного терминала. Иногда этот способ применяется шире и подтверждение пароля требуется для доступа к конкретным файлам, программам и т. и. В некоторых системах имеется главная таблица паролей, используемая операционной системой для проверки паролей, получаемых от пользователей; однако она является потенциально уязвимым местом всей системы защиты. Во избежание этого во многих операционных системах таблица хранится в зашифрованном виде. Более подробно об этом и других подобных методах можно узнать в Деннинг [1983]. Так как информация иногда должна находиться вне средств защиты, система идентификации и авторизации пользователей не всегда позволяет разрешить все проблемы, связанные с безопасностью. Рассмотрим, например, рис. 6.34а. Идентификация и авторизация пользователя за интерактивным терминалом произведены правильно. Мы предполагаем, что вычислительная система осуществляет соответствующий контроль доступа и имеет средства обеспечения безопасности на физическом уровне. Файл F ЦП Файл F ЦП Шифратор Прослушивание Линия связи Средства защиты Дешифратор Терминал Терминал а б Рис.6.34. Использование кодирования данных для их защиты во время передачи. Предполагается также, что и сам терминал на физическом уровне защищен. Однако линию связи между вычислительной системой и терминалом защитить бывает сложно или вообще невозможно. (Примером может служить проблема предотвращения подслушивания, когда терминал связан с ЭВМ через обычную телефонную сеть.) Это означает, что во время передачи уязвимой является любая информация из файла F, которая передается терминалу. Аналогично можно узнать все пароли, посылаемые пользователем, а следовательно, получить доступ к защищенным объектам. Обычно проблемы такого рода решаются с помощью шифровки данных. Информация, которая должна быть передана по незащищенной линии связи, зашифровывается (кодируется), когда она еще находится в пределах средств защиты. Переданная информация расшифровывается (декодируется) уже под защитой соответствующих средств получателя. Прослушивающий не сможет понять передаваемую зашифрованную информацию. Операции шифровки и дешифровки одинаково успешно могут выполняться как аппаратурой, так и программным обеспечением. Способов шифровки существует много. Для более подробного ознакомления с этими методами смотрите Деннинг [1983]. Конечно, эффективность любой системы зашиты всецело зависит от правильности и защищенности ее самой. В рассмотренной нами системе информация по контролю за доступом должна быть защищена от неавторизованных изменений. Должен существовать и механизм, обеспечивающий невозможность доступа пользователей к защищенным объектам, минуя систему безопасности. В этой связи часто бывают полезны такие аппаратные средства, как режимы пользователя/супервизора, привилегированные инструкции и механизм защиты памяти. Очень важно быть уверенным и в том, что част;, операционной системы, связанная с правилами защиты (ядро безопасности) функционирует правильно. Дальнейшее обсуждение этих вопросов может быть найдено в Дейтел [1984], где есть и обзор слабых мест в средствах обеспечения безопасности операционных систем, и интересные н поучительные случаи по вскрытию защиты системы. 6.4. Способы построения операционных систем В этом разделе будут вкратце рассмотрены некоторые важные вопросы, касающиеся создания операционных систем и их структуры. В разд. 6.4.1 вводится понятие операционной системы с иерархической структурой, используемой при построении многих реальных систем. В разд. 6.4.2 показано, как операционная система может поддерживать многочисленные виртуальные машины. С их помощью у пользователя создается впечатление, что он работает на выделенной ему части аппаратных средств. Разд. 6.4.3 посвящен мультипроцессорным операционным системам; в нем обсуждаются некоторые способы распределения заданий между процессорами. 6.4.1. Иерархическая структура Многие реально существующие операционные системы разработаны и реализованы на основе иерархической структуры. Примеры подобно_ структуры приведены на рис. 6.35. В этом случае основным принципом, как видно из рис. 6.35а, является обобщение идеи расширенной машины, приведенной нарис.6.1. Каждый слой, или уровень, в структуре может использовать функции, предоставляемые ему более низким уровнем, как если бы они являлись частью реальной машины. Так, Уровень О, часто называемый ядром (kernel) операционной системы, имеет дело непосредственно с аппаратурой; Уровень 1 имеет дело с интерфейсом, предоставляемым уровнем О, и т. д. Программы пользователя имеют дело с интерфейсом самого высокого уровня (в данном случае Уровня 3), который представляет собой интерфейс пользователя, рассмотренный в разд. 6.1. Интерфейс пользователя Уровень 3 Уровень 2 Реальная машина Уровень 1 Уровень 0 (ядро) а Уровень 3 2 1 0 Функции Управление файлами Управление памятью Управление вводом-выводом Диспетчеризация, управление ресурсами б Рис. 6.35. Пример системы с иерархической структурой. На рис. 6.356 показаны функции, соответствующие уровням рассматриваемой структуры. Их расположение определяется взаимоотношениями между выполняемыми ими операциями. В общем случае функции каждого уровня могут обращаться лишь к функциям того же или более низкого уровня; таким образом, не должно быть внешних вызовов. В нашем примере программы управления файлами (УровеньЗ) для распределения буферов должны использовать менеджер памяти (Уровень 2), а для чтения и записи блоков - супервизор ввода-вывода. Если управление памятью производится при помощи размещения страниц но запросу, то менеджер памяти для передачи страниц между реальной памятью и вспомогательной тоже должен вызывать супервизор ввода- вывода. Все уровни системы используют предоставляемые Уровнем О функции планирования процессов и управления ресурсами. У иерархической структуры много преимуществ. Системные программы на каждом уровне могут использовать относительно простые функции н интерфейсы, предоставляемые более низкими уровнями. Программисту пет необходимости вникать в то, как они в действительности реализуются. Операционная система может быть создана и отлажена но уровням, начиная с нулевого. Это сильно снижает сложность каждой части системы и намного упрощает реализацию заданий и их отладку. На рис. 6.356 показано типичное расположение функций, однако различные системы существенно отличаются друг от друга. Рассмотрим, например, программы обработки прерывания. Во многих системах обработчики прерываний первого уровня (FLIH - First-Level Interrupt Handlers) размещены в ядре (Уровень О). После начальной обработки прерываний FLIH может передать управление программе более высокого уровня; это исключение из правил, согласно которым внешние вызовы недопустимы. Так, например, FLIH в случае прерывания и отсутствию страницы может сохранить информацию о состоянии, разрешить другие прерывания, а затем передать управление программе Уровня 2 (см. рис. 6.25в). В некоторых операционных системах иерархическое распределение функций в особых случаях сделано более сложным. Информацию об этом и о других примерах иерархических структур можно найти в Лорин [1981] и Питерсон [1983]. Иерархические системы различаются также правилами передачи управления с одного уровня на другой. В строгой иерархии каждый уровень может обращаться только к уровню, находящемуся непосредственно под ним. Так, Уровень 3 может связываться только с Уровнем 2. Если программе управления файлами в нашем примере необходимо вызвать супервизор ввода-вывода, то этот запрос будет передан с Уровня 2 на Уровень 1. Преимущество этого подхода заключается в простоте использования: каждый уровень взаимодействует только с одним интерфейсом. Однако такое ограничение может привести к потере эффективности, так как возрастает число вызовов, которые нужно сделать для достижения внутреннего уровня. В прозрачной иерархии (transparent hierarchy) каждый уровень может связываться непосредственно с интерфейсом любого более низкого уровня. Так, например, программа пользователя может обращаться к программам управления файлами Уровня 3 или непосредственно вызывать функции супервизора ввода-вывода Уровня 1. Дальнейшее обсуждение иерархических структур операционных систем можно найти в Питерсон [1983], Лорин [1981] и Мэдник [1974]. 6.4.2. Виртуальные машины Чтобы перед пользователями создать иллюзию того, что они работают на автономных виртуальных машинах, концепцию иерархической структуры, рассмотренную в предыдущем разделе, можно расширить. Подход, связанный с применением виртуальных машин, делает возможной одновременную работу различных операционных систем на одной и той же реальной машине. Таким образом, виртуальные машины мы можем рассматривать как перенесение принципов мультипрограммирования на самый 11ижпин *'ревень операционной системы. Рассмотрим, например, рис. 6.36. Операционная система ОС1 мультипрограммная, поддерживающая одновременную работу трех пользователей; операционная система ОС2 - однопрограммная; ОСЗ - операционная система, которая в этот момент тестируется. Есть также программа пользователя (Пользователь5), предназначенная для работы в режиме супервизора в качестве автономной программы, не находящейся под управлением операционной системы. Тестовый пользовател ь Пользователь4 Пользователь3 Пользователь2 Пользователь1 Операционная система ОС1 Операционная система ОС2 Операционная система ОС3 Пользователь 5 (автономная программа) Монитор виртуальной машины (МВМ) Реальная машина Рис. 6.36. Операционная система с виртуальными машинами и многими пользователями. Все три операционные системы плюс автономный пользователь фактически работают на одной реальной машине. Однако они имеют дело не с ней, а с монитором виртуальной машины (MEM), благодаря которому у каждого пользователя создается впечатление, что он работает на автономной машине. Таким образом, в то время как обычные пользователи обслуживаются традиционным способом, имеется возможность отлаживать новые операционные системы, а также разрешить пользователям в особых случаях работать в режиме супервизора '). Если тестируемая система или автономный пользователь разрушит систему, то это отразится лишь на их виртуальных машинах. Другие пользователи реальной машины смогут спокойно продолжать свою работу. На рис. 6.37 показано, каким образом создается подобная иллюзия. Программы самого нижнего уровня операционной системы имеют дело не с реальной машиной, а с монитором I) Автор идеализирует положение дел. В режиме виртуальных машин можно отлаживать только те части операционных систем, которые не затрагивают аппаратных управляющих регистров и других средств аппаратуры, используемых монитором ОС. Прим, ред. виртуальной. МВМ, совершенно невидимый ни операционной системе, ни программе пользователя, предоставляет те же ресурсы, обслуживание и функции, что и лежащая в основе реальная машина. Интерфейс пользователя Ядро МВМ Реальная машина Виртуальная машина. Рис.6.37. Виртуальная машина как расширение понятия иерархической структуры. Каждый непосредственный пользователь виртуальной, например ОС1 или Пользователь5 на рис. 6.36, факт работает не в режиме супервизора, а в пользовательском режиме. Когда такой пользователь пытается выполнить привилегированную команду, такую как 510, STI или LPS, программное прерывание. По нему управление передается монитору виртуальной машины. MВM имитирует (с учетом реальности машины) выполнение требуемой команды, а возвращает управление пользователю. Монитор виртуальной машины активизируется и по прерыванию на реальной машине. Он определяет, какая из виртуальных машин должна быть задействована, и производит соответствующие изменения ее состояния. Практически монитор виртуальной машины представляет собой завершенную, хотя и простую, операционную систему реальной машины. Другие операционные системы и пользователи виртуальной машины являются «пользователями» реальной операционной системы (МВМ). Таким образом, монитор виртуальной машины призван осуществлять все самые существенные, рассмотренные нами машинно-зависимые функциям MВM сохраняет для каждой машины информацию о состояния и распределяет реальный ЦП между различными виртуальными машинами; это не что иное, как выполнение функции планирования процессов, рассмотренной в разд. 6.2. Используя методы аналогичные рассмотренным выше, MВM выделяет каждой виртуальной машине самостоятельную виртуальную память и не сколько виртуальных каналов ввода-вывода. Наиболее очевидные преимущества виртуальных машин - это гибкость и удобство. Чтобы удовлетворить нужды различных пользователей, одновременно могут работать разные операционные системы. В то время как производится тестирование операционных систем или программ автономных пользователей, машина по-прежнему остается доступной и для обычных пользователей. При работе с виртуальными машинами можно достичь и более высокой степени защиты, поскольку ни одна виртуальная машина не имеет доступа к ресурсам другой. Недостатком, безусловно, является значительная сложность системы, моделирующей работу каждой виртуальной машины. Например, если операционная система, функционирующая на виртуальной машине, сама использует средства виртуальной памяти, то может понадобиться наличие двух раздельных уровней трансляции динамических адресов. Эффективность операционной системы виртуальной машины в основном зависит от того, сколько операций должно моделироваться MEM, а сколько может быть выполнено непосредственно на реальной машине. Дальнейшее обсуждение операционных систем виртуальных машин можно найти в Дейтел [1984] и Питерсон [1983]. 6.4.3. Мультипроцессорные системы До сих пор при обсуждении операционных систем мы имели дело с машинами, в состав которых входил один центральный процессор (ЦП). В этом разделе будут рассмотрены некоторые возможные пути проектирования мультипроцессорных операционных систем. ЦП ЦП ОС ОС Память Память Файлы Файлы Файлы ЦП ОС Память Рис. 6.38. Мультипроцессорные системы со слабо связанными процессорами. Операционная система мультипроцессорной машины должна выполнять функции, аналогичные тем, что мы рассматривали для однопроцессорных систем. Конечно, некоторые из них могут нуждаться в изменениях. Например, планировщик процессов может выделить пользовательским заданиям более одного ЦП, поэтому в активном состоянии может находиться сразу несколько процессов. Существует также ряд менее очевидных вопросов, большинство из которых связано с организацией системы в целом. На рис. 6.38 показана простейшая форма мультипроцессорной организации. Каждый процессор имеет свою собственную память, устройства ввода-вывода и другие ресурсы. Вдобавок на каждом процессоре функционирует его собственная операционная система. Процессоры соединяются линиями связи. Посылка по ним запроса является единственным способом получения доступа одного процессора к ресурсам другого. Подобная организация, часто называемая мультипроцессорной системой со слабо связанными процессорами, очень похожа на сеть из однопроцессорных систем. Такие системы часто используются, если среди различных процессоров существует специализация функции. Например, некоторые системы разделения времени для осуществления всех деталей управления связью с терминалами пользователей имеют периферийный процессор. Главный процессор выполняет всю текущую вычислительную работу, ОС ЦП ЦП ОС Память ЦП ЦП Файлы Память ЦП Файлы ЦП а б Рис. 6.39. Мультипроцессорные системы с сильно связанными процессорами с обработкой: а - типа главный-подчиненный и б - симметричной. связываясь с периферийным процессором всякий раз, когда требуется произвести обмен с терминалами. Операционная система, работающая на каждом процессоре мультипроцессорной системы со слабо связанными процессорами, очень похожа на те, что мы рассматривали при обсуждении однопроцессорных систем. Единственной действительно новой функцией является управление сообщениями, посылаемыми по линиям связи. Мультипроцессорные системы со слабо связанными процессорами относительно просты, потому что каждый ресурс выделяется kako*ly-нибудь одному процессору. С другой стороны, в некоторых мультипроцессорных системах допускается прямое распределение ресурсов между процессорами. Такая организация, называемая мультипроцессорной системой с сильно связанными процессорами, отчасти усложняет задачи операционной системы. На рис. 6.39 демонстрируются две разновидности мультипроцессорной системы с сильно связанными процессорами. На рис. 6.39а показана система, реализующая принцип главный - подчиненный. Это означает, что все управление ресурсами и другими функциями операционной системы осуществляется одним главным процессором. На него полностью возложено управление работой всех подчиненных процессоров, выполняющих задания пользователей. Процессоры взаимодействуют либо непосредственно через линии связи, либо через рабочие области совместно используемой памяти. В мультипроцессорных системах, реализованных по принципу главный - подчиненный, процессоры могут совместно использовать такие ресурсы, как память и файлы данных. Однако программы и структуры данных, составляющих саму операционную систему, не разделяются; они используются только главным процессором. Таким образом, такой тип операционной системы также несколько похож на рассмотренные нами однопроцессорные системы. Наиболее важной проблемой в мультипроцессорных системах типа главныйподчиненный является несбалансированность использования ресурсов. Например, главный процессор может быть перегружен запросами от служб операционной системы, и это станет причиной длительных простоев подчиненных процессоров. Вдобавок любая неисправность в аппаратуре главного процессора вызывает останов всей системы. Всего этого можно избежать, разрешив любому процессору выполнять любую функцию, которая будет затребована операционной системой или программой пользователя. Этот подход, называемый симметричной обработкой (иногда используется термин «распределенная ОС» .- Ред.), демонстрируется на рис. 6.396. С его помощью ликвидируются потенциально слабые места в системе типа главный - подчиненный, так как все процессоры имеют возможность выполнять один и тот же набор функций. Вдобавок неисправность одного процессора не обязательно вызовет выход из строя всей системы. Остальные процессоры могут продолжить выполнение всех необходимых функций. В симметричных мультипроцессорных системах различные части операционной системы могут выполняться одновременно разными процессорами. Из-за этого разработка подобных систем может оказаться значительно сложнее и труднее по сравнению с рассмотренными выше. Например, все процессоры должны иметь доступ ко всем структурам данных, используемым операционной системой. Однако процессоры имеют дело с этими структурами данных независимо друг от друга, и, если два процессора одновременно пытаются изменить одну и ту же структуру, могут возникнуть осложнения (см. разд. 6.3.3). Симметричные мультипроцессорные системы должны иметь средства, с помощью которых осуществляется управление доступом к структурам данных и к критическим таблицам операционной системы. Для решения этой задачи не достаточно операций запроса и отказа, рассмотренных в разд. 6.3.3, так как два различных процессора могут выполнить операции запроса одновременно. Поэтому обычно требуется, чтобы аппаратура обладала особыми свойствами, позволяющими одному процессору захватывать управление критическим ресурсом, блокируя на один шаг все другие процессоры. Обсуждение подобных средств можно найти в Питерсон [1983] и Лорин [1981]. Дальнейшую информацию по операционным системам для мультипроцессоров можно найти в Дейтел [1984], Лорин [1981] и Мэдник [1974]. 6.5. Примеры реализаций В этом разделе дается описание нескольких реально существующих операционных систем, специально выбранных для того, чтобы продемонстрировать разнообразие в разработке и применении программного обеспечения такого рода. Как и в предыдущих примерах, в наши задачи не входит дать полное и подробное описание каждой системы. Вместо этого мы выделим некоторые присущие им наиболее интересные или часто встречающиеся свойства, а для читателей, желающих получить большую информацию, дадим ссылки на соответствующую литературу. Первые два примера - операционные системы, не привязанные к какой-нибудь одной машине. В разд. 6.5.1 рассматривается небольшая переносимая однопользовательская система UCSD Pascal, функционирующая на мини-ЭВМ. В разд. 6.5.2 описывается UNIX, более сложная операционная система, которая также может быть реализована и на разных ЭВМ. Другие примеры - системы, разработанные для определенных семейств ЭВМ. Разд. 6.5.3 посвящен NOSTW, функционирующей на ЭВМ серии CDC CYBER; это пример мультипроцессорной операционной системы. В разд. 6.5.4 рассматривается система VAX/VMSTW, в которой используются некоторые интересные методы управления виртуальной памятью. В разд. 6.5.5 рассматривается VM/370 фирмы IBM, операционная система, на базе которой могут быть реализованы многочисленные виртуальные машины. 6.5.1. Система UCSD Pascal Система UCSD Pascal-это операционная система для мини-ЭВМ. Написана она почти целиком на языке Паскаль и функционирует на псевдо-машине, или Р-машине (разд. 5.4.3 и 5.5.2). Таким образом, она может работать на любой ЭВМ, где реализован соответствующий интерпретатор. Сама по себе система UCSD Pascal относительно проста. Поскольку она предназначена для поддержания работы одного пользователя на мини-ЭВМ, для ее реализации не требуется применения сложных средств. Одной из наиболее интересных частей системы UCSD Pascal является ее интерфейс с пользователями, имеющий древовидную структуру. На каждом уровне дерева системой выдается строка-подсказка (prompt line), часто называемая меню (menu). Она содержит список команд, которые может ввести пользователь. По каждой из них выбирается соответствующая ветвь, и пользователь переводится на более низкий уровень дерева. Затем выдается новая строка-подсказка, содержащая команды, которые можно ввести уже на этом уровне. На самом нижнем уровне система запрашивает у пользователя при необходимости дополнительную информацию и выполняет команду. Например, когда система загружена впервые, ею выдается строка-подсказка вида Команды: E(dit,R(un,F(ile,C(omp,L(ink,X(ecute,A(ssem,D(ebug. По команде Е пользователь переводится на один уровень ниже и попадает в Редактор (Editor), где выдается новая строка-подсказка. Она может иметь вид Редактор: A(djst,C(py,D(lete,F(ind,l(nsrt,J(mp,R(eplace, Q(uit,X(chng,Z(ap. (Реальные строки-подсказки в зависимости от версий системы могут немного различаться.) По команде I система запрашивает у пользователя вводимый текст. По команде Q пользователь выходит из Редактора и возвращается к корню дерева (начальной строке-подсказке.) На самом внешнем уровне имеющиеся команды обеспечивают простой доступ к наиболее важным функциям системы. Редактор позволяет пользователю ввести текст и отредактировать любой заданный текстовой или системный рабочий файл. В программе Файлер имеются команды для управления файлами на диске. Это, например, команды выдачи справочной информации, копирования и создания новых файлов. Компилятор, ассемблер и Редактор связей - это системные программы, выполняющие функции, аналогичные тем, что были рассмотрены ранее. По команде R(un внешнего уровня система начинает выполнение программы из текущего рабочего файла. По X(ecute система запрашивает имя файла и выполняет содержащуюся в нем программу. По D(ebug программа из текущего файла выполняется под управлением системного Отладчика, который вызывается в случае появления ошибки или по достижении контрольной точки, заданной пользователем. В системе имеется также ряд вспомогательных программ. К ним относится, например, такие, посредством которых производится настройка системы на конкретную аппаратную конфигурацию, создаются самозагружающиеся программы, выполняются операции форматирования диска, не предусмотренные в Фаилере. Вспомогательные программы запускаются на внешнем уровне при помощи команды X(ecute. Операционной системой UCSD Раsсаl программы пользователей обеспечиваются набором лишь основных сервисных функций. В соответствии с нуждами программ, написанных на Паскале, в системе реализовано динамическое распределение памяти; тем самым имеется возможность в случае надобности переназначить содержимое основной памяти. Поддержка параллельности работы минимальна. Большая часть активности системы, включая операции ввода-вывода, синхронизована. Это означает, например, что операция READ приводит к тому, что программе пользователя приходится ждать завершения физической операции ввода-вывода; в это время ни одно другое задание не может быть выполнено. Дальнейшую информацию но системе UCSD Pascal можно найти в SofTech Microsystems [1980 и 1983]. 6.5.2. UNIX Операционная система UNIX первоначально была разработана в Bell Laboratories для семейства ЭВМ DEC PDP-11. С тех пор она была реализована на многих других машинах, от больших ЭВМ до микропроцессоров. Сейчас существует несколько версий UNIX. Ниже будут рассмотрены некоторые присущие всем им наиболее интересные свойства системы. Интерфейс пользователя в системе UNIX реализован программой, называемой оболочкой (shell). Это интерпретатор командных строк, воспринимающий строки, вводимые пользователем, в качестве запросов на выполнение других программ. Сначала по каждой команде вызывается соответствующая программа. По ее окончании оболочка также завершает работу и ожидает от пользователя следующую команду. Или же оболочка может запросить команду сразу после запуска требуемой программы, не дожидаясь ее завершения. В функции оболочки входят также замена параметров, выполнение команд, основанных на сравнения символьных строк, а также передача управления в пределах последовательности команд. Стандартная оболочка, рассмотренная выше, предназначена для того, чтобы предоставить пользователю доступ ко всем возможностям системы UNIX. Иногда бывает желательно вреду смотреть особый интерфейс пользователя, что можно легко достичь, применив нестандартный вариант оболочки. Имя реализующей ее программы может содержаться в пользовательском разделе файла паролей. Эта программа может интерпретировать команды таким образом, чтобы обеспечивался интерфейс, приспособленный к требованиям особого пользователя. Например, для пользователей системы секретарского редактирования при помощи файла паролей может быть задано, чтобы вместо стандартной оболочки была использована программа редактирования текстов. Таким образом, пользователи могут начать работу сразу же после входа в систему. Оболочка может помочь и в обеспечении защиты системы: пользователям индивидуальной командной оболочки может быть запрещено вызывать но предусмотренные для них программы системы UNIX. В системе UNIX процессам разрешено создавать другие процессы, независимо претендующие на системные ресурсы. Системный вызов fork приводит к разбиению процесса на два выполняющихся независимо: родительский процесс и порожденный процесс. В UNIX имеются средства, обеспечивающие синхронизацию процессов, при помощи которых родительский процесс может ожидать завершения работы порожденного процесса, а также получать информацию о его состоянии. Процессы в UNIX могут связываться непосредственно друг с другом через логические межпроцессные каналы, называемые транспортерами (pipes). Для передачи сообщений между процессами используются обыкновенные запросы на чтение и запись. Ни процессу, ни тем более простому файлу не требуется знать, что в этом принимают участие транспортеры. Синхронизация процессов и буферизация сообщений автоматически обрабатываются системой. Несколько процессов при помощи транспортеров могут связываться в линейную структуру и образовывать конвейер (pipeline), в котором выход одного процесса является входом другого. UNIX поддерживает иерархическую файловую систему. Для файлов пользователя имеются каталоги с древовидной структурой; система тоже имеет несколько каталогов для своих нужд. Один и тот же файл может фигурировать в нескольких различных каталогах, возможно, и под разными именами. Все такие файлы имеют одинаковый статус. Иными словами, файл не принадлежит какому-то одному определенному каталогу. Файлы могут быть и не включены ни в один каталог. Самое необычное свойство файловой системы UNIX - существование специальных файлов. Каждому устройству ввода-вывода, поддерживаемому UNIX, соответствует по крайней мере один такой файл. Специальные файлы читаются и пишутся так же, как и обычные, содержащиеся на диске, Однако запрос на чтение и запись специальных файлов вызывает запуск соответствующего устройства. Это делает файлы ввода-вывода и устройства ввода-вывода очень похожими. Например, устройства являются объектом того же самого механизма защиты, что и обычные файлы. Дальнейшую информацию и ссылки, касающиеся системы UNIX, можно найти в Баурн [1983], Дейтел [1984] и Ритчи [1978] . 6.5.3. NOS NOS - операционная система, предназначенная для использования на ЭВМ серии CDC CYBER и 6000. Это мультипроцессорные машины, имеющие один или два Центральных процессора (ЦП) и до 20 периферийных процессоров (ЦП). ЦП и ПП работают с общей центральной памятью; таким образом, NOS является примером мультипроцессорной системы с сильно связанными процессорами. Однако к каналам и устройствам ввода-вывода ЦП не имеет прямого доступа. Этот доступ обеспечивается периферийными процессорами. Используя специальные аппаратные команды, ПП могут запускать и останавливать ЦП. Под управлением NOS ЦП используется практически только для выполнения программ пользователей. ПП осуществляют ввод-вывод и большую часть функций операционной системы; однако определенная системная работа, если это может быть сделано с большей эффективностью, выполняется на ЦП. Каждому ПП в центральной памяти отведен блок из восьми слов, через который осуществляется связь с системой. Все управление машиной возложено на системный монитор, состоящий из программ MTR на ПП и CPUMTR на ЦП. МТ1? непрерывно сканирует запросы на обслуживание заданий пользователей. Подробно об этом будет рассказано ниже. CPUMTI? назначает периферийным процессорам задания, по одному на каждого. Когда задание завершается, ПП оповещает систему. Этого ждет CPUMTR, прежде чем назначить ПП следующее задание. Каждому заданию пользователя в центральной памяти отведена определенная область. Машины, на которых функционирует NOS, имеют аппаратно реализованные регистры перемещений, аналогичные тем, что были рассмотрены в разд. 6.2.4; поэтому существует возможность перемещать задания во время их выполнения. NOS получает от этого двойное преимущество. Во-первых, заданиям разрешается через запросы на обслуживание операционной системе увеличивать или уменьшать количество выделенной им центральной памяти. Чтобы удовлетворить эти запросы, NOS перераспределяет содержимое центральной памяти. Во-вторых, NOS может временно приостановить выполнение некоторого задания с тем, чтобы сделать центральную память доступной процессу с более высоким приоритетом. Это называется свертыванием задания. Когда же задание вновь будет развернуто и продолжит выполнение, то не обязательно, чтобы оно размещалось в той же самой области центральной памяти, что и раньше. Первые 64 слова области памяти, выделенной заданию, зарезервированы под связь с операционной системой. Чтобы осуществить запрос на обслуживание операционной системе, программа пользователя заносит его код в фиксированное место области связи. Программа MTR периферийного процессора непрерывно сканирует эти области, проверяя наличие кода запроса. Когда он обнаружен, MTR активизирует CPIJMTR для того, чтобы тот назначил ПП для запрашиваемого действия. Средства, используемые NOS для синхронизации выполнения программ и сервисных функций, несколько отличны от тех, что были рассмотрены в разд. 6.2.2 и 6.2.3. Программа пользователя, запрашивающая услуги операционной системы, может потребовать, чтобы ее временно перевели в состояние блокировки. Когда сервисная функция завершится, программа снова активизируется. В NOS это называется автоматическим перевызовом. Такое свойство часто используется, например, при ожидании завершения затребованной операции ввода-вывода. Если программа желает продолжать работу во время выполнения сервисной функции, то она запрашивает функцию без заказа автоматического перевызова. Если программа должна ждать завершения затребованного сервиса, то она делает специальный запрос на периодический перевызов, в результате чего она на некоторое время переводится в заблокированное состояние. По истечении этого времени программа снова активизируется и проверяет, может ли она продолжить работу или нет. Если сервисная функция еще не завершена, то программа вновь делает запрос на периодический перевызов. Аналогичный технический прием может быть использован для осуществления нескольких запросов на обслуживание, например одновременного выполнения несвязанных друг с другом операций ввода-вывода. Дополнительную информацию по NOS можно найти в CDC [1981 и 1983] 6.5.4. VAX/VMS VAX/VMS - это многоцелевая операционная система, предназначенная для использования на ЭВМ серии VAX. Под ее управлением можно работать в трех режимах: пакетном, разделения времени и реального времени. Неотъемлемой частью VAX является виртуальная память, а управление ею - одним из наиболее интересных свойств операционной системы VAX/VMS. Вся виртуальная память VAX/VMS состоит из 232 байт и делится на адресное пространство системы и адресное пространство процессов размером 232 байт каждый. Адресное пространство системы распределяется между всеми процессами, однако каждый процесс пользователя имеет и свое собственное адресное пространство. Оно в свою очередь делится на программную область и область управления. В первой из них находится программа, выполняемая процессом в текущий момент. Вторая отведена под системные стеки и прочую информацию, используемую операционной системой. Виртуальная память состоит из 512-байтовых страниц. Существует системная таблица страниц, которая содержит по одному входу для каждой страницы адресного пространства системы. По нему можно судить, загружен ли соответствующий ей страничный кадр или нет. Начальный адрес и длина таблицы задаются в двух специальных регистрах. Адресное пространство процессов описывается двумя таблицами страниц: одной для программной области, другой для области управления. Таблицы располагаются в адресном пространстве системы и сами могут участвовать в страничных операциях. Программы управления памятью также используют таблицу, содержащую информацию о состоянии всех страничных кадров в памяти, всех виртуальных страниц в системе и об их размещении. По страничному прерыванию акт11визируется системная программа, называемая pager. Она выбирает подходящий страничный кадр и переписывает требуемую виртуальную страницу в память. Для каждого процесса число страниц, одновременно находящихся в памяти, ограничено. Когда достигается их предельное число, pager выбирает страницу для выталкивания из памяти. Максимальное число страниц для каждого процесса устанавливается динамически и зависит от частоты происходящих в нем страничных прерываний. Измененные страницы, выталкиваемые из памяти, не переносятся во внешнюю память сразу. Вместо этого они заносятся в так называемый список измененных страниц. Чтобы минимизировать накладные расходы, связанные со страничным обменом, pager пытается записывать страницы кластерами (т.е. по несколько страниц за одну операцию записи). Если страница из этого списка запрашивается до того, как она в действительности записана, то для ее восстановления не требуется выполнения какого-либо физического ввода-вывода. Аналогично неизмененные страницы, выбранные для откачки, заносятся в список свободных страниц. Если входящая в него страница потребуется до того, как страничный кади был заново использован, она может быть перевызвана без выполнения физического ввода-вывода. Другая программа операционной системы, называемая swapper, используется для перемещения в память и из нее рабочих множеств целых процессов. Задача swapper держать в памяти активные процессы с наивысшим приоритетом, чтобы они могли участвовать в планировании. Таким образом, функции swapper аналогичны тем, что выполняет планировщик промежуточного уровня, упоминавшийся в разд. 6.3.2. Функция планирования процессов в VAX/VMS основана на 32 уровневой системе приоритетов. Нижние 16 уровней зарезервированы для обычных процессов; остальные для процессов реального времени. Каждый процесс в системе определен блоком управления процесса и своим заголовком. Блок управления процесса содержит кластеры флагов событий, которые могут быть использованы процессами для связи и координации их действий. Для синхронизации взаимодействующих процессов при использовании ими разделяемых ресурсов в VAX/VMS имеются средства блокировки. Существует шесть различных режимов блокировки, каждый из которых обеспечивает свой уровень разделения. Поддержка операций ввода-вывода осуществляется двумя уровнями: службой организации ведения записей (СОВЗ)системным сервисом ввода-вывода. Процедуры СОВЗ реализуют независимый от устройств файлово-структурированный доступ к устройствам ввода-вывода. Такие функции, как блокирование записей, могут быть выполнены либо СОВЗ, либо пользователем. Обслуживание, зависящее и не зависящее от устройств, обеспечивается системным сервисом ввода-вывода. Пользователи с достаточными привилегиями для задания своих файловых структур могут выполнять прямые операции ввода-вывода на персональных дисковых или ленточных устройствах. Оба типа поддержки ввода-вывода для создания очередей и выполнения физических операций ввода-вывода используют одни и те же системные программы более низкого уровня. Дальнейшую информацию по VAX/VMS можно найти в Дейтел [1984] и DEC [1982]. 6.5.5. VM/370 VM/370 - операционная система для IBM 370, поддерживающая работу многих виртуальных машин. Каждому пользователю кажется, что он имеет дело с самой System/370, включая и каналы ввода-вывода. Система VM/370 состоит из двух основных компонентов: управляющей программы (УП) и диалоговой мониторной системы (ДМС). УП является распорядителем ресурсов системы и выполняет те же функции, что и мониторы виртуальных машин, рассмотренные в разд. 6.4.2. В УП также имеются команды, помогающие в управлении и отладке операционной системы на виртуальной машине. Они позволяют выполнять такие действия, как чтение содержимого виртуальной памяти и установка контрольных точек. В сущности, это те же отладочные функции, которые системный программист может выполнить на пульте реального ЦП. На виртуальных машинах под управлением УП могут функционировать все операционные системы, обычно используемые на ЭВМ серии 370. Включая, конечно, и саму УЛ. Возможность выполнения УП на виртуальной машине под управлением другой УП делает возможной поддержку виртуального окружения УМ/370. Это особенно полезно при отладке частей УП или постановке новых версий УП в системе. Диалоговый монитор системы (ДМС) обеспечивает интерактивную поддержку единственного пользователя на одном терминале. Работа со многими терминалами осуществляется благодаря тому, что УП имеют возможность поддерживать функционирование многих виртуальных машин с ДМС на каждой из них. ДМС разработан специально для VM/370 и в своей работе прямо зависит от УЛ. В отличии от других операционных систем для IBM 370 функционировать самостоятельно на реальной машине он не может. Однако под управлением УП ДМС работает эффективнее, чем другие системы. Можно сказать, что ДМС предоставляет своим пользователям дружественный интерфейс. Он состоит скорее из набора команд, ориентированных на пользователя, чем из набора операторов для управления заданиями, как это имеет место в других операционных системах серии 370. 0 самой операционной системе помимо того, что необходимо для выполнения требуемой функции, пользователю нужно знать сравнительно мало. Когда имеешь дело с использованием операционных систем, поддерживающих работу виртуальных машин, приходится принимать во внимание производительность всей системы. Издержки, возникающие в результате функционирования монитора виртуальной машины, обычно уменьшают пропускную способность системы и увеличивают время ответа. В VM/370 имеются средства измерения производительности, с помощью которых во время выполнения УП можно получить информацию об использовании ресурсов, а также осуществить сбор результатов измерений для дальнейшего анализа. Существует также несколько способов увеличения производительности системы. Например, заданные страницы могут быть заблокированы в реальной памяти, а устройства и каналы - закреплены за определенной виртуальной машиной. Какая-либо виртуальная машина может функционировать в режиме виртуальный–как-реальный. В этом случае память виртуальной машины не участвует в осуществляемом под управлением УП размещении страниц, по запросу. Другие усовершенствования в программном обеспечении более специфичны и подразумевают изменения в операционных системах, на которых реализовано виртуальное окружение. Эти изменения позволяют операционной системе виртуальной машины узнать, что она работает под управлением VM/370, и связаться непосредственно с УЛ. Тем самым система не должна больше выполнять операции, которые при ее работе на виртуальной машине были бы излишни. Эти изменения, называемые в совокупности рукопожатием, повышают эффективность и увеличивают производительность виртуальной машины. В определенных моделях System/370 и соответствующих процессорах имеются специальные средства, предназначенные для использования с VM/370. Они позволяют аппаратно управлять некоторыми наиболее часто выполняемыми УП функциями. В результате достигается значительное увеличение производительности виртуальной машины. Дальнейшую информацию по VM/370 и соответствующие ссылки можно найти в Дейтел [1984] и Сирайт [1979].