Разница между ролями загрузчика и инициализацией C

Я читал о роли инициализации среды выполнения C по этой ссылке: http://www.embecosm.com/appnotes/ean9/html/ch05s02.html

В нем говорится, что инициализация времени выполнения выполняет такие задачи, как настройка стека, а на более подробных страницах также указывает, что он инициализирует сегмент bss нулями. В некоторых других местах я также читал, что он инициализирует данные и некоторые другие сегменты.

Это вызвало у меня сомнения в отношении того, что делает погрузчик? Поскольку некоторые из этих задач также являются ответственностью загрузчика.

Итак, мои вопросы:

  • Что делает инициализация времени выполнения или c runtime на самом деле?
  • Что делает загрузчик на самом деле?

ИЗМЕНИТЬ

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

Ответ 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" может размываться при заданных достаточно сложных автономных проектах для встроенных целей.

Сейчас:

  1. Что делает загрузчик [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 или он потерпит неудачу.
    • Ядро начинает выполнение двоичного файла, возможно, в _start, как указано.
    • Eli Bendersky имеет более подробное объяснение по этому вопросу под названием " Как статически связанные программы запускаются в Linux".
  • Загрузчики времени/динамические компоновщики/все, что вы хотите назвать. Я расскажу вам о статье Анатомия Linux-динамических библиотек для получения информации о том, как они работают. Конечно, набор функций dlopen(3)/dlsym(3)/dlclose(3)/dlerror(3) - это просто API для взаимодействия с динамическим загрузчиком. Я настоятельно рекомендую прочитать страницы руководства на этих интерфейсах, чтобы получить представление об функциональности, поддерживаемой динамическим загрузчиком Linux, и о том, какие вещи выполняет загрузчик.