Продолжаем разбираться с загрузчиками. Как вы помните, начальный загрузчик сталкивался с тем, что было неизвестно, что именно будет загружаться. Кроме того, ему приходилось работать с внешними накопителями напрямую, ведь в памяти машины еще не было ни ОС, ни драйверов. Но когда ОС уже загружена все должно быть легко и просто. Или нет?
Что требуется загружать во время работы ОС? Не сомневаюсь, что все назовут, как минимум, программы пользователя. Многие вспомнят о разделяемых библиотеках (.dll в Windows, .so в Linux, и т.д.). Продвинутые пользователи добавят драйверы. Знатоки упомянут модули ядра ОС. Все? Нет, не все, но нам и этого будет достаточно.
На самом деле, при всей функциональной разнице, между программами, библиотеками драйверами, модулями ядра есть очень много общего. И сам процесс их загрузки из внешнего хранилища и подготовки к работе может быть единым. Не верите? Давайте разбираться.
Начнем мы с загрузчиков, которые не являются ни связывающими, ни перемещающими. С самых простых. Но постепенно доберемся до весьма нетривиальных. Если конечно наберетесь терпения и дочитаете до конца.
Простейший случай
Помните, в первой части я упоминал о загрузке программ пользователя с бытового магнитофона в первых, бытовых же, ПК? Конечно, речь идет не о текстах программ на языках высокого уровня, например, на Basic или Focal, которые сохраняет и загружает соответствующий интерпретатор. Речь идет о программах и фрагментах программ в машинных кодах, в двоичном виде.
Такие программы являются просто двоичными образами, которые можно просто поместить в память и, возможно, передать им управление. Никакие дополнительные действия, никакая настройка, не требуются.
Загрузчик в таких ПК был простейшим. От него требовалось только прочитать данные с ленты и разместить их в памяти начиная с заданного адреса. Дополнительные проверки, например, корректности чтения или проверки формата, учитывать не будем.
Это скучный и неинтересный случай, но он нам полезен как первая ступенька. Давайте немного заглянем внутрь "образа программы". На самом деле, это не обязательно образ программы, это может быть и образ данных. И вообще, что угодно. Он просто загружается в память с заданного адреса, вот и все.
Но если это именно образ программы, то в нем будут располагаться, в любом порядке, области кода, констант, статических переменных имеющих начальные значения. Нет необходимости помещать в образ программы динамически создаваемые в стеке переменные, как и собственно стек, не инициализированные статические переменные, и т.д. То есть все то, что распределяется и создается уже после запуска загруженной программы на выполнение.
Впрочем, решение остается за программистом. Загружаемая двоичная программа должна полностью помещаться в доступную пользователю область свободной памяти. И это, пожалуй, единственное ограничение.
Такой загрузчик не является ни связывающим, ни перемещающим. Но у нас еще все впереди.
Простая ОС на универсальной ЭВМ
Малые универсальные ЭВМ часто работали под управлением достаточно простых ОС. Простейшие случаи однозадачных ОС рассматривать не будем, поговорим о простых многозадачных ОС, как минимум, с возможностью работы одной задачи переднего плана и одной фоновой. О многозадачных я кратко рассказывал в статье
Нас будет интересовать только деление памяти таких ОС на разделы. Не важно, что это за разделы, важно лишь то, что размеры разделов заданы или на этапе генерации ОС, или на этапе ее загрузки, или оператором во время работы ОС.
В данном случае показано, что доступная прикладным программам память поделена на три раздела. В общем случае, разделы могут иметь разные размеры. Раз у нас три раздела, значит и выполняться может три задачи одновременно, так как раздел в каждый момент времени может занимать только одна задача.
Обратите внимание, этот вариант не является развитием описанного выше варианта простейших ПК. Деление памяти на разделы появилось в ОС задолго до появления первого ПК. Но этот вариант сложнее ранее рассмотренного. Поэтому и рассматривается в таком порядке.
Деление памяти на разделы позволяет уйти от понятия адреса загрузки программы. Раздел, в котором будет выполняться программа определяется или на этапе ее компиляции и сборки, или, реже, при запуске указанием номера или имени раздела. Считаете, что не велика разница? Напрасно...
Даже в простых ОС программа пользователя на внешнем накопителе не является простым двоичным образом области памяти. Программа начинается со специального информационного блока - заголовка задачи. Формат этого заголовка определяется ОС. Поэтому давайте посмотрим на небольшой абстрактный пример
Заголовок выделен голубым цветом. Сам образ располагается вслед за заголовком. Загрузчик, все еще ни связывающий, ни перемещающий, выполняет загрузку задачи примерно так:
- Считывает заголовок задачи
- Используя номер (или имя) раздела из заголовка выполняет поиск раздела в системе узнавая таким образом адрес начала задачи в памяти машины
- Определяет размер задачи в памяти как сумму размеров кода, констант, инициализированных и неинициализированных данных, стека
- Проверяет, что размер задачи в памяти не превышает размера раздела
- Загружает в память последовательно секции кода и констант
- Резервирует память под инициализированные данные и выполняет ее очистку. Резервирует память под неинициализированные данные, но очистку не выполняет. Резервирует память под стек, очистка не выполняется.
- Помечает всю оставшуюся не распределенной память в разделе как доступную для динамического использования задачей.
- Передает загруженной и настроенной задаче управление
Это упрощенное описание. В частности, не сказано, что в заголовке содержится и смещение стартового адреса задачи (адрес на который передается управление) относительно секции кода в образе. Не сказано, что выполняется настройка оборудования управления памятью и регистров процессора. Эти детали нам не важны сегодня.
Давайте поговорим о некоторых этапах загрузки чуть подробнее. Во первых, задача (программа) не может выходить за пределы раздела памяти. Но она не обязана занимать раздел целиком. При этом даже не занятая в момент запуска часть раздела остается доступной задаче до момента ее завершения.
Свободную часть раздела часто называют пулом динамически распределяемой памяти. Из этого пула выделяет память, например, функция malloc языка С. И размер пула, естественно, зависит от размера раздела и размера задачи.
Во вторых, мы уже знаем, что нет необходимости хранить в образе программы на внешнем накопителе области памяти данных, которые не содержат данных на момент запуска. Это касается и стека. Константы хранить в образе надо. Константы это, например, текстовые строки и начальные значения инициализированных переменных.
Вот про инициализированные переменные нужно поговорить отдельно. Дело в том, что инициализированные переменные не являются инициализированными в момент запуска задачи! Парадокс? Вовсе нет. Момент присваивания инициализированным переменным начальных значений может быть различным.
Дело в том, что для многих (если не всех) современных языков программирования каждая переменная имеет еще и время жизни и область видимости. Глобальные переменные имеют время жизни равное времени жизни программы. Такие переменные инициализируются процедурой среды времени выполнения языка высокого уровня, которая выполняется до функции main (если используется язык С). Но эта процедура выполняется уже после старта задачи.
Статические переменные имеющие время жизни равное времени жизни отдельного модуля инициализируются соответствующей процедурой среды времени выполнения, которая вызывается при инициализации самого модуля, в котором они определены.
Все эти переменные занимают самостоятельное место в памяти. А вот автоматические переменные размещаются в стеке и постоянного места в памяти не имеют. Инициализация автоматических переменных выполняется при вхождении в их область видимости.
Очистку области памяти инициализированных переменных уже выполнил загрузчик. Поэтому процедурам инициализации достаточно только присвоить отличные от нуля значения соответствующим переменным.
Но где же наконец связывание и перемещение? Ведь мы рассматриваем связывающий перемещающий загрузчик. Скоро и до них доберемся.
Обобщенная структура памяти машины
В статье
я рассказывал о различных способах организации памяти ЭВМ. Большей частью речь шла о физической организации, но и логическая затрагивалась. Но в рамках сегодняшней статьи мы можем упростить различные способы физической организации памяти и свести к простой схеме база плюс смещение.
То есть, мы можем любую область памяти рассматривать как адрес ее начала и смещение внутри этой области от ее начала. Адрес начала может быть единым для нескольких областей, если они объединяются в одну большую область памяти. Или если память вообще не имеет оборудования управления памятью. Адрес начала сегмента или адрес начала страницы как раз и являются примерами базовой части адреса.
Адрес внутри сегмента, адрес внутри страницы, являются примерами смещения относительно базовой части адреса. И очень важным является тот факт, что смещение не зависит от базы. Пока программа оперирует только адресом внутри области памяти, тем самым смещением, она будет независимой от размещения самой области памяти внутри физической памяти машины.
Базовый адрес, расположение области памяти в памяти машины, определяет ОС. И область памяти можно безболезненно перемести в пределах физической памяти машины. При этом прикладная программа даже не заметит факта перемещения, так как оперирует только смещением.
Дополнительным условием безопасного перемещения области памяти является отсутствие внешних ссылок на ячейки внутри области. Как вариант, такие ссылки должна контролировать ОС и корректировать после перемещения области.
Все остальные механизмы и тонкости управления памятью нам сегодня не важны.
Структура образа программы на диске в современных ОС
Зависит от собственно ОС. Но в разных системах есть много общего, что и позволит нам рассмотреть общие моменты, пусть и для абстрактной задачи и абстрактной ОС.
Для понимания структуры задачи, а впоследствии и процедуры загрузки, полезно ознакомиться со статьями
Эти статьи не были особо популярны в момент опубликования. По вполне понятной причине - эти вопросы интересны не многим. Но сегодня нам будет нужно иметь представление о работе компоновщика в части связывания и перемещения различных объектов внутри программы. Повторно я не буду это описывать. В частности, предполагается, что читатели знают, что такое программные секции.
Образ задачи (программы) на диске, естественно, имеет заголовок, в котором содержится важнейшую информацию о структуре образа и параметрах задачи. Однако, даже в значительно упрощенном виде этот заголовок будет слишком сложен для рассмотрения в рамках сегодняшней статьи. Поэтому я буду лишь ссылаться на то, что какая то информация содержится в заголовке, но саму структуру заголовка приводить не буду.
Образ задачи состоит, не считая заголовка, из множества объектов. И одним из важнейших объектов является программная секция. Но эти программные секции отличаются от тех, которые определяются в объектных файлах. Секции в образе задачи создаются компоновщиком из секций объектных файлов.
Количество секций в образе может быть любым.
Программные секции а образе задачи делятся на следующие основные типы:
- Секции программного кода
- Секции данных
- Секция констант
Такое деление кажется очевидным, но оно имеет важное значение, которое не всегда видно с первого взгляда. Секция кода может получить управление и быть выполнена. Секции данных и констант не могут получить управление. И это верно даже для архитектуры фон Неймана, где нет явного деления на память программ и память данных. Это деление семантическое. И используется, в том числе, подсистемами безопасности и управления виртуальной памятью ОС.
Невозможность выполнения секции данных предотвращает возможность искажения кода программы за счет его модификации как данных. На секцию кода может быть наложено ограничение на любые действия, кроме выполнения. Но вопросы защиты памяти и безопасности мы сегодня не будем рассматривать.
Но сам факт, что секция кода не может быть изменена (только выполнена) позволяет не выделять ей место в свопе, что важно для подсистемы виртуальной памяти. Ведь при необходимости всегда можно восстановить секцию кода непосредственно из образа задачи. Однако, здесь требуется учитывать дополнительные атрибуты секции, о чем мы поговорим чуть позже.
Секция данных не может быть восстановлена из образа задачи, так как данные это переменные, которые изменяются при выполнении программы. Поэтому для секций данных подсистема виртуальной памяти выделяет место в свопе.
Секция констант, как и секция данных, не может быть выполнена. Однако, константы не изменяются во время выполнения программы, поэтому им не требуется выделять место в свопе, они могут быть восстановлены их образа задачи.
Секции имеют и дополнительные атрибуты, которые оказывают существенное влияние на размещение и управление соответствующими областями памяти.
Возможность перемещения секции в памяти определяется атрибутом
- Фиксированная/перемещаемая
Фиксированная секция, как следует из названия, не может быть перемещена после начального размещения. У внимательных читателей может возникнуть вопрос, зачем фиксировать область памяти, соответствующую секции, если прикладная программа работает только со смещением, как говорилось ранее?
Все верно, в пределах собственно прикладной задачи область памяти фиксировать не нужно. Но вы забываете, что адрес области, или ячеек в области, может быть передан куда то за пределы задачи. Например, как адрес буфера для обмена данными с другой задачей или внешним устройством. Ведь полноценное управление памятью никто не отменял. Внутри задачи достаточно смещения, но в пределах машины в целом, адрес должен быть полным.
Перемещение области сделать недействительными все внешние ссылки на ячейки в этой области. Или приведет к необходимости корректировать все внешние ссылки операционной системой после перемещения области. И об этом я говорил, вспомните.
Кроме того, фиксированная секция может иметь предопределенный адрес размещения в памяти, физический адрес. И это позволяет загружать модули ядра ОС и драйверы, а не только прикладные задачи.
Возможность сбросить содержимое области памяти и передать ее другой задаче определяется атрибутом
- Сбрасываемая/нет
В системах с виртуальной памятью страница физической памяти может временно передаваться другой задаче или ОС. Но такая передача не должна сказываться на работе задачи, которой страница была изначально выделена. Область памяти кода может быть очищена и передана другой задаче, а позже восстановлена из образа задачи. Область памяти данных, если они изменились, необходимо сначала записать в своп, потом очистить, и только тогда передавать другой задаче. Позже она будет восстановлена из свопа.
Но ведь может оказаться, что область памяти нельзя передавать другой задаче. Например, секция кода может содержать обработчик прерывания, а секция данных буфер обмена с внешним устройством.
Обратите внимание, что атрибут "не сбрасываемая" не может быть заменен атрибутом "не перемещаемая", хотя может показаться, что условия их применения одинаковые. Не сбрасываемая область памяти может быть перемещена, если на нее есть ссылки из других модулей задачи, но нет внешних ссылок.
Еще один важный атрибут любой секции - размер области в памяти. Именно в памяти, а не в образе на внешнем накопителе. Поскольку область памяти может быть выделена, но загружаемые в нее данные в образе могут отсутствовать.
Перечисленные параметры и атрибуты составляют заголовок секции. Любая секция в образе задачи имеет заголовок. Но кроме заголовка секция представлена и каталогами загружаемых данных. Каждая запись такого каталога имеет свой заголовок. Мы не будем его рассматривать отдельно, он довольно прост.
Таким образом, образ задачи на диске может иметь примерно такой вид
Красным цветом показана секция для которой выделяется область в памяти, но загрузки данных в нее не выполняется. Например, это может быть секция стека или неинициализированных данных. Как эта структура образа превращается в задачу в памяти мы рассмотрим чуть позже.
Такая структура образа не позволяет использовать разделяемые библиотеки. Напомню, что статически подключаемые библиотеки обрабатываются компоновщиком и не требуют дополнительных усилий при загрузке задачи. Разделяемые библиотеки подключаются динамически и могут быть изменены уже после компиляции и сборки задачи. Поэтому компоновщик оказывается бессильным.
Поэтому в образе задачи предусматривается еще один тип объектов - каталог внешних ссылок. Это те самые ссылки, которые не смог разрешить компоновщик. Что бы не привязываться к конкретной ОС будем считать, что внешняя ссылка определяется именем внешнего символа (процедуры, переменной) и именем библиотеки. Это позволяет избежать неоднозначности, если внешний символ экспортируется несколькими разделяемыми библиотеками.
Имя библиотеки и имя символа мы уже рассматривали. Можно сказать, что они определяют, куда именно идет ссылка. Имя секции и смещение (внутри секции) определяют, откуда идет ссылка. Это местоположение адреса, который должен быть откорректирован при настройке задачи после загрузки. На самом деле, в корректируемой машинной команде могут использоваться различные режимы адресации, что необходимо будет учитывать загрузчику. Но для нас это сегодня не столь важно, поэтому заострять внимание не будем.
Однако, наша задача может и сама экспортировать некоторые символы, может самая являться разделяемой библиотекой. Поэтому в образе может присутствовать и каталог экспортируемых символов. Он очень похож на каталог внешних ссылок, только нет необходимости для каждого символа указывать имя библиотеки. Я не буду отдельно иллюстрировать этот каталог.
Конечно, структура реального образа задачи (программы) гораздо сложнее. Но нам сегодня достаточно такого упрощенного представления.
Вот теперь мы наконец то готовы рассмотреть и собственно
Связывающий перемещающий загрузчик
Структура образа задачи, как мы только что видели, достаточно сложная. Но ведь и современные ОС сложные. Поэтому загрузчику, который загружает задачу в память и выполняет ее предварительную настройку, приходится нелегко.
Загрузка и настройка задачи выполняется в несколько этапов.
Считывание заголовка образа
Это самый простой этап. В заголовке содержится различная информация о задаче, например:
- Имя задачи/библиотеки/модуля. Имя задачи может отличаться от имени файла образа. К имени можно отнести и номер версии, например, библиотеки.
- Размер задачи в памяти. Может использоваться загрузчиком для запроса у ОС непрерывного блока памяти, который не обязательно равен сумме размеров отдельных областей.
- Различные атрибуты задачи.
- Указатели на список секций в образе, каталог внешних ссылок, каталог экспортируемых символов, другие объекты в образе.
Запрос памяти и размещение областей памяти
Прежде всего, загрузчик должен убедиться, что в системе есть достаточно свободной памяти для размещения задачи. В зависимости от схемы управления памятью в системе для задачи в памяти может выделяться один непрерывный блок или отдельные блоки под каждую область соответствующую секции в образе.
Мы договорились, что схемы управления памятью сегодня рассматривать не будем. Поэтому нам не важно, выделяется память единым блоком, или отдельными блоками. Для нас важно, что на данном этапе ОС выделила необходимую память для размещения всей задачи.
Я показал случай выделения задаче единого непрерывного блока памяти. Внутри выделенного блока памяти загрузчик распределил области памяти соответствующие секциям из образа задачи. При этом в памяти еще нет никакой информации. ОС или загрузчик могут заполнить выделенную память каким либо значением, например, обнулить.
В области верхних адресов показана область памяти, которая не принадлежит ни одной секции. Эта область не обязательно присутствует. Она может использоваться как пул для подсистемы управления динамически распределяемой памяти задачи. Помните malloc?
Загрузка данных из образа в память
Память получена и распределена. Теперь можно начинать загрузку образа задачи в память. Загрузка выполняется отдельно для каждой секции. Для этого используются каталоги загружаемых данных. Естественно, если у какой то секции нет связанных каталогов данных, то и загрузка не выполняется.
Давайте рассмотрим небольшой пример
На иллюстрации показана загрузка данных для одной секции. Каталог загружаемых данных содержит две записи. Соответственно, в выделенную под секцию область памяти загружается две порции данных. Остальная часть области памяти (на иллюстрации показан серым цветом) остается незаполненной данными.
Какая часть области памяти заполняется загружаемыми данными и сколько порций будет загружаться определяется компоновщиком. Есть два крайних случая. Первый, когда загружаемых данных вообще нет, может использоваться, например, для секции стека. Второй, когда вся область памяти заполняется загружаемыми данными, может соответствовать, например, секции программного кода. А между этими двумя крайними случаями лежит множество промежуточных вариантов.
Разрешение ссылок на внешние символы
После окончания загрузки можно переходить к настройке задачи. Разумеется, если задача не использует разделяемые библиотеки, то данный шаг пропускается.
Ссылка на внешний символ это вызов процедуры находящейся за пределами задачи или обращение к ячейке памяти за пределами задачи. Внешний символ может располагаться или внутри ОС, или в разделяемой библиотеке. Для нас нет большой разницы между этими случаями. Но для определенности будем рассматривать нахождение символа в разделяемой библиотеке. При этом будем рассматривать только автоматическое управление библиотеками.
Вспомните каталог внешних ссылок. С каждым внешний символом связано имя библиотеки, в которой он располагается. Загрузчик должен проверить каждую библиотеку из каталога. Если библиотека еще не загружена, то загрузчик приостанавливает работу и запускает новый процесс загрузчика, который выполнит загрузку библиотеки. После этого наш загрузчик возобновит работу.
После того, как все библиотеки загружены, можно начинать разрешение внешних ссылок и связывание. И здесь у загрузчика есть два варианта.
- Отображение адресного пространства библиотеки на адресное пространство задачи.
Обратите внимание, что такое отображение никак не влияет на размер выделенного задаче блока памяти. В большинстве случаев размер доступного адресного пространства задачи превышает размер выделенного физического блока памяти. И мы можем отобразить в это адресное пространство какие то дополнительные блоки памяти. И эти блоки памяти не утратят своей самостоятельности, просто будут доступны из нескольких задач.
С точки зрения задачи, библиотека становится ее частью. С точки зрения библиотеки, она остается независимой и может даже не знать об этом отображении.
- Обращение к библиотеке выполняется как обращение к самостоятельному процессу.
При этом отображение на адресное пространство задачи не выполняется. Обращение к внешнему символу выполняется как обращение к отдельному самостоятельному процессу - другой задаче. Как правило, такое обращение приводит к переключению задач и выполняется через специальный шлюз (программный).
Это случай соответствует к запросу процесса-клиента (нашей задачи) к процессу-серверу для получения обслуживания. Через вызов процедуры. Переключение задач мы сегодня рассматривать не будем.
Когда то давно я написал коротенькую статью Поддержка концепции задачи в процессорах Intel 80x86. Если вам интересно, можете почитать, как выглядит процесс переключения задач для одного из самых популярных семейств процессоров.
А мы возвращаемся к отображению библиотеки в адресное пространство задачи.
Когда все необходимые библиотеки отображены на адресное пространство задачи, можно приступать к собственно связыванию. Для этого загрузчик последовательно просматривает каталог внешних ссылок и выполняет поиск перечисленных там символов. Для каждого символа поиск позволяет определить базовый адрес библиотеки и смещение символа внутри библиотеки. То есть, его полный адрес.
Этот адрес помещается на соответствующее место в области памяти секции
По сути, работа загрузчика при этом схожа с работой компоновщика при обработке ссылок на внешние символы в объектных файлах.
Обратите внимание, что разделяемая библиотека загружена в собственный блок памяти и имеет собственное адресное пространство и свой базовый адрес размещения в памяти. Ее отображение в адресное пространство задачи получает дополнительный, алиасный, базовый адрес. При этом смещение до символа от базового адреса остается неизменным.
С точки зрения задачи это выглядит как перемещение секций библиотеки внутри адресного пространства задачи. Именно выглядит, а не является. Так как никакого физического перемещения данных не происходит.
После завершения связывания и "перемещения" настройка задачи в памяти машины завершена. Но это еще не все.
Настройка подсистемы виртуальной памяти
Имея полностью настроенную задачу в памяти нам еще нужно выполнить настройку подсистемы виртуальной памяти. И это тоже работа загрузчика.
Для каждой области памяти секции подсистеме виртуальной памяти передается информация об адресах в памяти и размере, атрибуты секции (перемещаемость, сбрасываемость). Для секций кода и констант так же передается информация о расположении секций в образе задачи на внешнем накопителе. Для секций данных подсистема виртуальной памяти выполняет распределение пространства свопа.
Теперь у нас настроено все, что нужно. И задача в памяти, и подсистема виртуальной памяти. Информация о задаче передана в ОС. Задача готова к запуску.
Запуск задачи на выполнение
Наконец то долгожданный момент! Загрузчик проделал огромную работу, но усилия того стоили. Теперь можно передать задачу операционной система, которая включит ее в список задач планировщика.
С этого момента задача начинает, наконец то, свое выполнение.
Различие между задачей и разделяемой библиотекой
Работа загрузчика и для задачи (программы), и для драйвера, и для модуля ядра, и для разделяемой библиотеки практически одинакова. Да, есть нюансы, но в целом все очень похоже. Но если задача после загрузки запускается на выполнение, то для библиотеки этого не требуется. Драйверы и ядро сегодня не будем рассматривать.
Тем не менее, библиотека может потребовать дополнительной инициализации. Инициализация может выполняться или специальной процедурой, которую программист предусмотрел для библиотеки, или передачей библиотеке управления, если она по сути является процессом-сервером. Процедура инициализации может выполняться и вызовом этой процедуры загрузчиком.
Кроме того, библиотека имеет в памяти дополнительный информационный блок. Тот самый каталог экспортируемых символов, который мы ранее уже упоминали. Именно в этом каталоге впоследствии и будет вести поиск загрузчик при выполнении связывания.
Заключение
Мы очень кратко и очень упрощенно рассмотрели работу связывающего перемещающего загрузчика, который выполняет загрузку и подготовку программ к запуску при работе ОС. Многие аспекты работы загрузчика и форматов образов задач сегодня остались "за кадром".
И все равно, статья получилась довольно большой и не самой простой для восприятия. Вопреки ожиданиям, связывающий загрузчик оказался куда сложнее начального загрузчика, который мы рассматривали в предыдущей статье.
Начальный загрузчик работает в условиях неопределенности, врукопашную общается с внешними устройствами, но его функция проста. Связывающий загрузчик, при всей мощной поддержке со стороны ОС, работает с гораздо более сложными структурами и должен учитывать массу не самых простых нюансов работы ОС. Это и делает его таким сложным.
Я понимаю, что работа загрузчика будет интересна немногим читателям. Как немногим были интересны статьи про компоновщик. Тем не менее, вопросы связанные с работой загрузчика задают не так редко. И эти вопросы не такие простые, как не простые ответы на них.
И сегодняшняя статья это попытка описать работу связывающего перемещающего загрузчика достаточно простым языком. В следующей, последней статье цикла, мы кратко рассмотрим загрузчики для микроконтроллеров. Будет интересно!
Приглашаю читателей посетить дружественный канал