NetBSD известна как хорошо переносимая операционная система, которая в настоящее время запускается на 44 различных архитектурах (на 12 различных типах процессоров). В этом документе объясняется, каким образом была достигнута такая переносимость и как она позволила упростить перено NetBSD на новую архитектуру. В качестве примера используется новая архитектура AMD x86-64, спецификации которой были опубликованы в конце 2000 года и оборудование с реализацией которой появилось в 2002 году.
Поддержка множества платформ была главной целью проекта NetBSD с самого начала. По мере переноса NetBSD на всё большее количество платформ, код ядра NetBSD изменялся так, чтобы становиться ещё более переносимым.
В первую очередь в переносах обобщается как можно больше кода. В NetBSD всегда нужно учитывать, что код может использоваться на других архитектурах, настоящих или будущих. Такой код является машинно-независимым и помещается в соответсвующее место в дереве исходного кода. Если же код задуман как машинно-независимый, но содержит условные выражения препроцессора, зависящие от архитектуры, то скорее всего это неправильный код и для избавления от этих выражений потребуется дополнительный уровень абстракции.
Предположения о размере любого типа не делаются. Предположения о размерах типов 32-битной платформы были большой проблемой при появлении 64-битных платформ. Большая часть таких проблем была устранена при переносе NetBSD на DEC Alpha в 1994 году. Разновидность этой проблемы была устранена при переносе на UltraSPARC (sparc64) в 1998 году, которая была 64-битной, но с порядком байт от старшего к младшему (по сравнению с Alpha, где использовался порядок байт от младшего к старшему). При взаимодействии со структурами данных фиксированного размера, такими как метаданные файловых систем на дисках, или со структурами данных, с которыми работает оборудование, используются типы явного размера, такие как uint32_t, int8_t и т.п.
Изначально BSD была написана в расчёте только на одну целевую платформу (PDP11, позднее - VAX). Позже был добавлен код для других платформ и 4.4BSD содержала код для 4 платформ. NetBSD основана на 4.4BSD, но с каждым годом количество поддерживаемых платформ неуклонно увеличивалось. По мере добавления платформ становилось очевидным, что многие из них используют одинаковые устройства, отличающиеся только низкоуровневыми методами доступа к регистрам устройства и методами прямого доступа в память. Это привело, например к тому, что 5 разных переносов имели 5 разных драйверов последовательного порта, содержащих почти идентичный код. Если учесть, что перенос на новое оборудование происходит каждые несколько месяцев, становится очевидным, что это неприемлемая ситуация.
Для исправления сложившейся ситуации были созданы слои bus_dma и bus_space [1], [5]. Слой bus_space отвечает за доступ к пространству ввода-вывода устройства, а слой bus_dma отвечет за прямой доступ в память. При каждом переносе NetBSD на новую архитектуру нужны реализации интерфейсов для каждой шины ввода-вывода, используемой этим компьютером. Если реализации есть, то все драйверы устройств, которые подключаются к такой шине ввода-вывода, должны скомпилироваться и заработать без каких-либо дополнительных усилий.
Конечно, не весь код удаётся обобщить. Некоторые части кода имеют дело с платформно-зависимым оборудованием или просто используют инструкции компьютера, которые нельзя сгенерировать компилятором. Некоторые утилиты пользовательского пространства также специфичны для платформы. Ниже перечислены наиболее важные машинно-зависимые части NetBSD, которые потребуются для переноса NetBSD на новую платформу.
Первое и наиболее важное: для переноса операционной системы на целевую платформу понадобится работающий набор инструментов для кросс-сборки (компилятор, ассемблер, компоновщик и т.п.). Набор инструментов GNU toolchain стал стандартом де-факто для открытых операционных систем и NetBSD не является исключением из этого правила. Поскольку двоичный формат ELF используется почти всеми новыми переносами NetBSD (и должен использоваться любым новым переносом), как и другими операционными системами, таким как Linux, то для подготовки GNU toolchain к работе с NetBSD обычно требуется всего-лишь создать и измененить несколько файлов конфигурации. Исключением может быть только отсутствие поддержки нужного процессора. В таком случае потребуется гораздо больше усилий.
Код загрузки загружает образ ядра в оперативную память и запускает его. Для загрузки образа он взаимодействует с прошивкой. Сложность написания кода загрузки сильно зависит от функциональности, предоставляемой прошивкой. Часто прошивка обладает ограниченными возможностями, позволяющими лишь загрузить вторичный загрузчик, содержащий более сложный код, умеющий работать с файловой системой, на которой может располагаться образ ядра, и с форматами файла ядра.
Очевидно, ловушки и прерывания обрабатываются машинно-зависимой частью ядра. По меньшей мере входные точки для них должны быть машинно-зависимым кодом, который сохраняет и восстанавливает регистры процессора. Кроме того, процессор может иметь разные типы ловушек, требующих разной обработки.
Блоки управления памятью (MMU) как правило сильно отличаются от процессора к процессору. Они могут отличаться даже в рамках одного семейства процессоров (например, серия PowerPC 4xx имеет блок управления памятью, который значительно отличается от такового у серии 6xx). Они также могут сильно отличаться по аппаратным возможностям. Например, блоки управления памятью могут иметь фиксированную структуру таблицы странц, которую они могут обходить аппаратно или оставлять многие операции на откуп программному обеспечению, передавая ему промахи буфера ассоциативной трансляции (TLB). Низкоуровневый код виртуальной памяти во всех системах, производных от 4.4BSD, называется модулем pmap. Его название происходит из операционной системы Mach, система виртуальной памяти которой использовалась в 4.4BSD.
На некоторых платформах бывают устройства, которые вряд ли встретятся на какой-либо другой платформе. Такими устройствами могут быть встроенные устройства последовательного порта и часы. Для переноса NetBSD на новую платформу часто бывает нужно написать драйвер по меньшей мере для одного такого устройства.
Как было написано выше, код устройства использует машинно-независимый интерфейс для операций ввода-вывода и прямого доступа в память. Отличия между платформами скрыты позади интерфейса, и реализации этого интерфейса, учитывающего особенности платформы. Этот интерфейс плотно используется в драйверах устройства, из-за чего важны высокая скорость и низкое потребление памяти. Часто этот интерфейс реализуется в виде набора макросов или встраиваемых функций.
В пользовательском пространстве машинно-зависимый код используется при инициализации Си и в некоторых библиотеках. В основном это интерфейсы к системным вызовам в библиотеке Си, оптимизированные функции для работы со строками в этой же библиотеке и обработка чисел с плавающей запятой в библиотеке math. Есть и более редко используемые библиотеки, например библиотека KVM для чтения памяти ядра. Наконец, от других платформ скорее всего будет отличаться и работа с разделяемыми библиотеками (способ перемещения).
Прежде чем перейти к частностям, для начала бегло ознакомимся с архитектурой AMD x86-64 [2]. Спецификации архитектуры AMD x86-64 (под кодовым названием Hammer - молот) были выпущены в конце 2000 года. В декабре 2001 года ещё не было её общедоступных аппаратных реализаций (NetBSD/x86-64 была разработана исключительно при помощи симулятора Simics x86-64, созданном VirtuTech). Поскольку x86-64 изначально была расширением архитектуры IA32 (или i386, как она известна в NetBSD), ниже также будет представлена и её предшественница.
Intel назвал 32-битную архитектуру IA32. Эта архитектура изначально была представлена в процессоре 80386 и в наши дни стала наиболее популярной архитектурой центральных процессоров, т.к. получила распространение в мире персональных компьютеров.
Процессоры IA32 обладают следующими особенностями [4]:
Архитектура x86-64 по сути является 64-битным расширением архитектуры IA32. К унаследованным "реальному" (16-битному) и "защищённому" (32-битному) режимам добавляется "длинный" (64-битный) режим. Реальный и защищённый режимы полностью совместимы с архитектурой IA32. Длинный режим позволяет запускать 32-битные двоичные файлы без изменений и содержит некоторое количество расширений. В этом документе, кроме особо оговорённых случаев, рассматривается только длинный режим.
Уже имеющиеся в архитектуре IA32 регистры общего назначения были расширены до 64 бит. Добавилось восемь регистров, так что их стало пятнадцать (не считая регистр esp, см. рисунок 2). Часто можно услышать жалобы на архитектуру IA32: в ней очень мало регистров общего назначения. Кроме того, было добавлено восемь дополнительных регистров SSE2. Для единообразия в инструкциях можно обратиться к младшим 16 или 8 битам всех регистров общего назначения, а не только к тем четырём, к которым так можно было обращаться в архитектуре IA32.
Вычисления и перемещения с использованием младших 16- и 8-битных частей регистров, в целях обратной совместимости, не влияют на сташрие биты. Однако 32-битные операции расширяются нулями. Для удобного использования 64-битных констант в инструкции перемещения был добавлен специальный промежуточный 64-битный регистр, значение из которого недоступно для других инструкций.
Представление о x86-64 как о расширении архитектуры IA32 также находит отражение в её блоке управления памятью (MMU). x86-64 обладает 64-битным виртуальным адресным пространством, однако в начальных реализациях архитектуры определено использование только 48 из этих 64 бит при 40 битах физического адресного пространства. Адреса расширяются знаковым битом, что приводит к невозможности обратиться к "дыре" в виртуальном адресном пространстве. Для того, чтобы блок управления памятью работал с 48-битным виртуальным адресным пространством, в дополнение к уровню, который уже был добавлен в реализации архитектуры IA32 для поддержки PAE (см. рисунок 3, прерывистая линия внутри виртуального адресного пространства отображает конец PAE), был добавлен дополнительный уровень таблицы страниц. В общем, схема таблицы страниц x86-64 - это схема PAE IA32 с 512 записями вместо четырёх в каталоге таблицы указателей и с четвёртым уровнем, который называется PML4. Эта схожесть настолько велика, что включение поддержки PAE является отдельным этапом переключения микропроцессора в длинный режим.
Среди других отличительных особенностей архитектуры x86-64 можно назвать следующие:
Обратившись к списку машинно-зависимых частей операционной системы в разделе 2, рассмотрим, что нужно сделать для переноса NetBSD на архитектуру x86-64.
Когда была начата работа над NetBSD/x86_64, в GNU toolchain уже имелась базовая поддержка x86-64, которая была добавлена разрабочиками SuSe, Inc для Linux. Поддержки разделяемых библиотек ещё не было, но компиляция и сборка приложений большей частью уже работала. Двоичный интерфейс приложений (ABI) также был определён [3]. Адаптация этого кода для NetBSD сводилась к простому изменению/созданию нескольких заголовочных файлов. На самом деле в независящих от операционной системы частях кода компилятора и компоновщика было несколько ошибок, но это было ожидаемо, т.к. код x86-64 был ещё слишком молодым.
Нужно учитывать некоторые особенности двоичного интерфейса приложений. Двоичный интерфейс приложений x86-64 определяет четыре модели позиционно-зависимого кода:
Есть похожие модели позиционно-независимого кода. По умолчанию программы в пользовательском пространстве используют "малую" модель кода. Во время работы над переносом другие модели кода пока ещё полностью не поддерживались, хотя после нескольких небольших изменений "огромная" модель стала стабильной и была использована при сборке ядра.
Поскольку настоящее оборудование x86-64 ещё не было доступно, а интерфейс прошивок для будущих компьютеров x86-64 ещё не был определён, код загрузки работал с тем, что предоставлял симулятор. Симулятор предоставлял обычный для персональных компьютеров интерфейс BIOS, поэтому код загрузчика NetBSD/i386 использовался почти в существующем виде. Однако в случае с x86-64 образ ядра загружался как 64-битный двоичный файл в формате ELF. Чтобы это сработало, потребовались простейшие изменения, т.к. код загрузки двоичных 64-битных файлов в формате ELF уже был машинно-независимым и находился в отдельной библиотеке, используемой загрузчиками NetBSD различных платформ.
Код инициализации в ядре (то есть первый код, исполняемый в ядре) для этого нового переноса NetBSD был написан буквально с нуля, хотя его можно рассматривать как расширенную версию кода из NetBSD/i386. Поскольку x86-64 после включения полностью совместим с IA32, нужно перевести центральный процессор из 16-битного режима в 32-битный режим, а затем, через несколько этапов, в 64-битный режим. Вот эти этапы:
Структура кода низкоуровневых ловушек и прерываний похожа на таковую из NetBSD/i386, но их код отличается. В x86-64 также используется таблица дескрипторов прерываний (IDT - Interrupt Descriptor Table) для настройки векторов для ловушек и прерываний. При входе в ловушку обычно нужно сохранить регистры, обработать ловушку, восстановить регистры. Несмотря на то, что высокоуровневый код ловушек можно использовать одновременно и в NetBSD/i386 и в NetBSD/x86-64, т.к. набор ловушек одинаков для обеих архитектур, это пока ещё не сделано.
Другой тип ловушек - это входные точки системных вызовов. x86-64 поддерживает те же механизмы, которые уже есть в архитектуре IA32: вход в ядро через программные прерывания или при помощи обращения к структуре особого типа, называемой шлюзом, которая выполняет автоматический переход в ядро. Эти инструкции производят некоторые действия, которые не нужны в случае плоского адресного пространства (когда всё виртуальное адресное пространство в 4 гигабайта доступно программе одним куском, в то время как обычно ядро занимает верхнюю область памяти). Инструкции SYSCALL и SYSRET оптимизированы для этого случая и могут использоваться для реализации более быстрого пути для системных вызовов, который может оказывать заметное влияние на производительность приложений. Код для них был написан, но ещё не был интегрирован. В настоящее время пока ещё используются входные точки в старом стиле, но в скором будущем это изменится.
Блок управления памятью x86-64 использует структуру таблицы страниц очень похожую на таковую из IA32, но с таблицей страниц расширенных физических адресов (PAE), дополненной до 4 уровней для работы с 48-битами виртуальной памяти, которой обладает начальное семейство процессоров x86-64. Из-за этой схожести был взят модуль pmap i386 и абстрагирован для реализации N-уровневой таблицы страниц IA32 с поддержкой как 32-битных, так и 64-битных записей. Получившийся код был успешно протестирован на обоих переносах NetBSD на x86-64 и на i386. Разница между кодом для архитектур IA32 и x86-64 была скрыта в макросах препроцессора Си и определениях типов. Преимущество этого подхода в том, что теперь подобающая поддержка PAE в NetBSD/i386 реализуется посредством условной компиляции нескольких макросов и определений типов.
Реализованная раскладка виртуальной памяти изображена на рисунке 4. Поскольку процессор выполняет знаковое расширение виртуальных адресов, между адресами 247 и 264 - 247 появляется неадресуемая область. Это не редкость, такую же область можно найти на процессорах SPARCv9 и Alpha. Раскладка памяти является в какой-то мере растянутой версией памяти IA32, что ожидаемо для совмещённого модуля pmap. Пользовательский процесс работает в нижней половине виртуальной памяти, в то время как ядро всегда находится в верхней половине. Часть верхней половины не используется, потому это могло бы привести к непропорциональному росту некоторых структур данных, а ядру не требуется большой общий объём виртуальной памяти. Верхняя часть нижней половины виртуальной памяти получается рекурсивным отображением страницы таблицы страниц, как и верхняя часть верхней половины виртуальной памяти (используется, если таблица страниц процесса отличается от той, на которую нужно переключиться).
В этой раскладке предполагается, что ядро находится за пределами "ядерной" модели кода, описанной в разделе о двоичном интерфейсе приложений. Потребовалась "огромная" модель, но она пока не поддерживается gcc. К счастью, её удалось включить при помощи нескольких небольших изменений. Ядро NetBSD/x86_64, скорее всего, будет переведено на "ядерную" модель двоичного интерфейса приложений, как только появится реальное оборудование и будет проведена оценка скорости. На данный момент используется эта раскладка, потому что она согласуется с расширением модели IA32 и позволяет упростить обобщение кода pmap.
Значительную часть кода реализации шины можно взять из переноса i386. В случае портов ввода-вывода необходимые инструкции совпадают, поэтому потребуется лишь небольшие изменения для того, чтобы они смогли работать с набором 64-битных регистров. То же относится и к вводу-выводу через отображаемую память. Для работы с 32-битной шиной PCI, которая не может обращаться к памяти по адресам выше 4 гигабайт, понадобится фреймворк для прямого доступа к памяти. На данный момент принято простое решение использовать с 32-битной шиной PCI для прямого доступа к памяти только адреса ниже 4 гигабайт. Позже это нужно будет пересмотреть, т.к. для компьютеров с оперативной памятью более 4 гигабайт доступная оперативная память может оказаться за пределами 4-гигабайтного предела. Для избежания этой проблемы можно воспользоваться буферами рикошета. Это промежуточные буферы для прямого доступа к памяти, данные из которых копируются в настоящее местоположение (или наоборот).
Пока перенос NetBSD x86-64 не работает с какими-либо специфичными для него устройствами. Симулятор симулирует некоторое количество компонентов оборудования, уже известного в мире персональных компьютеров (таких как мост узел-PCI, и т.п.). Эти компоненты не требуют доработки и "просто работают" после реализации слоёв bus_space и bus_dma.
Основная работа в пользовательском пространстве заключалась в переносе библиотек и кода инициализации Си. Перенос кода инициализации Си и библиотеки Си были почти тривиальными. Большая часть работы свелась к написанию переходников для системных вызовов и оптимизации строковых функций, с учётом того, что большинство аргументов в двоичном интерфейсе приложений x86-64 передаются через регистры, а не через стек, как принято в двоичном интерфейсе приложений i386. Над библиотекой math поработать пришлось больше. В ней можно обобщить часть кода i386 (а на самом деле i387), т.к. модуль обработки чисел с плавающей запятой имеет те же инструкции, но двоичный интерфейс приложений отличается. В двоичном интерфейсе приложений x86-64 аргументы с плавающей запятой передаются через регистры SSE, в то время как в двоичном интерфейсе i386 они передаются через стек. Для извлечения и подготовки аргументов было написано несколько макросов для различных (в основном тригонометрических) функций, а затем общая часть кода была использована в переносах i386 и x86-64. Наконец, динамическая компоновка была адаптирована для работы с типами, используемыми в разделяемых библиотеках x86-64.
Архитектура x86-64 предоставляет возможноть запуска 32-битных приложений i386 без изменений. Это полезная возможность и она используется операционными системами для запуска старых приложений из коробки. Однако её поддержка должна быть реализована в ядре. Главное, что нужно для запуска 32-битных приложений - это установка сегментов памяти для 32-битной совместимости в различных таблицах дескрипторов процессора. Процессор будет выполнять инструкции из этого сегмента в 32-битном окружении. Однако при отправке ловушек процессор будет переключаться в 64-битный режим. Преимущество в том, что в ядре не нужен какой-то особый код входа/выхода для 32-битных приложений. 32-битные программы имеют другой интерфейс к ядру, они передают аргументы в системные вызовы другим способом и с размерами, кратными 32 битам. Также структуры, передаваемые в ядро (или даже указатели на них) имеют другое выравнивание. Нужно учитывать эти особенности при реализации возможности запуска старых двоичных файлов NetBSD/i386.
Код совместимости для запуска двоичных файлов различных платформ (Linux, Tru64, Solaris и т.д.) долгое время был частью NetBSD. Поэтому не удивительно, что эти проблемы уже были решены ранее, когда в NetBSD/sparc64 потребовалось запускать двоичные файлы 32-битных SPARC. Код модуля compat_netbsd32 реализует маленький слой, который транслирует (при необходимости) 32-битные аргументы системных вызовов в их 64-битные аналоги.
Перенос NetBSD на архитектуру AMD x86-64 был выполнен за шесть недель, что подтвердило репутацию NetBSD как очень переносимой операционной системы. Одна неделя ушла на настройку кросс-платформенного набора интструментов и чтение спецификаций x86-64, три недели были потрачены на написание кода ядра, одна неделя была потрачена на написание кода пользовательского пространства и одна неделя - на тестирование и отладку всего этого. Во время тестовых запусков не было обнаржуено никаких проблем в машинно-независимых частях ядра, все (симулированные) драйверы устройств, файловые системы и т.п. заработали без каких-либо изменений.
Перенос прошёл гладко. В таблице 1 показано общее количество новых написанных строк кода.
Часть дерева | Строк на ассемблере | Строк на Си |
---|---|---|
libc | 310 | 2772 |
Код инициализации Си | 0 | 104 |
libm | 52 | 0 |
Ядро | 3314 | 17392 |
Динамический компоновщик | 59 | 172 |
Ещё есть код поддержки процессоров (таких как x86-64 и IA32), который можно было бы обобщить. В настоящее время pmap x86-64 реализован как самостоятельный, но может использоваться в обеих архитектурах. Код pmap в таблице 1 помечен как новый, но более 3500 его строк в той или иной мере основаны на коде i386. Часть кода таблицы дескрипторов тоже общая. После соответствующего обобщения кода можно будет уменьшить количество новых строк на Си до менее чем 10000.
Эта работа была оплачена моим нанимателем, Wasabi Systems, Inc. Я также хочу поблагодарить AMD за их поддержку, Virtutech за симулятор и людей из SuSe за работу над набором инструментов для Linux.
Автор перевода на русский язык: Владимир Ступин
←