# Как ядро, компилятор и код на C работают вместе
# Ядро
Ядро - это основа операционной системы. В традиционном концепте он отвечает за управление памятью, ввод-вывод, обработку прерываний и различные другие вещи. И даже в то время как некоторые современные проекты, такие как микроядра или макроядра, перемещают некоторые из этих сервисов в пользовательское пространство, это мало что значит.
Ядро делает свои службы доступными с помощью набора системных вызовов; то, как они вызываются и что они делают, в точности отличается от ядра к ядру.
# Библиотека на C
Библиотека C реализует стандартные функции C (т.е. вещи, объявленные в <stdlib.h>, <math.h>, <stdio.h> и т.д.) И предоставляет их в двоичной форме, подходящей для связывания с приложениями пользовательского пространства.
В дополнение к стандартным функциям C (как определено в стандарте ISO), библиотека C может (и обычно реализует) дополнительные функции, которые могут или не могут быть определены каким-либо стандартом. Например, стандартная библиотека C ничего не говорит о сети. Для Unix-подобных систем стандарт POSIX определяет, что ожидается от библиотеки C.
Следует отметить, что для реализации своей функциональности библиотека C должна вызывать функции ядра. Таким образом, для вашей собственной ОС вы, конечно, можете взять готовую библиотеку C и просто перекомпилировать ее для вашей ОС, но для этого требуется, чтобы вы рассказали библиотеке, как вызывать функции вашего ядра, и ваше ядро фактически предоставляло эти функции.
Подробнее о создании собственной библиотеке можно почитать тут: To do...
# Компилятор / Ассемблер
Ассемблер берет исходный код и превращает его в (двоичный) машинный код; точнее, он превращает исходный код в объектный код, который содержит дополнительную информацию, такую как имена символов, информация о перемещении и т.д.
Компилятор берет исходный код языка более высокого уровня и либо непосредственно превращает его в объектный код, либо (как в случае с GCC) превращает его в исходный код ассемблера и вызывает ассемблер для последнего шага.
Полученный объектный код еще не содержит кода для стандартных вызываемых функций. Если вы включили, например, <stdio.h> и использовали printf(), объектный код будет просто содержать ссылку, указывающую, что функция с именем printf() (и принимая const char * и ряд неназванных аргументов в качестве параметров) должна быть связана с объектным кодом, чтобы получить полный исполняемый файл.
Некоторые компиляторы используют стандартные библиотечные функции внутри, что может привести к тому, что объектные файлы будут ссылаться, например, на memset() или memcpy(), даже если вы не включили заголовок или использовали функцию с этим именем. Вам придется предоставить реализацию этих функций компоновщику, иначе связывание завершится неудачно. Автономная среда GCC ожидает только функции memset(), memcpy(), memcmp() и memmove(), а также библиотеку libgcc. Некоторые расширенные операции (например, 64-битные деления в 32-битной системе) могут включать внутренние функции компилятора. Для GCC эти функции находятся в libgcc. Содержимое этой библиотеки не зависит от того, какую ОС вы используете, и оно не испортит ваше скомпилированное ядро проблемами лицензирования любого рода.
# Компоновщик
Компоновщик берет объектный код, сгенерированный компилятором/ассемблером, и связывает его с библиотекой C(и/или libgcc.a или любой другой библиотекой ссылок, которую вы предоставляете). Это можно сделать двумя способами: статическим и динамическим.
# Статическая компоновка
При статическом линкинге компоновщик вызывается во время процесса сборки, сразу после запуска компилятора/ассемблера. Он берет объектный код, проверяет его на наличие неразрешенных ссылок и проверяет, может ли он разрешить эти ссылки из доступных библиотек. Затем он добавляет двоичный код из этих библиотек в исполняемый файл; после этого процесса исполняемый файл завершен, т.е. При запуске он не требует присутствия ничего, кроме ядра.
С другой стороны, исполняемый файл может стать довольно большим, и код из библиотек дублируется снова и снова, как на диске, так и в памяти.
# Динамическая компоновка
При динамическом связывании компоновщик вызывается во время загрузки исполняемого файла. Неразрешенные ссылки в объектном коде разрешаются по отношению к библиотекам, присутствующим в настоящее время в системе. Это делает исполняемый файл на диске намного меньше и позволяет использовать стратегии экономии места в памяти, такие как общие библиотеки (см. ниже).
С другой стороны, исполняемый файл становится зависимым от наличия библиотек, на которые он ссылается; если в системе нет этих библиотек, исполняемый файл не может работать.
# Общие библиотеки
Популярная стратегия заключается в совместном использовании динамически связанных библиотек между несколькими исполняемыми файлами. Это означает, что вместо присоединения двоичного файла библиотеки к исполняемому образу ссылки в исполняемом файле изменяются таким образом, чтобы все исполняемые файлы ссылались на одно и то же представление требуемой библиотеки в памяти.
Это требует некоторого обмана. Во-первых, библиотека должна либо вообще не иметь никакого состояния (статических или глобальных данных), либо предоставлять отдельное состояние для каждого исполняемого файла. Это становится еще сложнее с многопоточными системами, где один исполняемый файл может иметь более одного одновременного потока.
Во-вторых, в среде виртуальной памяти обычно невозможно предоставить библиотеку всем исполняемым файлам в системе по одному и тому же адресу виртуальной памяти. Для доступа к коду библиотеки по произвольному виртуальному адресу требуется, чтобы код библиотеки был независимым от позиции (что может быть достигнуто, например, путем установки параметра командной строки-PIC для компилятора GCC).
# ABI - Двоичный интерфейс приложения
ABI системы определяет, как на самом деле выполняются вызовы библиотечных функций и системные вызовы ядра. Это включает в себя, передаются ли параметры в стеке или в регистрах, как точки входа функций расположены в библиотеках и другие подобные проблемы.
При использовании статической компоновки результирующий исполняемый файл зависит от ядра, использующего тот же ABI, для которого был создан исполняемый файл; при использовании динамической компоновки исполняемый файл зависит от того, останется ли ABI библиотек неизменным.
# Неразрешенные символы
Компоновщик - это этап, на котором вы узнаете о том, что было добавлено без вашего ведома и что не предусмотрено вашей средой. Это может включать ссылки на alloca(), memcpy() или несколько других функций. Обычно это признак того, что либо ваша цепочка инструментов, либо параметры командной строки неправильно настроены для компиляции собственного ядра ОС, либо вы используете функции, которые еще не реализованы в вашей библиотеке C/среде выполнения! Вы наверняка столкнетесь с проблемами, если не используете кросс-компилятор и библиотеку libgcc и не имеете реализаций memcpy, memmove, memset и memcmp.
Другие символы, такие как _udiv* или __builtin_saveregs, доступны в libgcc. Если вы получаете ошибки из-за отсутствия таких символов, помните, что вам нужно прилинковать libgcc.