osdev.orgРазработка операционных систем
КАК РАБОТАЮТ ЯДРО, КОМПИЛЯТОР/АССЕМБЛЕР И БИБЛИОТЕКА СИ (6 августа 2012 года)

Оглавление | Оригинал 


Содержание

1. Ядро
2. Библиотека Си
3. Компилятор/Ассемблер
4. Линкер
 4.1. Статическое связывание
 4.2. Динамическое связывание
 4.3. Общие библиотеки
 4.4. ABI - Интерфейс бинарных приложений
 4.5. Неразрешаемые символы
5. __alloca, __main (ссылка на оригинал)
6. memcpy (ссылка на оригинал)

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

 Ядро предоставляет свои сервисы посредством набора системных вызовов; их название и реализация различны для различных ядер.

Библиотеки Си
 Всегда следует учитывать: когда вы работаете на собственном ядре, вам не доступны библиотеки Си. Вы не должны использовать #include для того что вы не написали сами. Так же вам придётся портировать существующие библиотеки Си. Смотрите статьи Кросс-компилятор GCC, Дальнейшие шаги/Стандартные библиотеки.

 Библиотека Си реализует стандарные функции Си (например объявляемые в <stdlib.h>, <math.h>, <stdio.h> и пр.) и предоставляет их в бинарном виде пригодном для связывания с приложениями пространства пользователя.

 В дополнение к стандарным функциям Си (определённым в стандарте ISO), библиотека Си может (и обычно делает это) реализовывать дополнительную функциональность, которая может определяться или не определяется стандартом. Стандартная библиотека Си, например, не говорит о работе с сетью. Для Unix-подобных систем, стандарт POSIX определяет то, что ожидается от библиотеки Си; другие системы могут фундаментально отличаться.

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

 Некоторые реализации стандарной библиотеки Си включают:

 Некоторые примеры доступны в Библиотеке Вызовов, или вы можете создать собственную библиотеку Си.

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

 Компилятор принимает исходный код языка высоко уровня, и непосредственно преобразует его в объектный кодя, или (как в случае GCC) преобразовыет его в исходный код Ассемблера и вызывает Ассемблер для последнего шага.

 Результирующий объектный код ещё не содержит код для вызова стандарных функций. Если вы включите например <stdio.h> и используете printf(), объектный код будет содержать ссылку о том, что функция с именем printf() (принимающая const char* и число неименованных аргументов в качестве параметров) должна быть связана с объектным кодом для получения исполняемого файла.

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

 Некторые дополнительные операции (например 64-битное деление в 32-битных системах) может включать внутреннюю функциональность компилятора. Для GCC, эти функции размещаются в libgcc.a. Содержимое этой библиотеки несвязано с используемой операционной системой, т.ч. вы можете просто взять libgcc.a и связать его с вашим ядром.

Линкер (Компоновщик)
 Линкер принимает объектный код порождённый компилятором/ассемблером и связывает его с библиотекой Си (и/или libgcc.a или другой предоставленной библиотекой). Это можно сделать двумя способами: статическим или динамическим.

Статическое связывание
 При статическом связывании, линкер вызывает процесс сборки сразу после запуска компилятора/ассемблера. Он берёт объектный код, проверяет его на наличие не разрешимых ссылок, и если проверка успешна, разрешает ссылки на доступные библиотеки. Затем, линкер добавляет двоичный код этих библиотек в исполняемый файл и создаёт полный исполняемый файл, т.е. при запуске данного файла ему не требуется ничего дополнительно кроме наличия ядра.

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

Динамическое связывание
 При динамическом связывании, линкер (компоновщик) вызывается при загрузке выполняемого файла. Неразрешённые ссылки в объектном коде разрешаются на библиотеки представленны в системе. Это позволяет создавать исполняемые файлы достаточно небольшими и позволяет использовать стратегии экономии памяти, такие как разделяемые библиотеки (смотрите далее).

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

Общие (разделяемые) библиотеки
(Полезная статья на OpenNet.ru -
http://www.opennet.ru/soft/ruprog/shlibs.txt)
 Достаточно популярной стратегией является динамическое связывание разделяемых библиотек для множества исполняемых файлов. Это означает, что вместо присоединения бинарника библиотеки к образу исполняемого файла, в исполняемых файлах проставляются ссылки относящиеся к одной и той же необходимой библиотеке.

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

 Во-вторых, в среде виртуальной памяти, как правило не возможно обеспечить библиотеку для всех исполняемых файлов в системе в том же самом адресном пространстве виртуальной памяти. Для доступа библиотечного кода к произвольному виртуальному адресутребуется позиционнонезависимый код библиотеки (что может быть достигнуто например, путём установки аргумента -PIC в командной строке компилятора GCC). Это требует поддержки особенности бинарного формата (таблиц перераспределения), и может приводить к менее эффективному коду на данной архитектуре.

ABI -
Application Binary Interface
 ABI системы определяет, как на самом деле вызываются функции библиотеки и системные вызовы ядра. Он включает, например, бедет ли параметры передаваться в стек или в регистры, как размещаются точки входа в библиотеки и пр.

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

Неразрешаемые символы
 Связывание это стадия, на которой вы обнаруживаете что некоторые вещи которые были добавлены без вашего ведома, и которые не предоставляются вашей средой. Сюда можно включить ссылки на alloca(), memcpy() и некоторые другие. Как правило, это признак того, что ваши инструменты или опции командной строки не корректно установлены для компиляции ядра вашей ОС - или что вы используете функциональность которая не реализована в вашей библиотеке Си или среде исполнения.

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

 Другие символы, такие как _udiv* или __builtin_saveregs, доступны в ОС-независимой библиотеке libgcc.a. Если вы получаете ошибки об отсутствии данных символов, попробуйте связать ваше ядро с этой библиотекой. Здесь эта тема рассматривается подробно.