Текст лекции. · Основные понятия и определения: операционная система (ОС), программное обеспечение (ПО), системное программное обеспечение

Ключевые вопросы

Лекция № 6. Загрузка программ. Часть 2

Продолжительность: 2 часа (90 мин.)

· Основные понятия и определения: операционная система (ОС), программное обеспечение (ПО), системное программное обеспечение.

· Относительная загрузка

· Базовая адресация

· Позиционно-независимый код

· Оверлеи

· Сборка программ

14.2.1 Относительная загрузка — до 15 мин.

Относительный способ загрузки состоит в том, что мы загружаем программу каждый раз с нового адреса. При этом мы должны настроить ее на новые адреса, а для этого нам надо вспомнить материал предыдущей главы и по­нять, что же именно в программе привязано к адресу загрузки.

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

Сложность здесь в том, что если абсолютные адресные поля можно най­ти анализом кодов команд (деассемблированием), то значение в адрес­ный регистр может загружаться задолго до собственно адресации, при­чем, как мы видели в примерах кода для процессора SPARC, формирование значения регистра может происходить и по частям. Без помощи программиста или компилятора (в этой главе мы не будем различать написанный на ассемблере или компилированный код, а того, кто генерировал код, будем называть программистом) решить вопрос о том, какая из команд загружает в регистр скалярное значение, а какая — будущий адрес или часть адреса, невозможно. Та же проблема возникает в случае, если мы используем в качестве указателя ячейку статически инициализованных данных (пример 14.1).

Пример 14.1. Примеры статически инициализованных указателей в С

int buf[20], *bufptr=buf;

char * message="No message defined yet\n";

void do_nothing hook(int);

void (*hook)(int)=do_nothing_hook;

Довольно легко построить и пример кода, в котором адресация происходит вообще без явного использования каких-либо регистров, во всяком случае, без загрузки в них значений (пример 14.2).

Пример 14.2. Реализация косвенного перехода по адресу dst_seg:dst_offs

push dst_seg; Это и будет ссылкой на абсолютный адрес

push dst offs

retf

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

Ассемблер при каждой ссылке на такой символ генерирует не только "заготовку" адреса в коде, но и запись в таблице перемещений (relocation table). Эта запись хранит место ссылки на такой символ в коде или данных. Если в ссылке используется только часть адреса, как в командах sethi НО, Ihi(addr) процессора SPARC, или move ax, segment addr процессора 8086, мы запоминаем и этот факт.

В качестве "заготовки" адреса обычно используется смешение адресуемого объекта от начала программы. При настройке программы на реальный адрес загрузки нам, таким образом, необходимо пройти по всем объектам, пере­численным в таблице перемещений, и переместить каждую из ссылок — сформировать из заготовки адрес.

Файл, содержащий таблицу перемещений, гораздо сложнее абсолютного загружаемого модуля и носит название относительного или перемешаемого загрузочного модуля. Именно такой формат имеют ехе-файлы в системе MS DOS (пример 14.2).Пример 14.2. Заголовок ЕХЕ-файла MS DOS. Цитируется по WINT.H из поставки

14.2.2 Базовая адресация — до 15 мин.

Впрочем, если уж мы полагаемся на содействие программиста, можно пойти в этом направлении дальше: мы объявляем один или несколько регистров про­цессора базовыми (несколько регистров могут использоваться для адресации различных сегментов программы, например, один — для кода, другой — для статических данных, третий — для стека) и договариваемся, что значения этих регистров программист принимает как данность и никогда сам не мо-1ифицирует, зато все адреса в программе он вычисляет на основе значений гих регистров.

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

Именно так происходит загрузка corn-файлов в системе MS DOS. Система выделяет свободную память, настраивает для программы базовые регистры DS и CS, которые почему-то называются сегментными, и передает управле­ние на стартовый адрес. Ничего больше делать не надо.

14.2.3 Позиционно-независимый код — до 15 мин.

За всеми этими разговорами мы чуть было не забыли о третьем способе формирования адреса в программе. Это относительная адресация, когда ад­рес получается сложением адресного поян команды и адреса самой этой ко­манды — значения счетчика команд. Код, в котором используется только такая адресация, можно загружать с любого адреса без всякой перенастрой­ки. Такой код называется позиционно-независимый {position-independent).

Позиционно-независимые программы очень удобны для загрузки, но, к со­жалению, при их написании следует соблюдать довольно жесткие ограниче­ния, накладываемые на используемые в программе методы адресации. На­пример, нельзя пользоваться статически и ни и и ал изо ванны ми переменными указательного типа, нельзя делать на ассемблере фокусы и т. д. Возникают сложности при сборке программы из нескольких модулей.

