Фрэнк ван дер Линден. Перенос NetBSD на AMD x86-641: тематическое исследование переносимости операционной системы, 2002

Введение

NetBSD известна как хорошо переносимая операционная система, которая в настоящее время запускается на 44 различных архитектурах (на 12 различных типах процессоров). В этом документе объясняется, каким образом была достигнута такая переносимость и как она позволила упростить перено NetBSD на новую архитектуру. В качестве примера используется новая архитектура AMD x86-64, спецификации которой были опубликованы в конце 2000 года и оборудование с реализацией которой появилось в 2002 году.

1 Переносимость

Поддержка множества платформ была главной целью проекта NetBSD с самого начала. По мере переноса NetBSD на всё большее количество платформ, код ядра NetBSD изменялся так, чтобы становиться ещё более переносимым.

1.1 Основное

В первую очередь в переносах обобщается как можно больше кода. В NetBSD всегда нужно учитывать, что код может использоваться на других архитектурах, настоящих или будущих. Такой код является машинно-независимым и помещается в соответсвующее место в дереве исходного кода. Если же код задуман как машинно-независимый, но содержит условные выражения препроцессора, зависящие от архитектуры, то скорее всего это неправильный код и для избавления от этих выражений потребуется дополнительный уровень абстракции.

1.2 Типы

Предположения о размере любого типа не делаются. Предположения о размерах типов 32-битной платформы были большой проблемой при появлении 64-битных платформ. Большая часть таких проблем была устранена при переносе NetBSD на DEC Alpha в 1994 году. Разновидность этой проблемы была устранена при переносе на UltraSPARC (sparc64) в 1998 году, которая была 64-битной, но с порядком байт от старшего к младшему (по сравнению с Alpha, где использовался порядок байт от младшего к старшему). При взаимодействии со структурами данных фиксированного размера, такими как метаданные файловых систем на дисках, или со структурами данных, с которыми работает оборудование, используются типы явного размера, такие как uint32_t, int8_t и т.п.

1.3 Драйверы устройств

Изначально BSD была написана в расчёте только на одну целевую платформу (PDP11, позднее - VAX). Позже был добавлен код для других платформ и 4.4BSD содержала код для 4 платформ. NetBSD основана на 4.4BSD, но с каждым годом количество поддерживаемых платформ неуклонно увеличивалось. По мере добавления платформ становилось очевидным, что многие из них используют одинаковые устройства, отличающиеся только низкоуровневыми методами доступа к регистрам устройства и методами прямого доступа в память. Это привело, например к тому, что 5 разных переносов имели 5 разных драйверов последовательного порта, содержащих почти идентичный код. Если учесть, что перенос на новое оборудование происходит каждые несколько месяцев, становится очевидным, что это неприемлемая ситуация.

Для исправления сложившейся ситуации были созданы слои bus_dma и bus_space [1], [5]. Слой bus_space отвечает за доступ к пространству ввода-вывода устройства, а слой bus_dma отвечет за прямой доступ в память. При каждом переносе NetBSD на новую архитектуру нужны реализации интерфейсов для каждой шины ввода-вывода, используемой этим компьютером. Если реализации есть, то все драйверы устройств, которые подключаются к такой шине ввода-вывода, должны скомпилироваться и заработать без каких-либо дополнительных усилий.

2 Машинно-зависимые части

Конечно, не весь код удаётся обобщить. Некоторые части кода имеют дело с платформно-зависимым оборудованием или просто используют инструкции компьютера, которые нельзя сгенерировать компилятором. Некоторые утилиты пользовательского пространства также специфичны для платформы. Ниже перечислены наиболее важные машинно-зависимые части NetBSD, которые потребуются для переноса NetBSD на новую платформу.

2.1 Набор инструментов

Первое и наиболее важное: для переноса операционной системы на целевую платформу понадобится работающий набор инструментов для кросс-сборки (компилятор, ассемблер, компоновщик и т.п.). Набор инструментов GNU toolchain стал стандартом де-факто для открытых операционных систем и NetBSD не является исключением из этого правила. Поскольку двоичный формат ELF используется почти всеми новыми переносами NetBSD (и должен использоваться любым новым переносом), как и другими операционными системами, таким как Linux, то для подготовки GNU toolchain к работе с NetBSD обычно требуется всего-лишь создать и измененить несколько файлов конфигурации. Исключением может быть только отсутствие поддержки нужного процессора. В таком случае потребуется гораздо больше усилий.

