Выяснив, что представляет собой программа, давайте рассмотрим процедуру ее загрузки в оперативную память компьютера (многие из обсуждаемых далее концепций, впрочем, в известной мере применимы и к прошивке программы в ПЗУ).
Для начала предположим, что программа была заранее собрана в некий единый самодостаточный объект, называемый загрузочным или загружаемым модулем. В ряде операционных систем программа собирается в момент загрузки из большого числа отдельных модулей, содержащих ссылки друг на друга, но об этом ниже.
Для того чтобы не путаться, давайте будем называть программой ту часть загрузочного модуля, которая содержит исполняемый код. Результат загрузки программы в память будем называть процессом или, если нам надо отличать загруженную программу от процесса ее исполнения, образом процесса, К образу процесса иногда причисляют не только код и данные процесса (подвергнутые преобразованию как в процессе загрузки, так и в процессе работы программы), но и системные структуры данных, связанные с этим процессом. В старой литературе процесс часто называют задачей.
В системах с виртуальной памятью каждому процессу обычно выделяется свое адресное пространство, поэтому мы иногда будем употреблять термин процесс и в этом смысле. Впрочем, во многих системах значительная часть адресных пространств разных процессов перекрывается - это используется для реализации разделяемого кода и данных.
В рамках одного процесса может исполняться один или несколько потоков или нитей управления. Это понятие будет подробнее разбираться в главе 8.
Некоторые системы предоставляют и более крупные структурные единицы, чем процесс. Например, в системах семейства Unix существуют группы процессов, которые используются для реализации логического объединения процессов в задания (job). Ряд систем имеют также понятие сессии - совокупности всех заданий, которые пользователь запустил в рамках одного сеанса работы. Впрочем, соответствующие концепции часто плохо определены, а их смысл сильно меняется от одной ОС к другой, поэтому мы практически не будем обсуждать эти понятия.
В более старых системах и в старой литературе называют результат загрузки задачей, а процессами - отдельные нити управления.
Однако в наиболее распространенных ныне ОС семейств Unix и Win32, принято задачу называть процессом, а процесс - нитью (tread).
Этой терминологии мы и будем придерживаться, кроме тех случаев, когда будем обсуждать примеры из жизни ОС, в которой принята иная терминология.
В системах семейства Unix новые процессы создаются системным вызовом fork. Этот вызов создает два процесса, образы которых в первый момент полностью идентичны, у них различается только значение, возвращенное вызовом fork. Типичная программа, использующая этот вызов, выглядит так, как представлено в примере 3.1.
При этом каждый из процессов имеет свою копию всех локальных и статических переменных. На процессорах со страничным диспетчером памяти физического копирования не происходит. Изначально оба процесса используют одни и те же страницы памяти, а дублируются только те из них, которые были изменены. На системах, не имеющих страничного или сегментного диспетчера памяти, fork требует копирования адресных пространств, что приводит к большим накладным расходам, да и просто не всегда возможно.
Если мы хотим запустить другую программу, то мы должны исполнить системный вызов из семейства ехес. Вызовы этого семейства различаются только способом передачи параметров. Все они прекращают исполнение текущего образа процесса и создают новый процесс с новым виртуальным адресным пространством, но с тем же идентификатором процесса. При этом у нового процесса будет тот же приоритет, будут открыты те же файлы (это часто используется), и он унаследует ряд других важных характеристик.
Несколько неожиданное, но тем не менее верное описание действия ехес - это замена образа процесса в рамках того же самого процесса.
Запуск другой программы в UNIX выглядит примерно так, как представлено в примере 3.2. Программа в примере 3.2 запускает командный интерпретатор /bin/sh, известный как Bourne shell, приказывает ему исполнить команду is -l и перенаправляет стандартный вывод этой команды в файл Is.log.
Техника программирования, основанная на fork/exec. несколько отличается от принятой во многих других современных системах, в том числе Win32, где при создании нового процесса мы сразу же указываем программу, которую он будет исполнять.
Пропустить примеры| Пример 3.1 Создание процесса в системах семейства UNIX. |
int pid; /* Идентификатор порожденного процесса */
switch (pid = fork ())
{
case 0: /* Порожденный процесс */
…..
break;
case -1: /* Ошибка */
perror("Cannot fork") ;
exit(1) ;
default: /* Родительский процесс */
…..
/* Здесь мы можем ссылаться на порожденный процесс, используя значение pid */
}
|
| Пример 3.2 Создание процесса и замена программы в системах семейства UNIX. |
int pid; /* Идентификатор порожденного процесса */
switch (pid = fork ())
{
case 0: /* Порожденный процесс */
dup2(1, open (“ls.log”, 0_WRONLY | 0_CREAT)) ;
/* Перенаправить открытый файл #1
* (stdout) в файл Is.log */
execl (“/bin/sh", "sh", "-с", "ls", "-1", 0) ;
/* Сюда мы попадаем только при ошибке! */
/* fail through */
case -1: /* Ошибка */
perror("Cannot fork or exec") ;
exit(1) ;
default: /* Родительский процесс */
…..
/* Здесь мы можем ссылаться на порожденный процесс, * используя значение pid */
}
|
Но вернемся к способам загрузки программ.
Первый, самый простой, вариант состоит в том, что мы всегда будем загружать программу с одного и того же адреса. Это возможно в следующих случаях.
Загрузочный файл, используемый при таком способе загрузки, называется абсолютным загрузочным модулем.
Начальное содержимое образа процесса формируется путем простого копирования модуля в память. В системе RT-11 такие файлы имеют расширение .sav от saved - сохраненный.
В системе UNIX на 32-разрядных машинах также используется абсолютная загрузка. Загружаемый файл формата a.out (современные версии Unix используют более сложный формат загружаемого модуля и более сложную схему загрузки, которая будет обсуждаться в разд. 5.4) начинается с заголовка (рис. 3.1), который содержит:
За заголовком следует содержимое областей text и data. Затем может следовать отладочная информация. Она нужна символьным отладчикам, но самой программой не используется.
При загрузке система выделяет процессу text_size байтов виртуальной памяти, доступной для чтения/исполнения, и копирует туда содержимое сегмента text. Затем отсчитывается data_size байтов памяти, доступной для чтения/ записи, и туда копируется содержимое сегмента data. Затем отсчитывается еще bss_size байтов памяти, доступной для чтения/записи, которые прописываются нулями.

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

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


Сложность здесь в том, что если абсолютные адресные поля можно найти анализом кодов команд (деассемблированием), то значение в адресный регистр может загружаться задолго до собственно адресации, причем, как мы видели в примерах кода для процессора SPARC, формирование значения регистра может происходить и по частям. Без помощи программиста или компилятора (в этой главе мы не будем различать написанный на ассемблере или компилированный код, а того, кто генерировал код, будем называть программистом) решить вопрос о том, какая из команд загружает в регистр скалярное значение, а какая - будущий адрес или часть адреса, невозможно. Та же проблема возникает в случае, если мы используем в качестве указателя ячейку статически инициализованных данных (пример 3.3).
Пропустить примеры| Пример 3.3 Примеры статически инициализованных указателей в С |
Int buf[20], *bufptr = buf; char * message = "No message defined yet\n"; void do_nothing_hook(int); void (*hook)(int)=do_nothing_hook; |
Довольно легко построить и пример кода, в котором адресация происходит вообще без явного использования каких-либо регистров, во всяком случае, без загрузки в них значений (пример 3.4).
Пропустить примеры| Пример 3.4 Реализация косвенного перехода по адресу dst_seg:dst_offs |
push dst_seg ; Это и будет ссылкой на абсолютный адрес push dst_offs retf |
На практике содействие программиста загрузчику состоит в том, что программист старается без необходимости не использовать в адресных полях и в качестве значений адресных регистров произвольные значения (необходимость в этом может возникать при адресации системных структур данных или внешних устройств, расположенных по фиксированным адресам). Вместо этого, программист применяет ассемблерные символы, соответствующие адресам.
Ассемблер при каждой ссылке на такой символ генерирует не только "заготовку" адреса в коде, но и запись в таблице перемещений (relocation table). Эта запись хранит место ссылки на такой символ в коде или данных. Если в ссылке используется только часть адреса, как в командах sethi %10, %hi(addr) процессора SPARC, или move ax, segment addr процессора 8086, мы запоминаем и этот факт.
В качестве "заготовки" адреса обычно используется смещение адресуемого объекта от начала программы. При настройке программы на реальный адрес загрузки нам, таким образом, необходимо пройти по всем объектам, перечисленным в таблице перемещений, и переместить каждую из ссылок - сформировать из заготовки адрес.
Файл, содержащий таблицу перемещений, гораздо сложнее абсолютного загружаемого модуля и носит название относительного или перемещаемого загрузочного модуля. Именно такой формат имеют ехе-файлы в системе MS DOS (пример 3.5).
Пропустить примеры| Пример 3.5. Заголовок ЕХЕ-файла MS DOS. Цитируется по WINT.H из поставки MS Visual C++ v6.0 (перевод комментариев автора) |
#define IMAGE_DOS_SIGMATURE Ox4D5A // MZ
typedef struct _IMAGE_DOS_HEADER { // Заголовок DOS .EXE
WORD e_magic; // Магическое число (сигнатура)
WORD e_cblp; // Длина последней страницы файла в байтах
WORD e_cp; // Количество страниц в файле
WORD e_crlc; // Количество перемещений
WORD e_cparhdr; // Размер заголовка в параграфах
WORD e_minalloc; // Минимальное количество дополнительных параграфов
WORD e_maxalloc; // Максимальное количество дополнительных параграфов
WORD e_ss; // Начальное (относительное) значение SS
WORD e_sp; // Начальное значение SP
WORD e_сsum; // Контрольная сумма
WORD e_ip; // Начальное значение IP
WORD e_cs; // Начальное (относительное) значение CS
WORD e_lfarlc; // Адрес таблицы перемещений в файле
WORD e_ovno; // Номер перекрытия
WORD e_res[4] ; // Зарезервировано
WORD e_oemid; // OEM идентификатор (для e_oeminfo)
WORD e_oeminfo; // Информация OEM; специфично для e_oemid
WORD e_res2[10] ; // Зарезервировано
LONG e_lfanew; // Адрес следующего заголовка в файле
} IMAGE_DOS_HEADER, *PIMAGE DOS HEADER;
|
Наиболее поучительна в этом отношении система RT-11, в которой существуют загружаемые модули обоих типов. Обычные программы имеют расширение sav, представляют собой абсолютные загружаемые модули и грузятся всегда с адреса 01000. Ниже этого магического адреса находятся векторы прерываний и стек программы. Сама операционная система вместе с драйверами размещается в верхних адресах памяти. Естественно, вы не можете загрузить одновременно два sav-файла.
Однако, если вам обязательно нужно исполнять одновременно две программы, вы можете собрать вторую из них в виде относительного модуля: файла с расширением rel. Такая программа будет загружаться в верхние адреса памяти, каждый раз разные, в зависимости от конфигурации ядра системы, количества загруженных драйверов устройств и других rel-модулей (рис. 3.5).

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

В этом случае для перемещения программы нам нужно только изменить значения базовых регистров, и программа даже не узнает, что загружена с другого адреса. Статически инициализованными указателями в этом случае пользоваться либо невозможно, либо необходимо всегда прибавлять к ним значения базовых регистров. Именно так происходит загрузка сот-файлов в системе MS DOS. Система выделяет свободную память, настраивает для программы базовые регистры DS и CS, которые почему-то называются сегментными, и передает управление на стартовый адрес. Ничего больше делать не надо.
За всеми этими разговорами мы чуть было не забыли о третьем способе формирования адреса в программе. Это относительная адресация, когда адрес получается сложением адресного поля команды и адреса самой этой команды - значения счетчика команд. Код, в котором используется только такая адресация, можно загружать с любого адреса без всякой перенастройки. Такой код называется позиционно-независамым (position-independent).
Позиционно-независимые программы очень удобны для загрузки, но, к сожалению, при их написании следует соблюдать довольно жесткие ограничения, накладываемые на используемые в программе методы адресации. Например, нельзя пользоваться статически инициализованными переменными указательного типа, нельзя делать на ассемблере фокусы, вроде того, который был приведен в примере 3.5, и т. д. Возникают сложности при сборке программы из нескольких модулей.
К тому же, на многих процессорах, например, на Intel 8080/8085 или многих современных RISC-процессорах, описанная выше реализация позиционно-независимого кода вообще невозможна, так как эти процессоры не поддерживают соответствующий режим адресации для данных. На процессорах гарвардской архитектуры адресовать данные относительно счетчика команд вообще невозможно - команды находятся в другом адресном пространстве.
Поэтому такой стиль программирования используют только в особых случаях. Например, многие вирусы для MS DOS и драйверы для RT-11 написаны именно таким образом.
В эпоху RT-11 хакеры писали драйверы. Сейчас они пишут вирусы. Еще любопытнее, что для некоторых персональных платформ, например, для Amiga, вирусов почти нет. Хакеры считают более интересным писать игры или демонстрационные программы для Amiga. Похоже, общение с IBM PC порождает у программиста какие-то агрессивные комплексы. Наблюдение это принадлежит не автору: см. [КомпьютерПресс 1993].;
Компиляторы современных систем семейства UNIX - GNU С или стандартный С-компилятор UNIX SVR4 имеют ключ -f pic (Position-Independent Code). Впрочем, код, порождаемый при использовании этого ключа, не является позиционно-независимым в указанном выше смысле: этот код все-таки содержит перемещаемые адресные ссылки. Задача состоит не в том, чтобы избавиться от таких ссылок полностью, а лишь в том, чтобы собрать все эти ссылки в одном месте и разместить их, по возможности, отдельно от кода. Какая от этого польза, мы поймем несколько позже, в разд. 5.4, а сейчас обсудим технические приемы, используемые для решения этой задачи. Код, генерируемый GNU С, использует базовую адресацию: в начале функции адрес точки ее входа помещается в один из регистров, и далее вся адресация других функций и данных осуществляется относительно этого регистра. На процессоре х86 используется регистр %ebx, а загрузка адреса осуществляется командами, вставляемыми в пролог каждой функции (пример 3.6), На процессорах, где разрешен прямой доступ к счетчику команд, соответствующий код выглядит проще, но принцип сохраняется: компилятор занимает один регистр и благодаря этому упрощает работу загрузчику.
Как мы видим в примере 3.7, на самом деле адресация происходит не относительно точки входа в функцию, а относительно некоторого объекта, называемого got или GLOBAL_OFFSET_TABLE. Счетчик команд используется для вычисления адреса этой таблицы, а не сам по себе. Подробнее мы разберемся с логикой работы этого кода (и заодно с тем, что означает еще один непонятный символ - plt) в разд. 5.4.Компилированный таким образом код предназначен в первую очередь для разделяемых библиотек формата ELF (Executable and Linking Format, формат исполняемых и собираемых [модулей], используемый большинством современных систем семейства Unix).
Пропустить примеры| Пример 3.6. Получение адреса точки входа в позиционно-независимую подпрограмму |
call L4 L4: popl %ebx |
| Пример 3.7. Позиционно-независимый код, порождаемый компилятором GNU С |
/* strerror.c lemx+gcc) -- Copyright (с) 1990-1996 by Eberhard Mattes */ #include |
Еще более интересный способ загрузки программы - это оверлейная загрузка (over-lay, лежащий сверху) или, как это называли в старой русскоязычной литературе, перекрытие. Смысл оверлея состоит в том, чтобы не загружать программу в память целиком, а разбить ее на несколько модулей и помещать их в память по мере необходимости. При этом на одни и те же адреса в различные моменты времени будут отображены разные модули (рис. 3.7). Отсюда и название.

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

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

Для разрешения внешних ссылок мы должны создать две таблицы: в одной перечислены внешние объекты, на которые ссылается модуль, в другой - объекты, определенные внутри модуля, на которые можно ссылаться извне. Обычно с каждым таким объектом ассоциировано имя, называемое глобальным символом. Как правило, это имя совпадает с именем соответствующей функции или переменной в исходном языке.
Для каждой ссылки на внешний символ мы должны уметь определить, является эта ссылка абсолютной или относительной, либо это вообще должна быть разность или сумма двух или даже более адресов, и т. д. Для определения объекта, с другой стороны, мы должны уметь указать, что это абсолютный или перемещаемый символ, либо что он равен другому символу плюс заданное смещение, и т. д.
Кроме того, в объектных файлах может содержаться отладочная информация, формат которой может быть очень сложным. Следовательно, объектный файл представляет собой довольно сложную и рыхлую структуру. Размер собранной программы может оказаться в два или три раза меньше суммы длин объектных модулей. Типичный объектный модуль содержит следующие структуры данных.
Как правило, код и данные разбиты на именованные секции. В masm/tasm (MASM - Microsoft Assembler, Tasm - Turbo Assembler) такие секции называются сегментами, в DЕСовских и UNIX'овскиx ассемблерах - программными секциями (psect). В готовой программе весь код или данные, описанный в разных модулях, но принадлежащий к одной секции, собирается вместе. Например, в системах семейства Unix программы, написанные на языке С, состоят из минимум трех программных секций:
Некоторые форматы объектных модулей, в частности ELF (Executable and Linking Format- формат исполняемых и собираемых [модулей], используемый современными системами семейства Unix), предоставляют особый тип глобального символа - слабый (weak) символ (пример 3.8). При сборке программы компоновщик не выдает сообщения об ошибке, если обнаруживает два различных определения такого символа, при условии, что одно из определений является слабым - таким образом, слабый символ может быть легко переопределен при необходимости. Особенно полезен этот тип при помещении объектного модуля в библиотеку.
Пропустить примеры| Пример 3.7. Структура данных объектного модуля ELF (цитируется по elf.h из поставки Linux 2.2.16, перевод комментариев автора). |
/* Заголовок файла ELF. Находится в начале каждого файла ELF. */
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Магическое число и другая информация */
Elf32_Half e_type; /* Тип объектного файла */
Elf32_Half e_machine; /* Архитектура */
E1f32_Word e_version; /* Версия объектного файла */
Elf32_Addr e_entry; /* Виртуальный адрес точки входа */
Elf32_0ff e_phoff; /* Смещение таблицы заголовка программы */
/* в файле */
Elf32_0ff e_shoff; /* Смещение таблицы заголовков секций в файле */
Elf32_Word e_flags; /* Процессорно-зависимые флаги */
Elf32_Half e_ehsize; /* Размер заголовка ELF в байтах */
Elf32_Half e_phentsize; /* Размер элемента */
/* таблицы заголовка программы */
Elf32_Half e_phnum; /* Счетчик элементов таблицы заголовка программы */
Elf32_Half e_shentsize; /* Размер элемента таблицы заголовков секций */
Elf32_Half e_shnum; /* Счетчик элементов таблицы заголовков программ */
Elf32_Half e_shstrndx; /* Индекс таблицы имен секций в таблице заголовков секций */
} Elf32_Ehdr;
/* Поля в массиве e_indent. Макросы EI_* суть индексы в этом массиве.
Макросы, следующие за каждым определением EI_*, суть значения,
которые соответствующий байт может принимать. */
#define EI_MAG0 0 /* Индекс нулевого байта сигнатуры1 */
#define ELFMAG0 0x7f /* Значение нулевого байта сигнатуры */
#define EI MAGI 1 /* Индекс первого байта сигнатуры */
#define ELFMAG1 'E' /* Значение первого байта сигнатуры */
#define EI_MAG2 2 /* Индекс второго байта сигнатуры */
#define ELFMAG2 'L' /* Значение второго байта сигнатуры */
#define EI MAG3 3 /* Индекс третьего байта сигнатуры */
#define ELFMAG3 'F' /* Значение третьего байта сигнатуры */
/* Объединение идентификационных байтов, для сравнения по словам */
#define ELFMAG "\177ELF"
#define SELFMAG 4
#define EI_CLASS 4 /* Индекс байта, указывающего класс файла */
#define ELFCLASSNONE 0 /* Не определено */
#define ELFCLASS32 1 /* 32-разрядные объекты */
#define ELFCLASS64 2 /* 64-разрядные объекты */
#define ELFCLASSNUM 3
#define EI_DATA 5 /* Индекс байта кодировки данных */
#define ELFDATANONE 0 /* Не определена кодировка данных */
#define ELFDATA2LSB 1 /* Двоичные дополнительные, младший байт первый */
#define ELFDATA2MSB 2 /* Двоичные дополнительные, старший байт первый */
#define ELFDATANUM 3
#define EI_VERSION 6 /* Индекс байта версии файла */
/* Значение должно быть EV_CURRENT */
#define EI_OSABI 7 /* идентификатор OS ABI */
#define ELFOSABI_SYSV 0 /* UNIX System V ABI */
#define ELFOSABI_HPUX 1 /* HP-UX */
#define ELFOSABI_ARM 97 /* ARM */
#define ELFOSABI_STANDALONE 255 /* Самостоятельное (встраиваемое) приложение * /
#define EI_ABIVERSION 8 /* версия ABI */
#define EI_PAD 9 /* Индекс байтов выравнивания */
/* Допустимые значения для е_type (тип объектного файла). */
#define ET_NONE 0 /* Не указан тип */
#define ET_REL I /* Перемещаемый файл */
#define ET_EXEC 2 /* Исполнимый файл */
#define ET_DYN 3 /* Разделяемый объектный файл */
#define ET_CORE 4 /* Образ задачи +/
#define ET_NUM 5 /* Количество определенных типов */
#define ET_LOPROC 0xff00 /* Специфичный для процессора */
#define ET_HIPROC 0xffff /* Специфичный для процессора */
/* Допустимые значения для e_machine (архитектура). */
#define EM_HONE 0 /* Не указана машина */
#define ЕМ_М32 1 /* AT&T WE 32100 */
#define EM_SPARC 2 /* SUN SPARC */
#define EM_386 3 /* Intel 80386 */
#define EM_68K 4 /* Motorola m68k family */
#define EM_88K 5 /* Motorola m88k family */
#define EM_486 6 /* Intel 80486 */
#define EM_860 7 /* Intel 80860 */
#define EM__MIPS 8 /* MIPS R3000 big-endian */
#define EM_S370 9 /* Amdahl */
#define EM_MIPS_RS4_BE 10 /* MIPS R4000 big-endian */
#define EM_RS6000 11 /* RS6000 */
#define EM_PARISC 15 /* HPPA */
#define EM_nCUBE 16 /* nCUBE */
#define EM_VPP500 17 /* Fujitsu VPP500 */
#define EM_SPARC32PLUS 18 /* Sun's "v8plus" */
#define EM_960 19 /* Intel 80960 */
#define EM_PPC 20 /* PowerPC */
#define EM_V800 36 /* NEC V800 series */
#define EM_FR20 37 /* Fujitsu FR20 */
#define EM_RH32 38 /* TRW RH32 */
#define EM_MMA 39 /* Fujitsu MMA */
#define EM_ARM 40 /* ARM */
#define EM_FAKE_ALPHA 41 /* Digital Alpha */
#define EM_SH 42 /* Hitachi SH */
#define EM_SPARCV9 43 /* SPARC v9 64-bit */
#define EM_TRICORE 44 /* Siemens Tricore */
#define EM__ARC 45 /* Argonaut RISC Core */
#define EM_H8_300 46 /* Hitachi H8/300 */
#define EM_H8_300H 47 /* Hitachi H8/300H */
#define EM_H8S 48 /* Hitachi H8S */
#define EM_H8_500 49 /* Hitachi H8/500 */
#define EM_IA_64 50 /* Intel Merced */
#define EM_MIPS__X 51 /* Stanford MIPS-X */
#define EM_COLDFIRE 52 /* Motorola Coldfire */
#define EM_68HC12 53 /* Motorola M68HC12 */
#define EM_NUM 54
/* Если необходимо выделить неофициальное значение для ЕМ_*, пожалуйста,
выделяйте большие случайные числа (0х8523, 0xa7f2, etc.), чтобы уменьшить
вероятность пересечения с официальными или не-GNU неофициальными значениями. */
#define EM_ALPHA 0х9026
/* Допустимые значения для e_version (версия). */
#define EV_NONE 0 /* Недопустимая версия ELF */
#define EV_CURRENT 1 /* Текущая версия */
#define EV_NUM 2
/* Элемент таблицы символов. */
typedef struct
{
Elf32_Word st_name; /* Имя символа (индекс в таблице строк) */
Elf32_Addr st_value; /* Значение символа */
Elf32_Word st_size; /* Размер символа */
unsigned char st_info; /* Тип и привязка символа */
unsigned char st_other; /* Значение не определено, 0 */
Elf32_Section st_shndx; /* Индекс секции */
} Elf32_Sym;
/* Секция syminfo, если присутствует, содержит дополнительную информацию о каждом динамическом символе. */
typedef struct
{
Elf32_Half si_boundto; /* Прямая привязка, символ, к которому привязан */
Elf32_Half si_flags; /* Флаги символа */
} Elf32_Syminfo;
/* Допустимые значения для si_boundto. */
#define SYMINFO_BT_SELF 0xffff /* Символ привязан к себе */
#define SYMINFO_BT__PARENT 0xfffe /* Символ привязан к родителю */
#define SYMINFO_ВТ_LOWRESERVE 0xff00 /* Начало зарезервированных записей */
/* Возможные битовые маски для si_flags. */
#define SYMINFO_FLG_DIRECT 0х0001 /* Прямо привязываемый символ */
#define SYMINFO_FLG_PASSTHRU 0х0002 /* Промежуточный символ для транслятора */
#define SYMINFO_FLG_COPY 0х0004 /* Символ предназначен для перемещения
копированием */
#define SYMINFO_FLG_LAZYLOAD 0х0008 /* Символ привязан к объекту
с отложенной загрузкой */
/* Значения версии Syminfo. */
#define SYMINFO_NONE 0
#define SYMINFO_CURRENT 1
#define SYMINFO_NUM 2
/* Как извлекать информацию из и включать ее в поле st-info */
#define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4)
#define ELF32_ST_TYPE(val) ((val) & 0xf)
#define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf)
/* Допустимые значения для подполя ST_BIND поля st_in?o (привязка символов). */
#define STB_LOCAL 0 /* Локальный символ */
#define STB_GLOBAL 1 /* Глобальный символ */
#define STB_WEAK 2 /* Слабый символ */
#define STB_NUM 3 /* Кол-во определенных типов. */
#define STB_LOOS 10 /* Начало ОС-зависимых значений */
#define STB_HIOS 12 /* Конец ОС-зависимых значений */
#define STB_LOPROC 13 /* Начало процессорно-зависимых значений */
#define STB_HIPROC 15 /* Конец процессорно-зависимых значений */
/* Допустимые значения для подполя ST_TYPE поля st_info (тип символа). */
#define STT_NOTYPE 0 /* Не указан */
#define STT_OBJECT 1 /* Символ - объект данных */
#define STT_FUNC 2 /* Символ - объект кода */
#define STT_SECTION 3 /* Символ связан с секцией */,
#define STT_FILE 4 /* Имя символа - имя файла */
#define STT_NUM 5 /* Кол-во определенных типов */
#define STT_LOOS 11 /* Начало ОС-зависимых значений */
#define STT_HIOS 12 /* Конец ОС-зависимых значений */
#define STT_LOPROC 13 /* Начало процессорно-зависимых значений */
#define STT_HIPROC 15 /* Конец процессорно-зависимых значений */
/* Индексы таблицы символов размещены в группах и цепочках кэша в секции
хэш-таблицы символов. Это специальное значение индекса указывает на конец
цепочки, и означает, что в этой группе более нет символов. */
#define STN_UNDEF 0 /* Конец таблицы. */
/* Элемент таблицы перемещений без добавочного значения (в секциях типа SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Адрес */
Elf32_Word r_info; /* Тип перемещения и индекс символа */
} Elf32_Rel;
/* /* Элемент таблицы перемещений с добавочным значением (в секциях типа SHT_RELA). */
typedef struct
{
Elf32_Addr r_offset; /* Адрес */
Elf32_Word r_info; /* Тип перемещения и индекс символа */
Elf32_Sword r_addend; /* Добавочное значение */
} Elf32_Rela;
/* Как извлекать информацию из и включать ее в поле r_info. */
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))
/* Типы перемещений для i386 (формулы взяты из
[docs.sun.com 816-0559-10] - авт.)
А - добавочное значение, используемое при вычислении значения перемещаемого поля.
В - базовый адрес, начиная с которого разделяемый объект загружается в память при
исполнении [программы]. Обычно разделяемый объект строится с базовым виртуальным
адресом, равным 0, но адрес при исполнении иной.
G - смещение записи в глобальной таблице смещений, где адрес перемещаемого
символа находится во время исполнения.
GOT - адрес глобальной таблицы смещений.
L - местоположение (смещение в секции или адрес) записи символа в процедурной таблице
связывания (PLT). PLT перенаправляет вызов функции по настоящему адресу. Редактор
связей создает начальную таблицу, а редактор связей времени исполнения модифицирует
записи во время исполнения.
Р - местоположение (смещение в секции или адрес) перемещаемого элемента памяти
(вычисляется с использованием г_offset).
S - значение символа, индекс которого находится в элементе таблицы перемещений. */
#define R_386_NONE 0 /* Не перемещать */
#define R_386_32 1 /* Прямое 32-разрядное - S + А */
#define R_386_PC32 2 /* 32-разрядное относительно PC - S + А - Р */
#define R_386_GOT32 3 /* 32-разрядный элемент GOT - G + A */
#define R_386_PLT32 4 /* 32-разрядный адрес PLT - L + А - Р */
#define R_386_COPY 5 /* Копировать символ при исполнении */
#define R_386_GLOB_DAT 6 /* Создать запись GOT - S*/
#define R_386_JMP_SLOT 7 /* Создать запись PLT - S */
#define R_386_RELATIVE 8 /* Сдвинуть относительно базы программы - В + А */
#define R_386_GOTOFF 9 /* 32-разрядное смещение GOT - S + А - GOT */
#define R_386_GOTPC 10 /* 32-разрядное смещение GOT относительно
PC - S + A - GOT */
/* Должна быть последняя запись. */
#define R_386_NUM 11
|
Крупные программы часто состоят из сотен и тысяч отдельных модулей. Кроме того, существуют различные пакеты подпрограмм, также состоящие из большого количества модулей. Один из таких пакетов используется практически в любой программе на языке высокого уровня - это так называемая стандартная библиотека. Для решения проблем, возникающих при поддержании порядка в наборах из большого количества объектных модулей, еще на заре вычислительной техники были придуманы библиотеки объектных модулей.
Библиотека, как правило, представляет собой последовательный файл, состоящий из заголовка, за которым последовательно располагаются объектные модули (рис. 3.11). В заголовке содержится следующая информация.

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

