Ассемблер и ядро Операционной Системы
Распространённое заблуждение — ядро Операционной системы, пишется на ассемблере. Сегодня я займусь экспериментом, демонстрирующим, что это не так.
Прошлый раз я экспериментировал с загрузчиком. В загрузчик помещалась тестовая программа и тестовая программа запускалась. Это было самое простое, что можно было запрограммировать без Операционной Системы. Теперь моя цель сделать следующий шаг. Собрать тестовое ядро Операционной Системы и загрузить его загрузчиком.
Ядро как и всё остальное на компьютере — программа. Но в отличии от всех остальных программ ядро должно всё делать само. Нет кроме BIOS ничего, что могло бы использоваться ядром как библиотека функций. (И BIOS не во всех режимах). Еще одно отличие ядра от обычных программ состоит в способе загрузки в память. Обычную программу, чтобы программа могла работать, не просто копируют в память, а настраивают процессор таким образом, создавая виртуальное адресное пространство, чтобы программа могла работать в любом месте памяти. Это делает ядро. Ядро загружается Boot Loader-ом. В 512 байтный Boot Loader поместить еще и операции по настройке страничного механизма для привязки ядра к адресам памяти скорее всего невозможно. Поэтому ядро должно загружаться в память по определенному адресу. Проще всего написать программу, загружающуюся по определенному адресу, на ассемблере. Но это не продуктивно.
Построение компьютерных программ давно состоит из 2-х этапов. Этап компиляции и этап линковки. На этапе компиляции получаются объектные файлы, содержащие скомпилированные функции с описанием точек входа (адресов функций), адресов переменных, внешних имен функций, на которые ссылается код из объектного файла, и так далее.
Компьютерные программы представляют из себя файл на диске. Чтобы файл стал процессом, выполняемым процессором, программа должна быть загружена из файла в память. Это делает загрузчик выполнимого файла. (Не путать с загрузчиком операционной системы). У Операционных систем форматы выполнимых файлов разные, соответственно и загрузчики выполнимых файлов разные.
Второй этап в процессе “изготовления” программ — линковка. Линковщик берет объектные файлы и собирает из них выполнимый файл в определённом формате, подходящем для загрузчика выполнимых файлов.
Таким образом, имеем следующие сущности:
- Форматы выполнимых файлов
- Форматы объектных файлов
Форматы объектных файлов — это то, что получается на выходе из компиляторов.
Форматы выполнимых файлов — это то, что поддерживается загрузчиками выполнимых файлов. И есть ещё один формат. Т.н. binary. Это просто последовательность команд процессора в голом виде. (Самый простой формат ядра операционной системы). В тоже время форматы выполнимых файлов — это выходные форматы из линкера.
Один из линкеров, поддерживающий различные форматы объектных файлов и выполнимых файлов — ld (линкер из пакета GNU binutils). ld поставляется в комплекте с разными компиляторами. В том числе с gcc. Именно с помощью ld производится линковка ядра линукса и многих других операционных систем. Кроме сборки перемещаемых форматов, таких как elf или Windows PE, ld умеет собирать, в том числе и raw binary выполнимые файлы.
Как это делается? По-умолчанию, линкеру указывается в командной строке список объектных файлов, линкер прилинковывает их к специальному объектному файлу, вызывающему функцию _main и формирует файл нужного формата.
ld может также использовать для работы специальный скрипт, позволяющий гибко настраивать как создавать выполнимый файл. При работе со скриптом легко полностью избежать использования скрытых объектных файлов, стандартных библиотек и так далее. При работе со скриптовым файлом линкер будет использовать только то, что указано в скриптовом файле. Никаких файлов и библиотек по умолчанию. Имя скрипта передается в параметре -T. Например команда слинкует ядро из двух объектных файлов и скрипта: $ ld -Tlink.ld -o kernel.bin start.o second.o
Теперь о линкерном скрипте несколько подробнее. Пример скрипта взят с osdever:
Ядро в данном случае будет загружаться по адресу со смещением в 1M относительно начала памяти компьютера. Описываются переменные code, data, bss, end. Задается выравнивание всех сегментов на границу страницы памяти (4096 байт). В качестве стартовой берётся функция с именем start. Функция взята из примера: http://www.osdever.net/bkerndev/Docs/basickernel.htm
Приведенный пример удивляет адресом загрузки. Как в real mode можно загрузить ядро по физическому адресу за пределами первого мегабайта? Ответ: в real mode никак. Пример этот рассчитан на использование GRUB, работающего в защищенном режиме.
Подведу итоги про ld.
Существует множество версий gcc и binutils. Они поддерживают самые разные форматы объектных файлов. В том числе есть версии binutils скомпилированные под Windows. Это позволяет разрабатывать ядро Операционной системы с использованием любого подходящего для разработчика компилятора. Главное, чтобы в пару к компилятору была соответствующая версия binutils, поддерживающая нужную версию объектных и выполнимых (тоже объектных) файлов.
Дополнительные примеры линкерного скрипта можно посмотртеть в статье: Настройка виртуальных адресов внутри ядра
UEFI
P.S. Есть ещё один аргумент против ассемблера… Как известно, загрузкой операционной системы после включения компьютера занимается BIOS. BIOS — Basic Input Output System. А в целом, API для работы с аппаратурой, представляет из себя набор прерываний, с передачей данные через регистры, а также набор портов ввода-вывода и memory mapped областей памяти. Появилась такая система в момент разработки первых компьютеров IBM/PC еще в 80-е годы прошлого века. Архитектура IBM PC Compatible систем медленно эволюционировала. Появлялись новые системные шины и стандарты: ISA, EISA, PCI, и т.д. При этом BIOS оставался частной собственностью нескольких компаний. BIOS предоставляет интерфейсы программам только в реальном режиме, интерфейсы BIOS устарели, несли бремя совместимости и ограничены в возможностях. Такая ситуация стала тормозить развитие фантазии разработчиков аппаратных средств. При разработке платформы Itanium, Intel выступил с инициативой EFI (Extendible Firmware Interface). Идея EFI заключалась в замене BIOS новой разработкой. EFI, в отличии от BIOS больше не базируется на прерываниях. EFI больше не написан на ассемблере, и предоставляет для использования таблицы функций разного назначения написанных на Cи. Естественно процесс продвижения новой технологии долгий. Прошло несколько лет и после версии EFI 1.10 Intel передал права на дальнейшее развитие нового стандарта ново созданной инициативной группе UEFI, включающей в себя представителей Intel, IBM, Apple, Microsoft и других. Теперь эта технология получила приставку Unified, означающую, что единые переносимые firmware получат как компьютеры на базе Intel PC, так и планшетники на базе ARM. UEFI, по сравнению с BIOS несомненно прогрессивен. Он предоставляет графические драйвера, сетевые драйвера и т.д. Компьютер без операционной системы будет способен осуществлять в том числе сетевые подключения… Однако есть у этого всего и минусы. Одной из технологий, поддерживаемой UEFI является Safe boot. Запрет загрузки не подписанного софта. Это означает, что возможно создание железа, на котором будет загружаться лишь одна разрешенная (подписанная) операционная система.
Самым недавним ударом по BIOS стало требование Microsoft во всех системах, сертифицированных для работы с Windows 8 наличие UEFI safe boot.
Что из вышесказанного следует? Из вышесказанного следует, что платформе Intel PC в том её виде, в котором она существует сегодня, жить осталось недолго. Выход Windows 8 не за горами. Второй вывод — ассемблер перестает быть инструментом, даже при написании операционных систем. С появлением новых интерфейсов, необходимость в ассемблере, судя по всему уменьшится. Третий вывод — загрузка операционной системы, да и архитектура драйверов в ближайшее время переживет революционные изменения.