2.2 Код загрузки

Код загрузки загружает образ ядра в оперативную память и запускает его. Для загрузки образа он взаимодействует с прошивкой. Сложность написания кода загрузки сильно зависит от функциональности, предоставляемой прошивкой. Часто прошивка обладает ограниченными возможностями, позволяющими лишь загрузить вторичный загрузчик, содержащий более сложный код, умеющий работать с файловой системой, на которой может располагаться образ ядра, и с форматами файла ядра.

2.3 Ловушки и прерывания

Очевидно, ловушки и прерывания обрабатываются машинно-зависимой частью ядра. По меньшей мере входные точки для них должны быть машинно-зависимым кодом, который сохраняет и восстанавливает регистры процессора. Кроме того, процессор может иметь разные типы ловушек, требующих разной обработки.

2.4 Низкоуровневая работа с виртуальной памятью / блоком управления памятью

Блоки управления памятью (MMU) как правило сильно отличаются от процессора к процессору. Они могут отличаться даже в рамках одного семейства процессоров (например, серия PowerPC 4xx имеет блок управления памятью, который значительно отличается от такового у серии 6xx). Они также могут сильно отличаться по аппаратным возможностям. Например, блоки управления памятью могут иметь фиксированную структуру таблицы странц, которую они могут обходить аппаратно или оставлять многие операции на откуп программному обеспечению, передавая ему промахи буфера ассоциативной трансляции (TLB). Низкоуровневый код виртуальной памяти во всех системах, производных от 4.4BSD, называется модулем pmap. Его название происходит из операционной системы Mach, система виртуальной памяти которой использовалась в 4.4BSD.

2.5 Устройства, специфичные для переноса

На некоторых платформах бывают устройства, которые вряд ли встретятся на какой-либо другой платформе. Такими устройствами могут быть встроенные устройства последовательного порта и часы. Для переноса NetBSD на новую платформу часто бывает нужно написать драйвер по меньшей мере для одного такого устройства.

2.6 Код реализации шины

Как было написано выше, код устройства использует машинно-независимый интерфейс для операций ввода-вывода и прямого доступа в память. Отличия между платформами скрыты позади интерфейса, и реализации этого интерфейса, учитывающего особенности платформы. Этот интерфейс плотно используется в драйверах устройства, из-за чего важны высокая скорость и низкое потребление памяти. Часто этот интерфейс реализуется в виде набора макросов или встраиваемых функций.

2.7 Библиотеки

В пользовательском пространстве машинно-зависимый код используется при инициализации Си и в некоторых библиотеках. В основном это интерфейсы к системным вызовам в библиотеке Си, оптимизированные функции для работы со строками в этой же библиотеке и обработка чисел с плавающей запятой в библиотеке math. Есть и более редко используемые библиотеки, например библиотека KVM для чтения памяти ядра. Наконец, от других платформ скорее всего будет отличаться и работа с разделяемыми библиотеками (способ перемещения).

3 Оборудование x86-64

Прежде чем перейти к частностям, для начала бегло ознакомимся с архитектурой AMD x86-64 [2]. Спецификации архитектуры AMD x86-64 (под кодовым названием Hammer - молот) были выпущены в конце 2000 года. В декабре 2001 года ещё не было её общедоступных аппаратных реализаций (NetBSD/x86-64 была разработана исключительно при помощи симулятора Simics x86-64, созданном VirtuTech). Поскольку x86-64 изначально была расширением архитектуры IA32 (или i386, как она известна в NetBSD), ниже также будет представлена и её предшественница.

Рисунок 1: Трансляция виртуальных адресов IA32 (размер страницы - 4 килобайта). Записи в таблице страниц и каталоге страниц имеют размер 32 бита.

3.1 Архитектура IA32

Intel назвал 32-битную архитектуру IA32. Эта архитектура изначально была представлена в процессоре 80386 и в наши дни стала наиболее популярной архитектурой центральных процессоров, т.к. получила распространение в мире персональных компьютеров.

Процессоры IA32 обладают следующими особенностями [4]:

3.2 Основные расширения x86-64

Архитектура x86-64 по сути является 64-битным расширением архитектуры IA32. К унаследованным "реальному" (16-битному) и "защищённому" (32-битному) режимам добавляется "длинный" (64-битный) режим. Реальный и защищённый режимы полностью совместимы с архитектурой IA32. Длинный режим позволяет запускать 32-битные двоичные файлы без изменений и содержит некоторое количество расширений. В этом документе, кроме особо оговорённых случаев, рассматривается только длинный режим.

3.3 Регистры

Уже имеющиеся в архитектуре IA32 регистры общего назначения были расширены до 64 бит. Добавилось восемь регистров, так что их стало пятнадцать (не считая регистр esp, см. рисунок 2). Часто можно услышать жалобы на архитектуру IA32: в ней очень мало регистров общего назначения. Кроме того, было добавлено восемь дополнительных регистров SSE2. Для единообразия в инструкциях можно обратиться к младшим 16 или 8 битам всех регистров общего назначения, а не только к тем четырём, к которым так можно было обращаться в архитектуре IA32.

Вычисления и перемещения с использованием младших 16- и 8-битных частей регистров, в целях обратной совместимости, не влияют на сташрие биты. Однако 32-битные операции расширяются нулями. Для удобного использования 64-битных констант в инструкции перемещения был добавлен специальный промежуточный 64-битный регистр, значение из которого недоступно для других инструкций.

Рисунок 2: Регистры x86-64. Регистры, совместимые с IA32, обозначены курсивом.

3.4 Управление памятью

Представление о x86-64 как о расширении архитектуры IA32 также находит отражение в её блоке управления памятью (MMU). x86-64 обладает 64-битным виртуальным адресным пространством, однако в начальных реализациях архитектуры определено использование только 48 из этих 64 бит при 40 битах физического адресного пространства. Адреса расширяются знаковым битом, что приводит к невозможности обратиться к "дыре" в виртуальном адресном пространстве. Для того, чтобы блок управления памятью работал с 48-битным виртуальным адресным пространством, в дополнение к уровню, который уже был добавлен в реализации архитектуры IA32 для поддержки PAE (см. рисунок 3, прерывистая линия внутри виртуального адресного пространства отображает конец PAE), был добавлен дополнительный уровень таблицы страниц. В общем, схема таблицы страниц x86-64 - это схема PAE IA32 с 512 записями вместо четырёх в каталоге таблицы указателей и с четвёртым уровнем, который называется PML4. Эта схожесть настолько велика, что включение поддержки PAE является отдельным этапом переключения микропроцессора в длинный режим.

Рисунок 3: Трансляция виртуальных адресов в архитектуре x86-64 (со страницами размером 4096 байт). Записи в структуре таблицы страниц имеют размер 64 бита.

3.5 Прочее

Среди других отличительных особенностей архитектуры x86-64 можно назвать следующие:

4 Сам перенос

Обратившись к списку машинно-зависимых частей операционной системы в разделе 2, рассмотрим, что нужно сделать для переноса NetBSD на архитектуру x86-64.

4.1 Набор инструментов

Когда была начата работа над NetBSD/x86_64, в GNU toolchain уже имелась базовая поддержка x86-64, которая была добавлена разрабочиками SuSe, Inc для Linux. Поддержки разделяемых библиотек ещё не было, но компиляция и сборка приложений большей частью уже работала. Двоичный интерфейс приложений (ABI) также был определён [3]. Адаптация этого кода для NetBSD сводилась к простому изменению/созданию нескольких заголовочных файлов. На самом деле в независящих от операционной системы частях кода компилятора и компоновщика было несколько ошибок, но это было ожидаемо, т.к. код x86-64 был ещё слишком молодым.

Нужно учитывать некоторые особенности двоичного интерфейса приложений. Двоичный интерфейс приложений x86-64 определяет четыре модели позиционно-зависимого кода:

Есть похожие модели позиционно-независимого кода. По умолчанию программы в пользовательском пространстве используют "малую" модель кода. Во время работы над переносом другие модели кода пока ещё полностью не поддерживались, хотя после нескольких небольших изменений "огромная" модель стала стабильной и была использована при сборке ядра.

4.2 Код загрузчика и инициализации

Поскольку настоящее оборудование x86-64 ещё не было доступно, а интерфейс прошивок для будущих компьютеров x86-64 ещё не был определён, код загрузки работал с тем, что предоставлял симулятор. Симулятор предоставлял обычный для персональных компьютеров интерфейс BIOS, поэтому код загрузчика NetBSD/i386 использовался почти в существующем виде. Однако в случае с x86-64 образ ядра загружался как 64-битный двоичный файл в формате ELF. Чтобы это сработало, потребовались простейшие изменения, т.к. код загрузки двоичных 64-битных файлов в формате ELF уже был машинно-независимым и находился в отдельной библиотеке, используемой загрузчиками NetBSD различных платформ.