К тому же, на многих процессорах, например, на Intel 80S0/8085 или многих современных RISC-процессорах, описанная выше реализация позиционно-независимого кода вообще невозможна, так как эти процессоры не поддер­живают соответствующий режим адресации для данных. На процессорах гарвардской архитектуры адресовать данные относительно счетчика команд вообще невозможно — команды находятся в другом адресном пространстве.

Поэтому такой стиль программирования используют только в особых случа­ях. Например, многие вирусы для MS DOS и драйверы для RT-11 написаны именно таким образом.

Компиляторы современных систем семейства UNIX — GNU С или стандартный С-компилятор UNIX SVR4 имеют ключ -f PIC (Position-Independent Code}. Впрочем, код, порождаемый при использовании этого ключа, не является позиционно-независимым в указанном выше смысле: этот код все-таки содержит перемещаемые адресные ссылки. Задача состоит не в том, чтобы избавиться от таких ссылок полностью, а лишь в том, чтобы собрать все эти ссылки в од­ном месте и разместить их, по возможности, отдельно от кода. Какая от этого польза, мы поймем несколько позже, а сейчас обсудим технические приемы, используемые для решения этой задачи.

14.2.4 Оверлеи — до 15 мин.

Еще более интересный способ загрузки программы — это оверлейная загруз­ка (over-lay, лежащий сверху) или, как это называли в старой русскоязычной литературе, перекрытие. Смысл оверлея состоит в том, чтобы не загружать программу в память целиком, а разбить ее на несколько модулей и поме­щать их в память по мере необходимости. При этом на одни и те же адреса в различные моменты времени будут отображены разные модули. Отсюда и название потребность в таком способе загрузки появляется, если у нас виртуальное адресное пространство мало, например I Мбайт или даже всего 64 Кбайт (на некоторых машинах с RT-1I бывало и по 48 Кбайт, и многие полезные программы нормально работали!), а программа относительно велика. На со­временных 32-разрядных системах виртуальное адресное пространство обычно измеряется гигабайтами, и большинству программ этого хватает, проблемы с нехваткой можно решать совсем другими способами. Тем не менее, существуют различные системы, даже и 32-разрядные, в которых нет устройства управления памятью, и размер виртуальной памяти не может превышать объема микросхем ОЗУ, установленных на плате. Пример такой системы — упоминавшийся выше транспьютер. Важно подчеркнуть, что, несмотря на определенное сходство между задача­ми, решаемыми механизмом перекрытий и виртуальной адресацией, одно ни в коем случае не является разновидностью другого. При виртуальной ад­ресации мы решаем задачу отображения большого адресного пространства на ограниченную оперативную память. При использовании оверлея мы ре­шаем задачу отображения большого количества объектов в ограниченное адресное пространство. Основная проблема при оверлейной загрузке состоит в следующем: прежде чем ссылаться на оверлейный адрес, мы должны понять, какой из оверлей­ных модулей в данный момент там находится. Для ссылок на функции это просто: вместо точки входа функции мы вызываем некую процедуру, назы­ваемую менеджером перекрытий (overlay manager). Эта процедура знает, какой модуль куда загружен, и при необходимости "подкачивает" то, что загружено не было. Перед каждой ссылкой на оверлейные данные мы должны выполнять аналогичную процедуру, что намного увеличивает и замедляет Программу. Иногда такие действия возлагаются на программиста (Winl6, Mac OS до версии 10 — подробнее управление памятью в этих системах J., иногда — на компилятор (handle pointer в Zortech " C/C++ для MS DOS), но чаше всего с оверлейными данными вообще предпочитают не иметь дела. В таком случае оверлейным является только код. В старых учебниках по программированию и руководствах по операционным системам уделялось много внимания тому, как распределять процедуры между оверлейными модулями. Действительно, загрузка модуля с диска:представляет собой довольно длительный процесс, поэтому хотелось бы ми­нимизировать ее. Для этого нужно, чтобы каждый оверлейный модуль был как можно более самодостаточным. Если это невозможно, стараются выне­сти процедуры, на которые ссылаются из нескольких оверлеев, в отдельный модуль, называемый резидентной частью или резидентным ядром. Это мо­дуль, который всегда находится в памяти и не разделяет свои адреса ни с каким другим оверлеем. Естественно, оверлейный менеджер должен быть частью этого ядра. Каждый оверлейный модуль может быть как абсолютным, так и перемещае­мым. От этого несколько меняется устройство менеджера, по не более того, fin Архитектурах типа i80x86 можно делать оверлейные модули, каждый из ко-;торых адресуется относительно значения базового регистра cs и ссылается на данные, статически размещенные в памяти, относительно постоянного значения регистра ds. Такие модули можно загружать в память с любого адреса, может быть, даже вперемежку с данными. Именно так и ведут себя оверлейные менеджеры компиляторов Borland и Zortech.'3.7.

14.2.5 Сборка программ — до 15 мин.

В предыдущем разделе шла речь о типах исполняемых модулей,

но не гово­рилось ни слова о том, каким образом эти модули получаются. Вообще го­воря, способ создания загружаемого модуля различен в различных ОС, но в настоящее время во всех широко распространенных системах этот процесс выглядит примерно одинаково. Это связано, прежде всего, с тем, что эти системы используют одни и те же языки программирования и правила меж­модульного взаимодействия, в которых явно или неявно определяют логику раздельной компиляции и сборки.В большинстве современных языков программирования программа состоит из отдельных слабо связанных модулей. Как правило, каждому такому моду­лю соответствует отдельный файл исходного текста. Эти файлы независимо обрабатываются языковым процессором (компилятором), и для каждого из них генерируется отдельный файл, называемый объектным модулем. Затем запускается программа, называемая редактором связей, компоновщиком или линкером (linker ~ тот, кто связывает), которая формирует из заданных объ­ектных модулей цельную программу.

Объектный модуль отчасти похож по структуре на перемещаемый загрузоч­ный модуль. Дело в том, что сборку программы из нескольких модулей можно уподобить загрузке в память нескольких программ. При этом возни­кает та же задача перенастройки адресных ссылок, что и при загрузке отно­сительного загрузочного файла. Поэтому объектный модуль дол­жен в той или иной форме содержать таблицу перемещений. Можно, конечно, потребовать, чтобы весь модуль был позиционно-независимым, но это, как говорилось выше, накладывает очень жесткие ограничения на стиль программирования, а на многих процессорах (например Intel 8085) просто невозможно,

Кроме ссылок на собственные метки, объектный модуль имеет право ссы­латься на символы, определенные в других модулях. Типичный пример та­кой ссылки — обращение к функции, которая определена в другом файле исходного текста.

Для разрешения внешних ссылок мы должны создать две таблицы: в одной перечислены внешние объекты, па которые ссылается модуль, в другой — объекты, определенные внутри модуля, на которые можно ссылаться извне. Обычно с каждым таким объектом ассоциировано имя, называемое глобаль­ным символом. Как правило, это имя совпадает с именем соответствующей функции или переменной в исходном языке.

Для каждой ссылки на внешний символ мы должны уметь определить, яв­ляется эта ссылка абсолютной или относительной, либо это вообще должна быть разность или сумма двух или даже более адресов, и т. д. Для определе­ния объекта, с другой стороны, мы должны уметь указать, что это абсолютный [ияи перемешаемый символ, либо что он равен другому символу плюс за­данное смещение, и т. д.

Кроме того, в объектных файлах может содержаться отладочная информа­ция, формат которой может быть очень сложным. Следовательно, объект­ный файл представляет собой довольно сложную и рыхлую структуру. Раз­мер собранной программы может оказаться в два или три раза меньше суммы длин объектных модулей.

Типичный объектный модуль содержит следующие структуры данных: таблицу перемещений, т. е. таблицу ссылок на перемещаемые объекты внутри модуля; таблицу ссылок на внешние объекты. Иногда это называется таблицей или списком импорта.

Таблицу объектов, определенных в этом модуле, на которые можно ссылаться из других модулей. В некоторых случаях ее называют списком экс­
порта. Иногда таблицы экспорта и импорта объединяют и называют все
это таблицей глобальных символов. В этом случае для каждого символа
приходится указывать, определен он в данном модуле или нет, а если он­
определен, то как. Различную служебную информацию, такую, как имя модуля, программу, которая его создала (например, строка "gcc compiled").

Как правило, код и данные разбиты на именованные секции. В masm/iasm (MASM — Microsoft Assembler, Tasm — Turbo Assembler) такие секции назы­ваются сегментами, в DEC'obckhx и UNIX'oBbix ассемблерах — программны­ми секциями {psect). В готовой программе весь код или данные, описанный в разных модулях, но принадлежащий к одной секции, собирается вместе. Например, о системах семейства Unix программы, написанные на языке С, состоят из минимум трех программных секций:

.text — исполняемый код (современные компиляторы иногда помещают в эту секцию и данные, описанные как const);

.data — статически инициализированные данные;

.bss — неинициализированные данные.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: