\section{Сборка программ}

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

В большинстве современных языков программирования программа
состоит из отдельных слабо связанных модулей. Как правило, каждому
такому модулю соответствует отдельный файл исходного текста. Эти файлы
независимо обрабатываются языковым процессором (компилятором),
и для каждого из них генерируется отдельный файл, называемый
\term{объектным модулем}.
Затем запускается программа, называемая \term{редактором связей},
\term{компоновщиком} или \termint{линкером}{linker}{тот, кто связывает},
которая формирует из заданных объектных модулей цельную программу.

Интегрированные среды, типа Turbo Pascal или Borland C/C++
организованы таким же образом, только в них компилятор и редактор связей
по каким-то соображениям собраны вместе с текстовым редактором и
<<оболочкой>> -- модулями, которые перехватывают сообщения об
ошибках компиляции и т.д.

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

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

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

Одна из трудностей состоит в том, что значение символа может быть
определено двумя способами: как относительный адрес внутри модуля или
как абсолютное значение. Чаще всего используется первый способ.
Все адреса функций и переменных в обычных языках высокого уровня, таких
как C, C++ или Fortran, определяются именно таким образом. Второй способ
может использоваться в ассемблере:

\begin{example}\label{.org}
\small\begin{verbatim}

     .asect
; Дальнейший код входит в специальный абсолютный .psect
     .org     0100
     .globl   SYMBOL
     SYMBOL: .word ?
\end{verbatim}
\end{example}

Такая конструкция определит объект, который будет находиться по адресу
\verb"0100"
независимо от положения соответствующего объектного модуля внутри программы,
и даже от загрузочного адреса самой программы. Это может быть полезно при
работе с отображенными на память внешними устройствами, межпрограммном
взаимодействии и т.д.

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

В большинстве существующих систем мы все-таки можем ссылаться
на внешние символы при помощи относительной адресации. Например, мы можем
написать на ассемблере нечто вроде:
\begin{example}\label{rel_address}
\small\begin{verbatim}

         jmp SYMBOL - $
\end{verbatim}
\end{example}

\noindent где \verb"$" означает адрес текущей команды.
Если наш линкер собирает абсолютный загрузочный модуль, такая конструкция
породит вполне разумный код, даже если \verb"SYMBOL" представляет собой
абсолютный символ.
В случае же перемещаемого модуля такая конструкция может создать большие
сложности или вообще оказаться недопустимой. Действительно, в перемещаемом
модуле мы добавляем начальный загрузочный адрес ко всем ссылкам на абсолютные
адреса, а из такой ссылки мы должны его {\it вычесть}. Для этого мы должны,
как минимум, уметь задавать такую операцию в таблице перемещений.
Простая таблица на глазах превращается в целый командный язык.
Насколько авторам известно, формат \verb".exe"-файла \sysname{MS~DOS}
такого не позволяет.

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

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

Иногда эту таблицу объединяют с предыдущей и называют все это таблицей
глобальных символов. В этом случае для каждого символа приходится указывать,
определен он в данном модуле или нет, а если определен, то как.
\item Различную служебную информацию, такую, как имя модуля, программу,
которая его создала (например строка \verb'"gcc compiled"')
\item Отладочную информацию.
\item Собственно код и данные модуля.
Как правило, эта информация разбита на именованные секции.
В \progname{masm/tasm} такие секции называются \term{сегментами},
в \entname{DEC}'овских и \sysname{UNIX}'овых ассемблерах --
\termin{программными секциями}{psect}.
В готовой программе весь код или данные, принадлежащие к одной секции,
собираются вместе.
\end{itemize}

Крупные программы часто состоят из сотен и более отдельных модулей.
Кроме того, существуют различные пакеты подпрограмм, также состоящие из
большого количества модулей. Один из таких пакетов используется практически
в любой программе на языке высокого уровня -- это так называемая стандартная
библиотека. Для решения проблем, возникающих при поддержании порядка
в наборах из большого количества объектных модулей, еще на заре
вычислительной техники были придуманы \term{библиотеки объектных модулей}.

Библиотека, как правило, представляет последовательный файл,
состоящий из заголовка, за которым последовательно уложены объектные модули.
В заголовке содержится следующая информация:
\begin{itemize}
\itemСписок всех объектных модулей, со смещением каждого модуля от
начала библиотеки. Это нужно для того, чтобы можно было легко найти
требуемый модуль.
\itemСписок всех глобальных символов, определенных в каждом из модулей,
с указанием, в каком {\it именно} модуле он был определен.
\end{itemize}