Код инициализации в ядре (то есть первый код, исполняемый в ядре) для этого нового переноса NetBSD был написан буквально с нуля, хотя его можно рассматривать как расширенную версию кода из NetBSD/i386. Поскольку x86-64 после включения полностью совместим с IA32, нужно перевести центральный процессор из 16-битного режима в 32-битный режим, а затем, через несколько этапов, в 64-битный режим. Вот эти этапы:

  1. Включение расширений физических адресов (PAE).
  2. Установка бита длинного режима (LME - Long Mode Enable) в регистре EFER.
  3. Запись в регистр %cr3 адреса заранее подготовленной начальной четырёхуровневой структуры таблицы страниц.
  4. Включение страничного режима.
  5. Теперь центральный процессор работает в 32-битном сегменте совместимости. Подготовка временной таблицы глобальных дескрипторов (Global Descriptor Table) с сегментом памяти длинного режима и переход в этот сегмент.
  6. Ядро не было доступно из 32-битной области, поскольку находилось в верхней области памяти. После переключения в длинный режим оно оказывается в досягаемости инструкции перехода. Для запуска кода самого ядра остаётся только выполнить этот переход.

4.3 Ловушки и прерывания

Структура кода низкоуровневых ловушек и прерываний похожа на таковую из NetBSD/i386, но их код отличается. В x86-64 также используется таблица дескрипторов прерываний (IDT - Interrupt Descriptor Table) для настройки векторов для ловушек и прерываний. При входе в ловушку обычно нужно сохранить регистры, обработать ловушку, восстановить регистры. Несмотря на то, что высокоуровневый код ловушек можно использовать одновременно и в NetBSD/i386 и в NetBSD/x86-64, т.к. набор ловушек одинаков для обеих архитектур, это пока ещё не сделано.

Другой тип ловушек - это входные точки системных вызовов. x86-64 поддерживает те же механизмы, которые уже есть в архитектуре IA32: вход в ядро через программные прерывания или при помощи обращения к структуре особого типа, называемой шлюзом, которая выполняет автоматический переход в ядро. Эти инструкции производят некоторые действия, которые не нужны в случае плоского адресного пространства (когда всё виртуальное адресное пространство в 4 гигабайта доступно программе одним куском, в то время как обычно ядро занимает верхнюю область памяти). Инструкции SYSCALL и SYSRET оптимизированы для этого случая и могут использоваться для реализации более быстрого пути для системных вызовов, который может оказывать заметное влияние на производительность приложений. Код для них был написан, но ещё не был интегрирован. В настоящее время пока ещё используются входные точки в старом стиле, но в скором будущем это изменится.

4.4 Низкоуровневая работа с виртуальной памятью / блоком управления памятью

Блок управления памятью 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: Раскладка виртуальной памяти NetBSD/x86-64.

Реализованная раскладка виртуальной памяти изображена на рисунке 4. Поскольку процессор выполняет знаковое расширение виртуальных адресов, между адресами 247 и 264 - 247 появляется неадресуемая область. Это не редкость, такую же область можно найти на процессорах SPARCv9 и Alpha. Раскладка памяти является в какой-то мере растянутой версией памяти IA32, что ожидаемо для совмещённого модуля pmap. Пользовательский процесс работает в нижней половине виртуальной памяти, в то время как ядро всегда находится в верхней половине. Часть верхней половины не используется, потому это могло бы привести к непропорциональному росту некоторых структур данных, а ядру не требуется большой общий объём виртуальной памяти. Верхняя часть нижней половины виртуальной памяти получается рекурсивным отображением страницы таблицы страниц, как и верхняя часть верхней половины виртуальной памяти (используется, если таблица страниц процесса отличается от той, на которую нужно переключиться).

В этой раскладке предполагается, что ядро находится за пределами "ядерной" модели кода, описанной в разделе о двоичном интерфейсе приложений. Потребовалась "огромная" модель, но она пока не поддерживается gcc. К счастью, её удалось включить при помощи нескольких небольших изменений. Ядро NetBSD/x86_64, скорее всего, будет переведено на "ядерную" модель двоичного интерфейса приложений, как только появится реальное оборудование и будет проведена оценка скорости. На данный момент используется эта раскладка, потому что она согласуется с расширением модели IA32 и позволяет упростить обобщение кода pmap.

