Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Программирование и алгоритмические языки


Ulrich Drepper "Как писать разделяемые библиотеки"
Назад Оглавление Вперед

1.4. Запуск: ядро

Запуск выполнения программы начинается в ядре, как правило, с системного вызова execve. Код, который выполнялся в текущий момент, будет замен новой программой. Это означает, что содержимое адресного пространства будет заменено содержимым файла, в котором находится программа. Это осуществляется не при помощи простого отображения (с использованием mmap) содержимого файла. Файлы ELF структурированы и обычно в файле есть, по крайней мере, три области различного вида:

  • Код, который выполняется, в эту область, как правило, записывать нельзя;
  • Данные, которые можно изменять; в этой области, как правило, нельзя выполнять код;
  • Данные, которые не используются во время выполнения кода, поэтому их не требуется загружать во время запуска программы.\

Современные операционные системы и процессоры могут отдельно на каждой странице памяти {Примечание 1} разрешать или запрещать выполнять чтение , запись или исполнение кода. Желательно отмечать, на каких страницах запрещено выполнять запись, т. к. это значит, что эти страницы могут быть разделяемыми между процессами и к ним может выполняться обращение из приложения или объекта DSO. Защита от записи также поможет обнаружить и предотвратить непреднамеренное или злонамеренное изменение данных или даже кода.

Примечание 1: Страница памяти является самой маленькой сущностью, с которой работает подсистема памяти ОС. Размер страницы может быть различным в системах с различной архитектурой и даже в системах с одной и той же архитектурой.

Чтобы ядро могло находить различные области или, если говорить на языке ELF, сегменты и права доступа к ним, в файлах формата ELF определена таблица, в которой, кроме всего прочего, хранится и эта информация. В каждом исполняемом модуле и объекте DSO должна присутствовать таблица ELF Program Header (ELF-заголовок программы). Как показано на рис.1, она определяется с использованием типов данных Elf32 Phdr и Elf64 языка C.

Рис.1: Структура данных ELF-заголовка программы на C

Чтобы можно было найти структуру данных заголовка программы, требуется еще одна структура данных — заголовок ELF. Заголовке ELF является единственной структурой данных, для которой в файле выделено фиксированное место со смещением, равным нулю. Его структура данных на языке C показана на рис.2. В поле e phoff указывается, откуда, если считать от начала файла, начинается таблица заголовка программы. В поле e phnum указывается количество записей в таблице заголовка программы, а в поле е phentsize указывается размер каждой записи. Это последнее значение используется только на стадии исполнения двоичного кода для проверки его целостности.

Рис 2: Структура данных ELF-заголовка на C

Различные сегменты представлены в записях заголовка программы с помощью значения PT LOAD в поле типа p. Поля p offset и p filesz указывают, где в файле начинается сегмент и какова его длина. Поля p vaddr и p memsz указывают, где размещается сегмент в виртуальном адресном пространстве процесса и какого размера должна быть эта область памяти. Для самого значения поля p vaddr не нужно указывать конечный адрес загрузки. Динамические объекты DSO могут загружаться в произвольные адреса в виртуальном адресном пространстве. Но относительное положение сегментов важно. Для предварительно скомпонованных объектов DSO имеет смысл фактическое значение поля p vaddr: оно указывает адрес, для которого была выполнена предварительная компоновка объекта DSO. Но даже это не означает, что динамический компоновщик не может проигнорировать эту информацию, если это будет необходимо.

Размер сегмента в файле может быть меньше адресного пространства, которое он занимает в памяти. Первые p filesz байтов области памяти будут проинициализированы данными, взятыми из сегмента в файле, оставшаяся часть инициализируется нулями. Ее можно использовать для обработки секции BSS {Примечание 2} — секций неинициализированных переменных, которые, в соответствии со стандартом C, инициализируются нулями. Преимущество такой обработки неинициализированных переменных в том, что размер файла может быть уменьшен, поскольку не нужно запоминать неинициализированное значение, данные не должны копироваться с диска в память, а память, предоставляемая в ОС через интерфейс mmap, уже инициализирована нулями.

Примечание 2: В секции BSS находятся только байты NUL. Поэтому их не будет в приложении, когда оно записано на носителе в виде файла. Загрузчику для того, чтобы выделить память достаточного размера и заполнить ее значениями NUL, необходимо просто знать размер этой секции.

Наконец, флаги p сообщают ядру, какие права доступа можно использовать для страниц памяти. Это поле заполнено битами, которые определяются в следующей таблице. Флаги непосредственно отображаются в флаги, которые понимает интерфейс mmap.

p_flagsЗначениеФлаг mmapОписание
PFX1 PROT EXECРазрешено выполнение
PFW2 PROT WRITEРазрешена запись
PFR4 PROT READРазрешено чтение

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

В ядре жестко не прописывается, какой двоичный модуль должен быть динамическим компоновщиком. Вместо этого в приложении в заголовке программы есть запись с тегом PT INTERP. В поле p offset этой записи имеется смещение, указывающее имя этого файла и завершающееся значением NUL. Единственное требование к этому именованному файлу, чтобы его адрес загрузки не конфликтовал с адресами загрузки каких-либо исполняемых модулей, с которыми он может использоваться. В целом это означает, что динамический компоновщик не имеет фиксированного адреса загрузки и может быть загружен в любое место; это именно то, что разрешено динамически загружаемым двоичным модулям.

Как только динамический компоновщик будет также загружен в память, где должен быть запущен процесс, может стартовать динамический компоновщик. Обратите внимание, это не точка входа приложения, на которое передается управление. К работе готов только динамический компоновщик. Вместо того, чтобы сразу вызвать динамический компоновщик, нужно выполнить еще один шаг. Динамическому компоновщику нужно как-то сказать, где можно найти приложение и куда следует передать управление после того, как приложение будет завершено. Для этого существует специальный прием. Ядро помещает в стек нового процесса массив пар вида "тег - значение". В этом вспомогательном векторе кроме тех двух значений, которые были упомянуты ранее, хранятся еще несколько значений, которые позволяют динамическому компоновщику не делать несколько системных вызовов. В заголовочном файле elf.h определено несколько констант с префиксом AT. Это теги для записей во вспомогательный вектор.

После того, как вспомогательный вектор будет настроен, ядро, наконец, готово передать в пользовательском режиме управление динамическому компоновщику. Точка входа определена в поле записи e заголовка ELF динамического компоновщика.


Предыдущий раздел:   Следующий раздел:
Назад Оглавление Вперед