Линкер обычно собирает в программу все объектные модули,
которые были ему заданы в командной строке, даже если на этот модуль
не было ни одной ссылки.
С библиотечными модулями он ведет себя несколько иначе.

    Встретив ссылку на глобальный символ, линкер ищет определение
этого символа во всех модулях, которые ему были заданы. Если там такого
символа нет, то линкер ищет этот символ в заголовке библиотеки.
Если его нет и там, линкер говорит: \verb"Не определен символ SYMBOL"
и завершает работу.
Некоторые линкеры, правда, могут продолжить работу и даже собрать
загружаемый модуль, но, как правило, таким модулем пользоваться нельзя,
так как в нем содержится ссылка на некорректный адрес.
Если же определение символа в библиотеке есть, линкер вытаскивает
соответствующий модуль и дальше работает так, будто этот модуль был задан
ему наравне с остальными объектными файлами. Этот процесс повторяется до
тех пор, пока не будут разрешены все глобальные ссылки, в том числе и те,
которые возникли в библиотечных модулях -- или пока не будет обнаружен
неопределенный символ. Благодаря такому алгоритму в программу включаются
только те модули из библиотеки, которые нужны.

Во многих современных системах с виртуальной памятью существует
понятие разделяемой библиотеки. С точки зрения линкера она отличается
от обычной тем, что он всегда обязан настраивать ее на одни и те же
виртуальные адреса, и не имеет права производить перенастройку самого
кода библиотеки. Кроме того, этот код хранится вовсе не в загружаемом
модуле, а в отдельном файле. Часто этот файл представляет собой загружаемый
модуль специальной структуры. Все программы, использующие такую библиотеку,
в действительности работают с одной копией ее кода, но каждая из них
создает свою копию ее данных. Это достаточно сильно экономит память
и дисковое пpостpанство, используемое для хpанения пpогpамм,
особенно в случае больших библиотек, таких, как X Window Toolkit.

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

Некоторые архитектуры процессоров поддерживают динамически
пересобираемые программы, у которых вся настройка модуля вынесена в
отдельную таблицу. В этом случае модуль может быть прилинкован
одновременно к нескольким программам, использовать одновременно
разные копии сегмента данных, и каждая инкарнация модуля при этом даже
не будет подозревать о существовании других. Примером такой архитектуры
является Pascal-система \machname{Lilith}, разработанная Н.Виртом,
и ее наследники \machname{Кронос/N9000}.


{\smallВ этих архитектурах каждый объектный модуль соответствует одному
модулю в смысле языка высокого уровня Oberon (или NIL - N9000 Instrumental
Language). Далее мы будем описывать архитектуру системы \machname{N9000},
поскольку авторы с ней лучше знакомы.

Модуль может иметь не более 256 процедур, не более 256 переменных
и ссылаться не более, чем на 256 других модулей. Код модуля является
позиционно-независимым. Данные модуля собраны в отдельный сегмент,
и для каждой инкарнации модуля, то есть для каждой программы, которая
этот модуль использует, создается своя копия сегмента данных. В начале
сегмента содержится таблица переменных. Строки этой таблицы содержат либо
значения -- для скалярных переменных, таких как целое число или указатель,
либо адреса в сегменте данных. Кроме того, сегмент данных содержит
ссылку на сегмент кода. Этот сегмент кода содержит в себе таблицу адресов
точек входа всех определенных в нем функций.

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

Все ссылки на объекты в данном модуле осуществляются через индекс
в соответствующей таблице. Ссылки на внешние модули имеют вид
\verb"индекс модуля: индекс объекта".

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

Все эти свойства реализованы в системе команд, поэтому накладные
расходы относительно невелики. Точнее, они невелики по сравнению с
\machname{Intel 80286}, но уже великоваты по сравнению с \machname{i386},
а по сравнению с современными RISC-процессорами или системами типа
транспьютера они становятся уже недопустимыми.

Видно, что в системе может существовать несколько программ,
обращающихся к одним и тем же модулям и использующих одну и ту же
копию кода модуля.
Проблем с абсолютной/относительной загрузкой вообще не возникает.

Операционная система \sysname{TC} для \machname{N9000} была%
\footnote{Авторы не уверены, существует ли в настоящее
время хотя бы одна работоспособная машина этого типа.}
основана на сборке программ в момент загрузки.
В системе имелась специальная команда \verb"load" -- <<загрузить все модули,
используемые программой, и разместить для них сегменты данных, но саму
программу не запускать>>. В памяти могло сидеть одновременно несколько
программ; при этом модули, используемые несколькими из них, загружались
в одном экземпляре. Это значительно ускоряло работу. Например, можно было
загрузить в память текстовый редактор, и запуск его занимал бы доли секунды,
вместо десятков секунд, которые нужны для загрузки с жесткого диска фирмы
\entname{ИЗОТ}.

Любопытно, что когда началась реализация системы программирования
на языке C для этой машины, по ряду причин было решено не связываться с
динамической сборкой, а собирать обычные перемещаемые загрузочные модули.
}