4.5 Код реализации шины

Значительную часть кода реализации шины можно взять из переноса i386. В случае портов ввода-вывода необходимые инструкции совпадают, поэтому потребуется лишь небольшие изменения для того, чтобы они смогли работать с набором 64-битных регистров. То же относится и к вводу-выводу через отображаемую память. Для работы с 32-битной шиной PCI, которая не может обращаться к памяти по адресам выше 4 гигабайт, понадобится фреймворк для прямого доступа к памяти. На данный момент принято простое решение использовать с 32-битной шиной PCI для прямого доступа к памяти только адреса ниже 4 гигабайт. Позже это нужно будет пересмотреть, т.к. для компьютеров с оперативной памятью более 4 гигабайт доступная оперативная память может оказаться за пределами 4-гигабайтного предела. Для избежания этой проблемы можно воспользоваться буферами рикошета. Это промежуточные буферы для прямого доступа к памяти, данные из которых копируются в настоящее местоположение (или наоборот).

4.6 Устройства, специфичные для переноса

Пока перенос NetBSD x86-64 не работает с какими-либо специфичными для него устройствами. Симулятор симулирует некоторое количество компонентов оборудования, уже известного в мире персональных компьютеров (таких как мост узел-PCI, и т.п.). Эти компоненты не требуют доработки и "просто работают" после реализации слоёв bus_space и bus_dma.

4.7 Библиотеки

Основная работа в пользовательском пространстве заключалась в переносе библиотек и кода инициализации Си. Перенос кода инициализации Си и библиотеки Си были почти тривиальными. Большая часть работы свелась к написанию переходников для системных вызовов и оптимизации строковых функций, с учётом того, что большинство аргументов в двоичном интерфейсе приложений x86-64 передаются через регистры, а не через стек, как принято в двоичном интерфейсе приложений i386. Над библиотекой math поработать пришлось больше. В ней можно обобщить часть кода i386 (а на самом деле i387), т.к. модуль обработки чисел с плавающей запятой имеет те же инструкции, но двоичный интерфейс приложений отличается. В двоичном интерфейсе приложений x86-64 аргументы с плавающей запятой передаются через регистры SSE, в то время как в двоичном интерфейсе i386 они передаются через стек. Для извлечения и подготовки аргументов было написано несколько макросов для различных (в основном тригонометрических) функций, а затем общая часть кода была использована в переносах i386 и x86-64. Наконец, динамическая компоновка была адаптирована для работы с типами, используемыми в разделяемых библиотеках x86-64.

4.8 Код совместимости

Архитектура 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-битные аналоги.

5 Заключение и будущая работа

Перенос NetBSD на архитектуру AMD x86-64 был выполнен за шесть недель, что подтвердило репутацию NetBSD как очень переносимой операционной системы. Одна неделя ушла на настройку кросс-платформенного набора интструментов и чтение спецификаций x86-64, три недели были потрачены на написание кода ядра, одна неделя была потрачена на написание кода пользовательского пространства и одна неделя - на тестирование и отладку всего этого. Во время тестовых запусков не было обнаржуено никаких проблем в машинно-независимых частях ядра, все (симулированные) драйверы устройств, файловые системы и т.п. заработали без каких-либо изменений.

Перенос прошёл гладко. В таблице 1 показано общее количество новых написанных строк кода.

Таблица 1: Количество новых строк кода на Си/ассемблере в частях дерева исходного кода NetBSD
Часть дерева Строк на ассемблере Строк на Си
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.

Библиография

  1. Jason Thorpe: A Machine-Independent DMA Framework for NetBSD, Usenix 1998 Annual technical conference / Джейсон Р. Торп. Машинно-независимый фреймворк прямого доступа в память для NetBSD, ежегодная техническая конференция Usenix 1998.
  2. Advanced Micro Devices, Inc: The AMD x86-64 Architecture Programmers Overview
  3. Hubicka, Jaeger, Mitchell: x86-64 draft ABI
  4. Intel Corporation: Pentium 4 manuals
  5. Chris Demetriou: NetBSD bus_space(9) manual page, originally in NetBSD 1.3, 1997.

Сноски

  1. x86-64 является торговой маркой Advanced Micro Devices, Inc.

Автор перевода на русский язык: Владимир Ступин