Как мы видели в предыдущем разделе, объектные модули и библиотеки содержат достаточно информации, чтобы собирать программу не только заранее, но и непосредственно в момент загрузки. Этот способ, безусловно, требует больших затрат процессорного времени, чем загрузка заранее собранного кода, но дает и некоторые преимущества.
Главное преимущество состоит в том, что, если мы загружаем несколько программ, использующих одну и ту же библиотеку, мы можем настроить их на работу с одной копией кода библиотеки, таким образом, сэкономив память. Разделение кода привлекательно и с функциональной точки зрения, поэтому сборка в момент загрузки находит широкое применение в самых разнообразных ситуациях.
Примером такой сборки является широко используемая в Windows всех версий и OS/2 технология DLL (на самом деле, DLL обеспечивают сборку не только в момент загрузки, но и после нее - возможность подключить дополнительный модуль к уже загруженной программе), которая будет более подробно обсуждаться далее. В качестве других примеров можно привести Novell NetWare, OS-9, VxWorks и т. д. Впрочем, если мы говорим о системах, предназначенных для использования во встроенных приложениях (той же VxWorks), вопрос о том, является ли сборка перед прошивкой в ПЗУ сборкой в момент загрузки или сборкой заранее, носит схоластический характер.
Некоторые системы команд поддерживают динамически пересобираемые программы, у которых вся настройка модуля вынесена в отдельную таблицу. В этом случае модуль может быть подключен одновременно к нескольким программам, использовать одновременно разные копии сегмента данных, и каждая используемая копия модуля при этом даже не будет подозревать о существовании других. Примером такой архитектуры является Pascal-система Lilith, разработанная Н. Виртом, и ее наследники KpoHoc/N9000.
В этих архитектурах каждый объектный модуль соответствует одному модулю в смысле языка высокого уровня Оbегоn (или NIL - N9000 Instrumental Language). Далее мы будем описывать архитектуру системы N9000, поскольку автор с ней лучше знаком.
Модуль может иметь не более 256 процедур, не более 256 переменных и ссылаться не более чем на 256 других модулей. Код модуля является позиционно-независимым. Данные модуля собраны в отдельный сегмент, и для каждой используемой копии модуля, т. е. для каждой программы, которая этот модуль использует, создается своя копия сегмента данных. В начале сегмента содержится таблица переменных. Строки этой таблицы содержат либо значения- для скалярных переменных, таких как целое число или указатель, либо адреса в сегменте данных. Кроме того, сегмент данных содержит ссылку на сегмент кода. Этот сегмент кода содержит в себе таблицу адресов точек входа всех определенных в нем функций (рис. 3.13).
Ссылки на все внешние модули собраны в таблицу, которая также содержится в сегменте данных. Внешний модуль определяется началом его сегмента данных. Все ссылки на объекты в данном модуле осуществляются через индекс в соответствующей таблице. Ссылки на внешние модули имеют вид индекс модуля: индекс объекта.
Сегмент данных не может содержать никаких статически инициализованных данных. Вся инициализация производится специальной процедурой, которая вызывается при каждом новом использовании модуля. Все эти свойства реализованы в системе команд, поэтому накладные расходы относительно невелики.

