Ответ 1
- Что делает инициализация времени выполнения или c runtime на самом деле?
Википедия определяет библиотеку времени выполнения как:
набор низкоуровневых подпрограмм, используемых компилятором для вызова некоторых из поведения среды выполнения, путем вставки вызовов в библиотеку времени выполнения в скомпилированный исполняемый двоичный файл.
В случае программ на языке С, библиотека времени выполнения имеет очень мало возможностей за пределами загрузки программы. Компилятор запускает среду выполнения C, чтобы загружать различные объекты окружающей среды, а затем в основном отключает управление для пользователя, вызывая main
.
Учитывая ответы в комментариях к вашему вопросу, вы, возможно, уже выяснили, что процесс, с помощью которого программа загружается для своей среды, зависит от количества целевых сред. Учитывая количество платформ и операционных систем, поддерживаемых C в настоящее время и в прошлом, нет возможности перечислить все способы, с которыми работала или в настоящее время работает среда выполнения.
Каждая библиотека C имеет свою собственную среду выполнения C, и каждая среда, поддерживающая C, скорее всего, будет иметь разные проблемы и требования к загрузке. Эти требования во многом зависят от особенностей операционной системы или оборудования в сочетании с полнотой реализации C. Тем не менее, я могу ответить на некоторые вопросы, которые обычно выполняются в среде C, в среде, с которой вы можете быть знакомы.
-
Поскольку C-среда выполнения отвечает за вызов
main
, это означает, что функции вызова, зарегистрированные черезatexit(3)
, будут ответственны за время выполнения C. -
Разрешить и вызвать любые интерфейсы конструктора/деструктора (
_init
,_fini
и т.д.) -
Инициализировать и вызвать загрузчика в режиме реального времени (который отвечает за разрешение и загрузку динамических общих объектов, зарегистрированных во время привязки и загруженных во время выполнения).
-
Обработка вывода отдельных потоков изящно.
-
Инициализация и передача
argc
иargv
в программуmain
. -
Определить и инициализировать различные глобальные символы библиотеки C. Например, он корректно устанавливает
errno
для среды (современные системы определяютerrno
как потокобезопасные, поэтому он должен жить в TLS).environ
- это еще один глобальный символ, который требует инициализации до вызоваmain
. -
В этом случае среда выполнения C должна настроить TLS.
-
Тонны больше.
Вам может быть интересно просмотреть glibc реализацию среды выполнения, которая находится в каталоге csu (C start-up). (Есть некоторые части, относящиеся к машине, за пределами этого каталога.)
Различные системы будут иметь разные требования. Как вы уже прочитали, встроенная система может иметь значительно большую работу для среды выполнения, поскольку они могут отвечать за задачи, начиная от инициализации регистра до загрузки и выполнения программы (если это не предусмотрено никаким ядром). Различие между "C runtime" и "kernel" может размываться при заданных достаточно сложных автономных проектах для встроенных целей.
Сейчас:
- Что делает загрузчик [a] на самом деле?
Существует много типов загрузчиков, также в зависимости от среды выполнения. Для небольшой встроенной среды с EEPROM загрузчиком может быть какая-то прошивка, которая запускает выполнение всего, что находит на адресе 0. Вы можете также подумать о себе как загрузчике, вручную записывая свой двоичный код в EEPROM.
В современных операционных системах существует ряд погрузчиков.
-
Загрузчики. Исторически сложилось так, что они работали таким образом, что BIOS выбирает загрузочное устройство, смотрит на адрес, считывает 512 байт данных в память и начинает оттуда оттуда. Я уже давно выхожу из этого мира, поэтому я не уверен, какая разница с EFI/UEFI, кроме того, что они являются достаточно более полными (и сложными) средами бутстрапа.
-
Ядро. Когда вы выполняете программу, тонкие вещи идут под капотом, чтобы добиться этого. Предполагая, что вы запускаете свою программу из оболочки в некоторых Unix-подобных ОС, процесс загрузки может следовать следующим образом:
- Ваша оболочка пытается найти двоичный файл где-нибудь в настроенной вами среде
PATH
. Это делается путем выдачи ряда системных вызовов ядру для разрешения имени файла под другой последовательностью путей. - Предполагая, что файл найден, оболочка обычно будет
fork(2)
иexecve(2)
. Вызовfork(2)
заставляет ядро создавать новый процесс; вызовexecve(2)
заменяет клонированный двоичный код новым. - Ядро считывает первую страницу файла со своего носителя (диска, сети, памяти и т.д.) и пытается выяснить, как его выполнить.
- Если это двоичный файл ELF, он может определить это из двоичного заголовка. Затем ядро загружает разделы двоичного кода в память где-то на основе смещений, указанных в заголовках разделов ELF, настраивает отображаемые области для стеков и еще что-то, а затем начинает выполнение на основе адреса записи (также является частью заголовка ELF). Эта точка входа, вероятно,
_start
, часть времени выполнения C. - Если это не ELF-бинарный файл, он все равно может быть выполнен через интерпретатор. Ядро попытается проанализировать интерпретатор с самого начала файла (например,
#!/bin/bash
), устранить его и выполнить. В конце концов, он найдет исполняемый файл ELF или он потерпит неудачу.
- Если это двоичный файл ELF, он может определить это из двоичного заголовка. Затем ядро загружает разделы двоичного кода в память где-то на основе смещений, указанных в заголовках разделов ELF, настраивает отображаемые области для стеков и еще что-то, а затем начинает выполнение на основе адреса записи (также является частью заголовка ELF). Эта точка входа, вероятно,
- Ядро начинает выполнение двоичного файла, возможно, в
_start
, как указано. - Eli Bendersky имеет более подробное объяснение по этому вопросу под названием " Как статически связанные программы запускаются в Linux".
- Ваша оболочка пытается найти двоичный файл где-нибудь в настроенной вами среде
-
Загрузчики времени/динамические компоновщики/все, что вы хотите назвать. Я расскажу вам о статье Анатомия Linux-динамических библиотек для получения информации о том, как они работают. Конечно, набор функций
dlopen(3)
/dlsym(3)
/dlclose(3)
/dlerror(3)
- это просто API для взаимодействия с динамическим загрузчиком. Я настоятельно рекомендую прочитать страницы руководства на этих интерфейсах, чтобы получить представление об функциональности, поддерживаемой динамическим загрузчиком Linux, и о том, какие вещи выполняет загрузчик.