Вообще, среди современных ОС довольно много систем, использующих
тот или иной способ сборки при загрузке.
Таким образом устроен, например, \sysname{Novell Netware}. Таким же образом
организован ряд систем реального времени, таких как \sysname{OS-9} или
\sysname{VxWorks}.

Сборка при загрузке существенно замедляет процесс загрузки программы,
но упрощает, с одной стороны, разделение кода, а с другой стороны --
разработку программ. Действительно, из классического цикла внесения
изменения в программу: редактирование текста -- перекомпиляция --
пересборка -- перезагрузка (программы, не обязательно всей системы),
выпадает целая фаза. В случае большой программы это может быть длительная
фаза. В случае \sysname{Novell Netware} решающим оказывается первое
преимущество, в случае систем реального времени одинаково важны оба.

В системах \sysname{MS~Windows} и \sysname{OS/2} используется
способ загрузки, промежуточный между сборкой в момент загрузки и
сборкой заранее.
Загрузочный модуль в этих системах может быть полностью самодостаточным,
а может содержать ссылки на другие модули, называемые \termint{DLL}{Dynamically
Loadable Library}{динамически загружаемая библиотека}.
Самое хорошее в этой схеме то, что модуль, по собственному
желанию, может выбирать {\it различные} библиотеки. Единственное ограничение
состоит в том, что такие библиотеки обязаны быть совместимыми по вызовам.

Например, программа \progname{CorelDRAW!} может импортировать
и экспортировать изображения в различных видах, начиная от собственного
внутреннего формата \verb".CDR" или Windows Bitmap, и кончая
сильноупакованным форматом Jpeg или специализированными
форматами, вроде Targa-файлов.
Импорт и экспорт каждого формата выполняется отдельной DLL.

DLL на первый взгляд кажутся удобным средством разделения кода и
создания отдельно загружаемых программных модулей, но они имеют очень
серьезное ограничение.

Это ограничение проявляется, когда мы пытаемся создать в многопроцессной
среде DLL для работы с разделяемым ресурсом, например многооконную
графическую систему для работы нескольких задач с разделяемым терминалом.
Как будет показано в разделе \ref{reentr}, DLL, работающая с разделяемыми
данными, не сможет обрабатывать одновременно два вызова из разных
процессов. Это создаст много проблем, разрешение которых либо невозможно
в принципе, либо сопряжено с большими неудобствами.
Поэтому в многопроцессной среде нам придется выделить
оконную систему в отдельный процесс и оформлять обращения к ней не как
обычные вызовы процедур, а как посылку сообщений средствами межпроцессного
взаимодействия. Например, таким образом реализована сетевая оконная система
\progname{X~Windows}, реализованная на всех ОС семейства \sysname{Unix} и
многих других системах.

Средства межпроцессного взаимодействия будут подробнее обсуждаться в разделе
\ref{IPC}, а сейчас мы скажем только, что эти средства в современных ОС
по мощности не уступают прямому вызову процедуры.
Существует даже модель взаимодействия, которая так и называется --
\termint{RPC}{Remote Procedure Call}{Удаленный вызов процедуры}.
Само собой, такое использование этих средств сопровождается б\'{о}льшими
накладными расходами, чем прямой вызов; зато мы получаем намного большую
свободу -- например, мы можем обращаться к программе, исполняющейся на другом
процессоре или вообще на другой машине, или разделять общий ресурс (например,
общую базу данных) с другим процессом.

Поэтому то, что в \sysname{MS~Windows} делается при помощи DLL,
в большинстве современных ОС реализуется средствами межпроцессного
взаимодействия.

Кроме того, использование DLL сильно замедляет процесс загрузки программ
и несколько снижает общую производительность систем с виртуальной
памятью. Системы семейства \sysname{Unix} использующие монолитный
загрузочный имодуль и/или разделяемые библиотеки, заметно быстрее,
чем \sysname{OS/2} и \sysname{Windows~NT}, использующие DLL.