Точнее, они невелики по сравнению с Intel 80286, но уже великоваты по сравнению с i386, а по сравнению с современными RISC-процессорами или системами типа транспьютера они становятся недопустимыми. Впрочем, в разд. 5.4 мы увидим, как подобная структура используется и на "обычных" процессорах.
Видно, что в системе может существовать несколько программ, обращающихся к одним и тем же модулям и использующих одну и ту же копию кода модуля. Проблем с абсолютной/относительной загрузкой вообще не возникает. Операционная система ТС для N9000 была (автор не уверен, существует ли в настоящее время хотя бы одна работоспособная машина этой архитектуры) основана на сборке программ в момент загрузки. В системе имелась специальная команда load- "загрузить все модули, используемые программой, и разместить для них сегменты данных, но саму программу не запускать". В памяти могло находиться одновременно несколько программ; при этом модули, используемые несколькими из них, загружались в одном экземпляре. Это значительно ускоряло работу. Например, можно было загрузить в память текстовый редактор, и запуск его занимал бы доли секунды, вместо десятков секунд, которые нужны для загрузки с жесткого диска фирмы ИЗОТ.
Любопытно, что когда началась реализация системы программирования на языке С для этой машины, по ряду причин было решено не связываться с динамической сборкой, а собирать обычные перемещаемые загрузочные модули.
На практике, подобная архитектура более характерна для байт-кодов - прекомпилированных представлений программы, предназначенных для дальнейшей обработки интерпретатором - Java Virtual Machine, интерпретатором Smalltalk и т. д., чем для аппаратно реализованных систем команд. В таких системах команд порой используются и более экстравагантные решения.
Система команд AS/400 (сервер баз данных среднего уровня, производимый IBM) представляет собой машинно-независимый байт-код. При загрузке программы этот байт-код компилируется в бинарный код "реального" процессора, подобно тому, как это делается в большинстве современных реализации Java Virtual Machine. Точнее, наоборот, успех AS/400 был одним из важных факторов, которые подвигли фирму Sun на разработку Java, поэтому правильнее говорить, что современные JVM основаны на том же принципе компиляции при загрузке, что и AS/400.
Это решение обеспечивает невысокую стоимость аппаратуры (современные AS/400 основаны на микропроцессорах архитектуры Power PC. Их более высокая по сравнению с машинами, основанными на процессорах х86, цена обусловлена более производительными системной шиной и периферией), высокую производительность и возможность заменять архитектуру "реального" процессора без перекомпиляции пользовательского программного обеспечения. За время выпуска машин этой серии такая замена происходила дважды.
С другой стороны, отсутствие необходимости думать о том, как та или иная возможность может быть реализована аппаратно, позволила принимать весьма авангардистские решения, на которые не решался никто из разработчиков аппаратно реализованных CISC-архитектур, таких как VAX, Eclipse и даже апофеоза CISC, Intel 432.
AS/400 имеет единое адресное пространство в том смысле, что адресуемыми объектами являются не только сегменты кода и скалярных данных, но и объекты реляционной СУБД, такие, как таблицы, индексы, курсоры и т. д.
Фактически, адресации подлежит вся память системы как оперативная, так и дисковая. Адрес имеет два представления: его сегментная часть может хранить имя адресуемого объекта [в контексте этой главы это можно уподобить неразрешенной внешней ссылке) или собственно адрес, 64-битовое бинарное значение. Перед тем, как обратиться к объекту, адрес-имя надо преобразовать в бинарный формат, для чего существуют специальные команды [redbooks.ibm.com sg242222,pdf].
Механизм этого преобразования выполняет работу и файловой системы, и редактора связей, в том смысле, что и файловый доступ, и сборка программы содержат важную фазу преобразования имен (соответственно, имен файлов и имен внешних символов) в адреса, по которым можно осуществлять доступ.
Сборка при загрузке замедляет процесс загрузки программы (впрочем, для современных процессоров это замедление вряд ли имеет большое значение), но упрощает, с одной стороны, разделение кода, а с другой стороны - разработку программ. Действительно, из классического цикла внесения изменения в программу: редактирование текста - перекомпиляция - пересборка - перезагрузка (программы, не обязательно всей системы) выпадает целая фаза. В случае большой программы это может быть длительная фаза. В случае Novell Netware решающим оказывается первое преимущество (рис. 3.14), в случае систем реального времени одинаково важны оба.
В большинстве современных ОС, в действительности, сборка в момент загрузки происходит не из объектных модулей, а из предварительно собранных разделяемых библиотек. Такие библиотеки отличаются от обсуждавшихся в разд. 3.8, во-первых, тем, что из них невозможно извлечь отдельный модуль: все межмодульные ссылки внутри такой библиотеки разрешены, и ее необходимо всегда загружать как целое; и, во-вторых, тем, что список символов, экспортируемых такой библиотекой, не является объединением списков экспорта составляющих ее объектных модулей. При сборке такой библиотеки необходимо указать, какие из символов будут экспортироваться. Некоторые редакторы связей позволяют на этом этапе создавать дополнительные символы.

В Windows и OS/2 используется именно такой способ загрузки. Исполняемый модуль в этих системах содержит ссылки на другие модули, называемые DLL (Dynamically Loadable Library, динамически загружаемая библиотека). Фактически, каждый модуль в этих системах обязан содержать хотя бы одну ссылку на DLL, потому что интерфейс к системным вызовам в этих ОС также реализован в виде DLL.
DLL представляют собой библиотеки в том смысле, что обычно они собираются из нескольких объектных модулей. Но, в отличие от архивных библиотек, из DLL нельзя извлечь отдельный модуль, при присоединении библиотеки к программе она присоединяется и загружается целиком.
Главное достоинство DLL состоит в том, что модуль (как основной, так и библиотечный), по собственному желанию, может выбирать различные библиотеки, подгружая их уже после своей собственной загрузки. При этом нет даже строгого ограничения на совместимость этих библиотек по вызовам (две библиотеки совместимы по вызовам, если они имеют одинаковые точки входа с одинаковой семантикой): загрузчик предоставляет возможность просмотреть список глобальных символов, определенных в библиотеке, и получить указатель на каждый символ, обратившись к нему по имени (впрочем, количество и типы параметров или тип переменной, а тем более их семантику, загрузчик не сообщает - эту информацию надо получать из других источников, например из списка зарегистрированных в системе объектов СОМ).
Особенно удобна возможность вызывать любую функцию по имени при обращении к внешним модулям из интерпретируемых языков. В примере 3.9 для подключения внешних библиотек (в данном случае это стандартная библиотека RexxUtil и библиотека доступа к сетевым сервисам rxSock) применяются две процедуры: сначала RxFuncAdd с тремя параметрами: имя символа rexx, который будет использоваться для обращения к вызываемой функции, имя DLL и имя символа в этой DLL, а потом специальная функция, предоставляемая модулем (sysLoadFunc и sockLoadFunc соответственно), которая регистрирует в интерпретаторе REXX остальные функции модуля.
Пропустить примеры| Пример 3.9 Пример использования динамической библиотеки (здесь - REXX Socket) в интерпретируемом языке. |
/**********************************************************************************
ПРОСТОЙ HTTP клиент на REXX Dmitry Maximovich 2:5030/544.60 aka maxim@pabi.ru
***************************************************************************/
PARSE VALUE ARG(l) WITH Al A2
IF Al = '' THEN
DO
SAY 'USAGE: wwwget hostname[/path] [port]'
EXIT
END
ELSE
DO
PARSE VALUE Al WITH B1'/'B2
sServer = Bl
IF B2 = '’ THEN
DO
sRequest - "GET / HTTP 1.0'|I"0D0A0D0A"x
SAY 'Requesting /'
END
ELSE
DO
sRequest = 'GET /'||B2||' HTTP 1.0'||"0D0A0D0A"x
SAY 'Requesting /'||B2
END
END
IF A2 <> ‘’ THEN
DO
nPortNumber = A2
END
ELSE
DO
nPortNumber = 80
END
/* Загрузить REXX Socket Library если еще не загружена*/
IF RxFuncQuery('SockLoadFuncs') THEN
DO
rc = RxFuncAdd("SockLoadFuncs","rxSock","SockLoadFuncs")
rc = SockLoadFuncs()
END
IF RxFuncQueryC'SysLoadFuncs") THEN
DO
rc = RxFuncAdd( "SysLoadFuncs","RexxUtil","SysLoadFuncs")
rc = SysLoadFuncs()
END
rc=SockGetHostByName(sServer,"host.")
IF rc <> 1 THEN
DO
SAY 'CANNOT RESOLVE HOSTNAME TO ADDRESS; 'SServer
EXIT -1
END
SAY 'Trying server: 'host .name', address: 'host.addr', port: 'nPortNumber
socket = SockSocket('AF_INET','SOCK_STREAM',0)
IF socket < 0 THEN
DO
SAY 'UNABLE TO CREATE A SOCKET'
EXIT -1
END
address.family = 'AF_INET'
address.port = nPortNumber
address.addr = host.addr
rc = SockConnect(socket,'address.')
IF rc < 0 THEN
DO
SAY 'UNABLE TO CONNECT TO SERVER:'address.addr
SIGNAL DO
END
rc = SockSend(socket, sRequest)
SАY ' REQUEST: ***************************************************'
Resp = ''
DO FOREVER
rc = SockRecv(socket,"sReceive",256)
IF rc <= 0 THEN
LEAVE
Resp = Resp || sReceive
END
SAY ' '
/* CR -> CRLP */
nStart = 1
nStop = pos(X2C("0A"), Resp)
do while nStop > 0
SAY SUBSTR(Resp,nStart,nStop-nStart)
nStart = nStop + 1
nStop = pos(X2C("0A"), Resp, nStart)
end
DO:
rc = SockShutDown(socket,2)
rc = SockClose(socket)
|
При сборке DLL из нескольких объектных модулей программист должен предоставить DEF-файл (пример 3.10). В этом файле содержится перечисление символов, экспортируемых библиотекой (в отличие от обычных, "архивных" библиотек, набор этих символов не обязательно равен объединению наборов экспортных символов всех включенных в библиотеку объектов), а также некоторые другие параметры. Например, можно указать, что DLL имеет функции инициализации и терминации. Эти функции могут запускаться как при первой загрузке библиотеки (initglobal), так и при подключении библиотеки очередной программой (initinstance). Можно также управлять разделением сегмента данных DLL - применять общий сегмент данных для всех программ, использующих библиотеку, или создавать свою копию для каждой программы.
Пропустить примеры| Пример 3.10. DEF-файл из примеров кода VisualAge C++ V3.0 |
LIBRARY REXXUTIL INITINSTANCE LONGNAMES PROTMODE DESCRIPTION 'REXXUTIL Utilities - (c) Copyright IBM Corporation 1991' DATA MULTIPLE NONSHARED STACKSIZE 32768 EXPORTS SYSCLS = SysCIs @1 SYSCURPOS = SysCurPos @2 SYSCURSTATE = SysCurState @3 SYSDRIVEINFO = SysDriveInfo @4 SYSDRIVEMAP = SysDriveMap @5 SYSDROPFUNCS = SysDropFuncs @6 SYSFILEDELETE = SysFileDelete @7 SYSFILESEARCH = SysFileSearch @8 SYSFILETREE = SysFileTree @9 SYSGETMESSAGE = SysGetMessage @10 SYSINI - Sysini @11 SYSLOADFUNCS = SysLoadFuncs @12 SYSMKDIR =• SysMkDir @13 SYSOS2VER = SysOS2Ver @14 SYSRMDIR = SysRmDir @15 SYSSEARCHPATH = SysSearchPath @16 SYSSLEEP = SysSleep @17 SYSTEMPFILENAME = SysTempFileName @18 SYSTEXTSCREENREAD = SysTextScreenRead @19 SYSTEXTSCREENSIZE = SysTextScreenSize @20 SYSGETEA = SysGetEA @21 SYSPUTEA - SysPutEA @22 SYSWAITNAMEDPIPE = SysWaitNamedPipe @23 |
DLL являются удобным средством разделения кода и создания отдельно загружаемых программных модулей, но их использование сопряжено с определенной проблемой, которая будет подробнее объясняться в разд. 5.4. Забегая вперед, скажем, что концепция разделяемых DLL наиболее естественна в системах, где все задачи используют единое адресное пространство - но при этом ошибка в любой из программ может привести к порче данных или кода другой задачи. Стандартный же способ борьбы с этой проблемой - выделение каждому процессу своего адресного пространства - значительно усложняет разделение кода.
Другая проблема, обусловленная широким использованием разделяемого кода, состоит в слежении за версией этого кода. Действительно, представим себе жизненную ситуацию: в системе одновременно загружены тридцать программ, использующие библиотеку LIBC.DLL. При этом десять из них разрабатывались и тестировались с версией 1.0 этой библиотеки, пять - с версией 1.5 и пятнадцать - с версией 1.5а. Понятно, что рассчитывать на устойчивую работу всех тридцати программ можно только при условии, что все три версии библиотеки полностью совместимы снизу вверх не только по набору вызовов и их параметров, но и по точной семантике каждого из этих вызовов. Последнее требование иногда формулируют как bug-for-bug compatibility (корректно перевести это словосочетание можно так: полная совместимость не только по спецификациям, но и по отклонениям от них).
Казалось бы, исправление ошибок должно лишь улучшать работу программ, использующих исправленный код. На практике же бывают ситуации, когда код основной программы содержит собственные обходные пути, компенсирующие ошибки в библиотеке. Эти обходы могут быть как внесены сознательно (когда поставщик библиотеки исправит, еще неизвестно, а программа нужна сейчас), так и получиться сами собой (арифметический знак, перепутанный четное число раз и т. д.). В этих случаях исправление ошибки может привести к труднопредсказуемым последствиям. Нельзя также забывать и о возможности внесения новых ошибок при исправлении старых, поэтому при разработке и эксплуатации сложных программных систем, необходимо тщательно следить за тем, что именно и где изменилось, а не просто фиксировать ошибки.
Требование "совместимости с точностью до ошибок" - это лишь полемически заостренная формулировка требования контролируемости поведения кода. Из вышеприведенных соображений понятно, что нарушения такой контролируемости представляют собой проблему, которая, не будучи так или иначе разрешена, может серьезно усложнить работу администраторов системы и. приложений.
Катастрофические масштабы эта проблема принимает в системах семейства Windows, где принято помещать в дистрибутивы прикладных программ все потенциально разделяемые модули, которые этой программе могут потребоваться- среда исполнения компилятора и т.д. При этом каждое приложение считает своим долгом поместить свои разделяемые модули в C:\W1NDOWS\SYSTEM32 (в Windows NT/2000/XP это заодно приводит к тому, что установка самой безобидной утилиты требует администраторских привилегий). Средств же проследить за тем, кто, какую версию, чего, куда и зачем положил, практически не предоставляется.
В лучшем случае установочная программа спрашивает: "Тут вот у вас что-то уже лежит, перезаписать?". Стандартный деинсталлятор содержит список DLL, которые принадлежат данному приложению, и осознает тот факт, что эти же DLL используются кем-то еще, но не предоставляет (и, по-видимому, не пытается собрать) информации о том, кем именно они используются. Наличие реестра объектов СОМ не решает проблемы, потому что большая часть приносимого каждым приложением "разделяемого" кода (кавычки стоят потому, что значительная часть этого кода никому другому, кроме принесшего его приложения, не нужна) не является сервером СОМ.
В результате, когда, например, после установки MS Project 2000 перестает работать MS Office 2000 [MSkb RU270125], это никого не удивляет, а конфликты между приложениями различных разработчиков или разных "поколений" считаются неизбежными. Установить же в одной системе и использовать хотя бы попеременно две различные версии одного продукта просто невозможно - однако, когда каждая версия продукта использует собственный формат данных. а конверсия между ними неидеальна, это часто оказывается желательно.
Разработчики же и тестеры, которым надо обеспечить совместимость с различными версиями существующих приложений, при этом просто оказываются в безвыходной ситуации. Неслучайно поставщики VmWare (системы виртуальных машин для х86) как одно из главных достоинств своей системы рекламируют возможность держать несколько копий Windows одновременно загруженными на одной машине.
Привлекательный путь решения этой проблемы- давать каждому приложению возможность указывать, какие именно DLL ему нужны и где их искать, и позволять одновременно загружать одноименные DLL с разной семантикой - на самом деле вовсе не прост как с точки зрения реализации, так и с точки зрения управления системой. Системы с виртуальной памятью предлагают некоторые подходы к реализации этого пути, но это будет обсуждаться в разд. 5.4
При загрузке самой ОС возникает специфическая проблема: в пустой машине, скорее всего, нет программы, которая могла бы это сделать.
В системах, в которых программа находится в ПЗУ (или другой энергонезависимой памяти) этой проблемы не существует: при включении питания программа в памяти уже есть и сразу начинает исполняться. При включении питания или аппаратном сбросе процессор исполняет команду, находящуюся по определенному адресу, например, 0xFFFFFFFA. Если там находится ПЗУ, а в нем записана программа, она и начинает исполняться.
При разработке программ для встраиваемых приложений часто используются внутрисхемные имитаторы ПЗУ, доступные целевой системе как ПЗУ, а системе разработчика - как ОЗУ или специальное внешнее устройство.
Компьютеры общего назначения также не могут обойтись без ПЗУ. Программа, записанная в нем, называется загрузочным монитором. Стартовая точка этой программы должна находиться как раз по тому адресу, по которому процессор передает управление в момент включения питания. Эта программа производит первичную инициализацию процессора, тестирование памяти и обязательного периферийного оборудования, и, наконец, начинает загрузку системы. В компьютерах, совместимых с IBM PC, загрузочный монитор известен как BIOS.
На многих системах в ПЗУ бывает прошито нечто большее, чем первичный загрузчик. Это может быть целая контрольно-диагностическая система, называемая консольным монитором. Такая система есть на всех машинах линии PDP-11/VAX и на VME-системах, рассчитанных на OS-9 или VxWorks. Такой монитор позволяет вам просматривать содержимое памяти по заданному адресу, записывать туда данные, запускать какую-то область памяти как программу и многое другое. Он же позволяет выбирать устройство, с которого будет производиться дальнейшая загрузка. В PDP-11/VAX на таком мониторе можно даже писать программы, почти с таким же успехом, как на ассемблере. Нужно только уметь считать в уме в восьмеричной системе счисления.
На машинах фирмы Sun в качестве консольного монитора используется интерпретатор языка Forth. На ранних моделях IBM PC в ПЗУ был прошит интерпретатор BASIC. Именно поэтому клоны IBM PC имеют огромное количество плохо используемого адресного пространства выше сегмента 0хC000. Вы можете убедиться в том, что BASIC там должен быть, вызвав из программы прерывание 0х60. Вы получите на мониторе сообщение вроде: no rom basic. press any key то reboot. Вообще говоря, этот BASIC не является консольным монитором в строгом смысле этого слова, так как получает управление не перед загрузкой, а лишь после того, как загрузка со всех устройств завершилась неудачей.
После запуска консольного монитора и инициализации системы вы можете приказать системе начать собственно загрузку ОС. На IBM PC такое приказание отдается автоматически, и часто загрузка производится вовсе не с того устройства, с которого хотелось бы. На этом и основан жизненный цикл загрузочных вирусов.
Чтобы загрузочный монитор смог что бы то ни было загрузить, он должен уметь проинициализировать устройство, с которого предполагается загрузка, и считать с него загружаемый код. Поэтому загрузочный монитор обязан содержать модуль, способный управлять загрузочным устройством. Например, типичный BIOS PC-совместимого компьютера содержит модули управления гибким диском и жестким диском с интерфейсом Seagate 506 (в современных компьютерах это обычно интерфейс EIDE, отличающийся от Seagate 506 конструктивом, но программно совместимый с ним сверху вниз).
Кроме того, конструктивы многих систем допускают установку ПЗУ на платах контроллеров дополнительных устройств. Это ПЗУ должно содержать программный модуль, способный проинициализировать устройство и произвести загрузку с него (рис. 3.15).

Как правило, сервисы загрузочного монитора доступны загружаемой системе. Так, модуль управления дисками BIOS PC-совместимых компьютеров предоставляет функции считывания и записи отдельных секторов диска. Доступ к функциям ПЗУ позволяет значительно сократить код первичного загрузчика ОС, и, нередко, сделать его независимым от устройства
Проще всего происходит загрузка с различных последовательных устройств - лент, перфолент, магнитофонов, перфокарточных считывателей и т. д. Загрузочный монитор считывает и память все, что можно считать с заданного устройства и передает управление на начало той информации, которую прочитал.
В современных системах такая загрузка практически не используется. В них загрузка происходит с устройств с произвольным доступом, как правило - с дисков. При этом обычно в память считывается нулевой сектор нулевой дорожки диска. Содержимое этого сектора называют первичным загрузчиком. В IBM PC этот загрузчик называют загрузочным сектором, или boot-сектором.
Как правило, первичный загрузчик, пользуясь сервисами загрузочного монитора, ищет на диске начало файловой системы своей родной ОС, находит в этой файловой системе файл с определенным именем, считывает его в память и передает этому файлу управление. В простейшем случае такой файл и является ядром операционной системы. Размер первичного загрузчика ограничен чаще всего размером сектора на диске, т. е. 512 байтами. Если файловая система имеет сложную структуру, иногда первичному загрузчику приходится считывать вторичный, размер которого может быть намного больше. Из-за большего размера этот загрузчик намного умнее и в состоянии разобраться в структурах файловой системы. В некоторых случаях используются и третичные загрузчики.
Это последовательное исполнение втягивающих друг друга загрузчиков возрастающей сложности называется бутстрапом (bootstrap), что можно перевести как "втягивание [себя] за шнурки от ботинок".
Большую практическую роль играет еще один способ загрузки - загрузка по сети. Она происходит аналогично загрузке с диска: ПЗУ, установленное на сетевой карте, посылает в сеть пакет стандартного содержания, который содержит запрос к серверу удаленной загрузки. Этот сервер передает по сети вторичный загрузчик и т. д. Такая технология незаменима при загрузке бездисковых рабочих станций. Централизованное размещение загрузочных образов рабочих станций на сервере упрощает управление ими, защищает настройки ОС от случайных и злонамеренных модификаций и существенно удешевляет эксплуатацию больших парков настольных компьютеров, поэтому по сети нередко загружаются и машины, имеющие жесткий диск.
Проще всего происходит загрузка систем, ядро которых вместе со всеми дополнительными модулями (драйверами устройств, файловых систем и др.) собрано в единый загрузочный модуль. Например, в системах семейства Unix, ядро так и называется /unix (в FreeBSD - /vmunix, в Linux - /vmlinux, или, в случае упакованного ядра, /vmlinuz).
При переконфигурации системы, добавлении или удалении драйверов и других модулей необходима пересборка ядра, которая может производиться либо стандартным системным редактором связей, либо специальными утилитами генерации системы. Для такой пересборки в поставку системы должны входить либо исходные тексты (как у Linux и BSD), либо объектные модули ядра. Сборка ядра из объектных модулей на современных системах занимает не более нескольких минут. Полная перекомпиляция ядра из исходных текстов, конечно, продолжается существенно дольше.
На случай, если системный администратор ошибется и соберет неработоспособное ядро, вторичный загрузчик таких систем часто предоставляет возможность выбрать файл, который следует загрузить. Ядро таких систем обычно не использует никаких конфигурационных файлов - все настройки также задаются при генерации.
Большинство современных ОС используют более сложную форму загрузки, при которой дополнительные модули подгружаются уже после старта самого ядра. В терминах предыдущих разделов это называется "сборка в момент загрузки". Список модулей, которые необходимо загрузить, а также параметры настройки ядра, собраны в специальном файле или нескольких файлах. У DOS и OS/2 этот файл называется CONFIG.SYS, у Win32-систем - реестром (registry).
Сложность при таком способе загрузки состоит в том, что ядро, еще полностью не проинициализовавшись, уже должно быть способно работать с файловой системой, находить в ней файлы и считывать их в память.Особенно сложен этот способ тогда, когда драйверы загрузочного диска и загрузочной файловой системы сами являются подгружаемыми модулями. Обычно при этом ядро пользуется функциями работы с файловой системой, предоставляемыми вторичным (или третичным, в общем, последним по порядку) загрузчиком, до тех пор, пока не проинициализирует собственные модули. Вторичный загрузчик обязан уметь читать загрузочные файлы, иначе он не смог бы найти ядро. Если поставщики ОС не удосужились написать соответствующий вторичный загрузчик, а предоставили только драйвер файловой системы, ОС сможет работать с такой файловой системой, но не сможет из нее загружаться.
Некоторые системы, например DOS, могут грузиться только с устройств, поддерживаемых BIOS, и только из одного типа файловой системы - FAT, драйвер которой скомпонован с ядром. Любопытное развитие этой идеи представляет Linux, модули которого могут присоединяться к ядру как статически, так и динамически. Динамически могут подгружаться любые модули, кроме драйверов загрузочного диска и загрузочной ФС.
Преимущества, которые дает динамически собираемое в момент загрузки ядро, не так уж велики по сравнению с системами, в которых ядро собирается статически. Впрочем, ряд современных систем (Solaris, Linux, Netware) идут в этом направлении дальше и позволяют подгружать модули уже после загрузки и даже выгружать их. Такая архитектура предъявляет определенные требования к интерфейсу модуля ядра (он должен уметь не только инициализировать сам себя и, если это необходимо, управляемое им устройство, но и корректно освобождать все занятые им ресурсы при выгрузке), но дает значительные преимущества.
Во-первых, это допускает подгрузку модулей по запросу. При этом подсистемы, нужные только иногда, могут не загрузиться вообще. Даже те модули, которые нужны всегда, могут проинициализироваться, только когда станут нужны, уменьшив тем самым время от начала загрузки до старта некоторых сервисов. Второе, пожалуй даже более важное для системного администратора, преимущество состоит в возможности реконфигурировать систему без перезагрузки, что особенно полезно для систем коллективного пользования. И, наконец, возможность выгрузки модулей ядра иногда (но не всегда, а лишь если поломка не мешает драйверу корректно освободить ресурсы) позволяет корректировать работу отдельных подсистем - опять-таки без перезагрузки всей ОС и пользовательских приложений.
Оказавшись в памяти и, так или иначе, подтянув все необходимые дополнительные модули, ядро запускает их подпрограммы инициализации. При динамической подгрузке инициализация модулей часто происходит по мере их загрузки. Обычно инициализация ядра завершается тем, что оно загружает определенную программу, которая продолжает инициализацию - уже не ядра, но системы в целом.
Так, системы семейства UNIX имеют специальную инициализационную программу, которая так и называется - init. Эта программа запускает различные процессы-демоны, например cron - программу, которая умеет запускать другие заданные ей программы в заданные моменты времени, различные сетевые сервисы, программы, которые ждут ввода с терминальных устройств (getty), и т. д. Набор запускаемых программ задается в файле /etc/inittab (в разных версиях системы этот файл может иметь разные имена. /etc/inittab используется в System V). Администратор системы может редактировать этот файл и устанавливать те сервисы, которые в данный момент нужны, избавляться от тех, которые не требуются, и т. д.
Программа init остается запущенной все время работы системы. Она, как правило, следит за дальнейшей судьбой запущенных ею процессов. В зависимости от заданных в файле /etc/inittab параметров, она может либо перезапускать процесс после его завершения, либо не делать этого.
Аналогичный инициализационный сервис в той или иной форме предоставляют все современные операционные системы.
Полный цикл загрузки Solaris (версия Unix System V Release 4, поставляющаяся фирмой Sun) на компьютерах х86 происходит в шесть этапов. Первые три этапа стандартны для всех ОС, работающих на IBM PC-co вмести мой технике. При включении компьютера запускается прошитый в ПЗУ BIOS. Он проводит тестирование процессора и памяти и инициализацию машины- В процессе инициализации BIOS устанавливает обработчик прерывания int 13h. Этот обработчик умеет считывать и записывать отдельные секторы жестких и гибких дисков и производить некоторые другие операции над дисковыми устройствами. Первичные загрузчики ОС обычно пользуются этим сервисом. Некоторые ОС, например MS/DR DOS, используют этот сервис не только при загрузке, но и при работе, и, благодаря этому, могут не иметь собственного модуля управления дисками. Если загрузка происходит с жесткого диска, BIOS загружает в память и запускает нулевой сектор нулевой дорожки диска. Этот сектор обычно содержит не первичный загрузчик операционной системы, a MBR (Master Boot Record- главная загрузочная запись). Эта программа обеспечивает разбиение физического жесткого диска на несколько логических разделов (partition) и возможность попеременной загрузки различных ОС, установленных в этих разделах (рис. 3.16).

Разбиение физического диска на логические программа MBR осуществляет на основе содержащейся в ее теле таблицы разделов (partition table), которая содержит границы и типы разделов. MBR перехватывает прерывание Int 13h и транслирует обращения к дисковой подсистеме так, что обращения к логическому диску N преобразуются в обращения к N-ному разделу физического диска.
Один из разделов диска должен быть помечен как активный или загрузочный. MBR загружает начальный сектор этого раздела - обычно это и есть первичный загрузчик ОС. Многие реализации MBR, в том числе и поставляемая с Solaris, могут предоставлять пользователю выбор раздела, с которого следует начинать загрузку. Выбор обычно предоставляется в форме паузы, в течение которой пользователь может нажать какую-то клавишу или комбинацию клавиш. Если ничего не будет нажато, начнется загрузка с текущего активного раздела.
Так или иначе, но загрузочный сектор- по совместительству, первичный загрузчик Solaris оказывается в памяти и начинает исполняться. Исполнение его состоит в том, что он загружает - нет, еще не ядро, а специальную программу, называемую DCU (Device Configuration Utility, утилита конфигурации устройств). Основное назначение этой программы - имитация сервисов консольного монитора компьютеров фирмы Sun на основе процессоров SPARC.
DCU производит идентификацию установленного в машине оборудования. Пользователь может вмешаться в этот процесс и, например, указать системе, что такого-то устройства в конфигурации нет, даже если физически оно и присутствует, или установить драйверы для нового типа устройств. Драйверы, используемые DCU, отличаются от драйверов, используемых самим Solaris, называются они BEF (Boot Executable File), и начинают исполнение, как и сама DCU, в реальном режиме процессора х86.
Найдя все необходимое оборудование, DCU запускает вторичный загрузчик Solaris. Логический диск, выделенный Solaris, имеет внутреннюю структуру и также разбит на несколько разделов (рис. 3,17). Чтобы не путать эти разделы с разделами, создаваемыми MBR, их называют слайсами (slice). Загрузочный диск Solaris должен иметь минимум два слайса - Root (корневая файловая система) и Boot, в котором и размещаются вторичный загрузчик и DCU.

Вторичный загрузчик, пользуясь BEF-модулем загрузочного диска для доступа к этому диску, считывает таблицу слайсов и находит корневую файловую систему. В этой файловой системе он выбирает файл /kernel/unix, который и является ядром Solaris. В действительности, вторичный загрузчик исполняет командный файл, в котором могут присутствовать условные операторы, и, в зависимости от тех или иных условий, в качестве ядра могут быть использованы различные файлы, /kernel/unix используется по умолчанию. Кроме того, пользователю предоставляется пауза (по умолчанию 5 секунд), в течение которой он может прервать загрузку по умолчанию и приказать загрузить какой-то другой файл, или тот же файл, но с другими параметрами.
Будучи так или иначе загружено, ядро, пользуясь сервисами вторичного загрузчика, считывает файл /etc/system, в котором указаны параметры настройки системы. Затем, пользуясь информацией, предоставленной DCU, ядро формирует дерево устройств - список установленного в системе оборудования, и в соответствии с этим списком начинает подгружать модули, управляющие устройствами-драйверы. Подгрузка по-прежнему происходит посредством сервисов вторичного загрузчика - ведь все драйверы размещены на загрузочном диске и в корневой файловой системе, в том числе и драйверы самого этого диска и этой файловой системы.
Загрузив драйверы всех дисковых устройств и файловых систем (а при загрузке из сети - также сетевых контроллеров и сетевых протоколов), ядро начинает их инициализацию, С этого момента использовать сервисы вторичного загрузчика становится невозможно, но они уже и не нужны. Проинициализировав собственный драйвер загрузочного диска и корневой файловой системы, ядро запускает программу init, которая подключает остальные диски и файловые системы, если они есть, указывает параметры сетевых устройств и инициализирует их, запускает обязательные сервисы, в общем, производит всю остальную стартовую настройку системы.
Существуют ОС, которые не умеют самостоятельно выполнять весь цикл бутстрапа. Они используют более примитивную операционную систему, которая исполняет их вторичный (или какой это уже будет по счету) загрузчик, и помогает этому загрузчику поместить в память ядро ОС. На процессорах х8б в качестве стартовой системы часто используется MS/DR DOS, а загрузчик новой ОС оформляется в виде ЕХЕ-файла.
Таким образом устроены системы MS Windows l.x-З.х, Windows 95/98/ME, DesqView и ряд других "многозадачников" для MS DOS. Таким же образом загружается сервер Nowell Netware, система Oberon для х8б, программы, написанные для различных расширителей DOS (DOS extenders) и т. д. Многие из перечисленных систем, например Windows (версии младше 3.11 - в обязательном порядке, а 3.11 и 95/98/ME только в определенных конфигурациях) используют DOS и во время работы в качестве дисковой подсистемы. Тем не менее, эти программные пакеты умеют самостоятельно загружать пользовательские программы и выполнять все перечисленные во введении функции и должны, в соответствии с нашим определением, считаться полноценными операционными системами.