С момента своего запуска в 2007 году на Wii было портировано несколько операционных систем: Linux, NetBSD и совсем недавно — Windows NT. Сегодня к этому списку присоединяется Mac OS X.

В этом посте я расскажу, как я портировал первую версию Mac OS X, 10.0 Cheetah, на Nintendo Wii. Если вы не эксперт по операционным системам или инженер-программист, то не одиноки; этот проект был посвящён изучению и освоению бесчисленных «неизвестных неизвестных». Присоединяйтесь ко мне, чтобы вместе исследовать аппаратное обеспечение Wii, разработку загрузчика, патчинг ядра и написание драйверов — и дать версиям Mac OS X для PowerPC новую жизнь на Nintendo Wii.

Посетите репозиторий загрузчика wiiMac, чтобы узнать, как самостоятельно попробовать этот проект.

Исследование реализации

Прежде чем приступить к этому проекту, мне нужно было понять, возможно ли это вообще. Согласно комментарию на Reddit от 2021 года:

Вероятность того, что это когда-либо произойдёт, равна нулю.

Вдохновлённый, я начал с основ: какое оборудование используется в Wii и как оно соотносится с оборудованием, применяемым в настоящих компьютерах Mac той эпохи.

Аппаратная совместимость

Wii использует процессор PowerPC 750CL — эволюцию PowerPC 750CXe, который использовался в iBook G3 и некоторых iMac G3. Учитывая это близкое родство, я был уверен, что процессор не станет препятствием.

Что касается оперативной памяти, Wii имеет уникальную конфигурацию: всего 88 МБ, распределённых между 24 МБ 1T-SRAM (MEM1) и 64 МБ более медленной GDDR3 SDRAM (MEM2); нетрадиционно, но технически достаточно для Mac OS X Cheetah, которая официально требует 128 МБ оперативной памяти, но неофициально загружается с меньшим объёмом. Для большей безопасности я использовал QEMU для загрузки Cheetah с 64 МБ оперативной памяти и убедился, что проблем нет.

Другое оборудование, которое мне в конечном итоге потребуется поддерживать, включало:

  • последовательный отладочный вывод через USB Gecko;

  • SD-карта для загрузки остальной части системы после запуска ядра;

  • контроллеры прерываний;

  • видеовывод через фреймбуфер, расположенный в оперативной памяти;

  • USB-порты Wii для использования мыши и клавиатуры.

Убедившись, что оборудование Wii принципиально не несовместимо с Mac OS X, я переключил своё внимание на исследование программного стека, который мне предстояло портировать.

Программная совместимость

Mac OS X имеет ядро ​​с открытым исходным кодом (Darwin, с XNU в качестве ядра и IOKit в качестве модели драйверов), поверх которого наложены компоненты с закрытым исходным кодом (Quartz, Dock, Finder, системные приложения и фреймворки). Теоретически, если бы я смог достаточно модифицировать части с открытым исходным кодом, чтобы запустить Darwin, части с закрытым исходным кодом работали бы без дополнительных патчей.

Для портирования Mac OS X также потребуется понимание того, как загружается настоящий Mac. Компьютеры Mac на базе PowerPC начала 2000-х годов используют Open Firmware в качестве своей программной среды самого низкого уровня; для простоты её можно рассматривать как первый код, который выполняется при включении Mac. Open Firmware выполняет несколько функций, в том числе:

  • обнаружение и настройка оборудования;

  • построение дерева устройств (на основе обнаруженного оборудования);

  • предоставление полезных функций для ввода-вывода, отрисовки и связи с оборудованием;

  • загрузка и выполнение загрузчика операционной системы из файловой системы.

В конечном итоге Open Firmware передаёт управление BootX, загрузчику Mac OS X. BootX подготавливает систему, чтобы она могла в конечном итоге передать управление ядру. В обязанности BootX входят:

  • чтение дерева устройств из Open Firmware;

  • загрузка и декодирование ядра XNU, исполняемого файла Mach-O, из корневой файловой системы;

  • передача управления ядру.

После запуска XNU зависимости от BootX или Open Firmware отсутствуют. XNU продолжает инициализацию процессоров, виртуальной памяти, IOKit, BSD и, в конечном итоге, продолжает загрузку, загружая и запуская другие исполняемые файлы из корневой файловой системы.

Последним элементом головоломки был запуск моего собственного кода на Wii — тривиальная задача благодаря тому, что Wii «взломана», и это позволяет любому запускать homebrew с полным доступом к оборудованию через Homebrew Channel и BootMii.

Подход к портированию

Вооружившись знаниями о том, как работает процесс загрузки на реальном Mac, а также о том, как запускать низкоуровневый код на Wii, мне нужно было выбрать подход к загрузке Mac OS X на Wii. Я рассмотрел три варианта:

  • портировать Open Firmware и использовать его для запуска неизменённого BootX для загрузки Mac OS X;

  • портировать BootX и модифицировать его так, чтобы он не зависел от Open Firmware и использовался для загрузки Mac OS X;

  • написать собственный загрузчик, который выполняет минимальную настройку для загрузки Mac OS X.

Поскольку Mac OS X не зависит от Open Firmware или BootX после запуска, тратить время на портирование любого из них казалось ненужным отвлечением. Кроме того, как Open Firmware, так и BootX предлагают дополнительную сложность для поддержки множества различных аппаратных конфигураций — сложность, которая мне не нужна, поскольку это нужно только для работы на Wii. Следуя примеру проекта Wii Linux, я решил написать свой собственный загрузчик с нуля. Загрузчик должен, как минимум, выполнять следующие функции:

  • инициализировать оборудование Wii;

  • загрузить ядро ​​с SD-карты;

  • создать дерево устройств и аргументы загрузки;

  • передать управление ядру.

После запуска ядра код загрузчика переставал иметь значение. На этом этапе я переключал своё внимание на патчинг ядра и написание драйверов.

Создание загрузчика

Я решил использовать в качестве основы для своего загрузчика пример низкоуровневого кода для Wii под названием ppcskel. ppcskel переводит систему в разумное начальное состояние и предоставляет полезные функции для таких распространённых задач, как чтение файлов с SD-карты, отрисовка текста в буфер кадров и запись отладочных сообщений в USB Gecko.

Загрузка ядра

Далее мне нужно было выяснить, как загрузить ядро ​​XNU в память, чтобы передать ему управление. Ядро хранится в специальном двоичном формате, называемом Mach-O, и его необходимо правильно декодировать перед использованием.

Исполняемый формат Mach-O хорошо документирован и может рассматриваться как список команд загрузки, которые указывают загрузчику, куда поместить различные разделы двоичного файла в памяти. Например, команда загрузки может указать загрузчику прочитать данные из файла со смещением 0x2cf000 и сохранить их по адресу памяти 0x2e0000. После обработки всех команд загрузки ядра мы получаем следующую структуру памяти:

0x00000000: Exception vectors
0x00011000: LC_SEGMENT __TEXT
0x002e0000: LC_SEGMENT __DATA
0x00367000: LC_SEGMENT __KLD
0x00395000: LC_SEGMENT __LINKEDIT
0x00434000: LC_SEGMENT __SYMTAB
0x004d3000: LC_SEGMENT __HEADER

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

Вызов ядра

Чтобы перейти к адресу памяти точки входа ядра, мне нужно было преобразовать адрес в функцию и вызвать её:

(*(void (*)())kernel_entry_point)(boot_args_address, MAC_OS_X_SIGNATURE);

После выполнения этого кода экран погас, и отладочные логи перестали поступать через последовательное отладочное соединение — хотя это и не впечатляет, но это индикатор того, что ядро ​​работает.

Тогда возник вопрос: насколько далеко я продвинулся в процессе загрузки? Чтобы ответить на него, мне пришлось начать изучать исходный код XNU. Первый выполняемый код — это подпрограмма _start на языке ассемблера PowerPC. Этот код перенастраивает оборудование, переопределяя все специфические для Wii настройки, выполненные загрузчиком, и в процессе отключает функциональность загрузчика для последовательной отладки и вывода видео. Без обычных средств отладочного вывода мне пришлось бы отслеживать прогресс другим способом.

Придуманный мной подход был немного хаком: внести бинарные изменения в ядро, заменив инструкции на те, которые зажигают один из светодиодов на передней панели Wii. Если светодиод загорался после перехода к ядру, то я знал, что оно ​​хотя бы дошло до этого этапа. Включение одного из этих светодиодов так же просто, как запись значения по определённому адресу памяти. На ассемблере PowerPC эти инструкции выглядят так:

lis    r5, 0xd80       ; load upper half of 0x0D8000C0 into r5
ori    r5, r5, 0xc0    ; load lower half of 0x0D8000C0 into r5
lwz    r4, (r5)        ; read the 32-bit value from 0x0D8000C0
sync                   ; memory barrier
xori   r4, r4, 0x20    ; toggle bit 5
stw    r4, (r5)        ; write the value back to 0x0D8000C0

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

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

После анализа множества процедур запуска ядра я в итоге составил следующий план выполнения:

1. start.s: start
2. start.s: allStart
3. start.s: nextPVR
4. start.s: donePVR
5. start.s: doOurInit
6. start.s: noFloat
7. start.s: noVector
8. start.s: noSMP
9. start.s: noThermometer
10. ppc_init.c: ppcInit
11. pe_init.c: PE_INIT_PLATFORM
12. device_tree.c: find_entry (crash with 300 exception)

Это был важный этап — ядро ​​определённо работало, и я даже перевёл его в высокоуровневый код на C. Чтобы преодолеть сбой с исключением 300, загрузчику необходимо было передать указатель на допустимое дерево устройств.

Создание и передача дерева устройств

Дерево устройств — это структура данных, представляющая всё оборудование в системе, которое должно быть доступно ОС. Как следует из названия, это дерево, состоящее из узлов, каждый из которых может содержать свойства и ссылки на дочерние узлы.

На реальных компьютерах Mac загрузчик сканирует оборудование и строит дерево устройств на основе найденных данных. Поскольку оборудование Wii не меняется, этот этап сканирования можно пропустить. В итоге я жёстко прописал дерево устройств в коде.

В загрузчике я использовал дерево устройств, заимствованное из проекта Wii Linux.

Поскольку я не был уверен, сколько аппаратных компонентов Wii мне нужно будет поддерживать, чтобы продвинуться дальше в процессе загрузки, то начал с минимального дерева устройств: корневого узла с дочерними узлами для процессоров и памяти:

/
└── cpus
    └── PowerPC,750
└── memory

Мой план состоял в том, чтобы расширять дерево устройств, добавляя новые аппаратные компоненты по мере продвижения в процессе загрузки — в конечном итоге создав полное представление всего оборудования Wii, которое я планировал поддерживать в Mac OS X.

После того, как дерево устройств было создано и сохранено в памяти, мне нужно было передать его ядру в составе boot_args:

typedef struct boot_args {
    u16	Revision;	                /* Revision of boot_args structure */
    u16	Version;	                /* Version of boot_args structure */
    char CommandLine[256];	        /* Passed in command line */
    DRAMBank PhysicalDRAM[26];	    /* base and range pairs for the 26 DRAM banks */
    Boot_Video Video;		        /* Video Information */
    u32	machineType;	            /* Machine Type (gestalt) */
    void *deviceTreeP;	            /* Base of flattened device tree */
    u32	deviceTreeLength;           /* Length of flattened tree */
    u32	topOfKernelData;            /* Highest address used in kernel data area */
} boot_args_t;

С деревом устройств в памяти я преодолел сбой device_tree.c. Загрузчик хорошо выполнял основные операции: загрузку ядра, создание аргументов загрузки и дерева устройств, и, в конечном итоге, вызов ядра. Для дальнейшего прогресса мне нужно было переключить внимание на исправление исходного кода ядра, чтобы устранить оставшиеся проблемы совместимости.

Исправление ядра

На этом этапе ядро ​​зависало при выполнении кода для настройки видеопамяти и памяти ввода-вывода. XNU того времени делает предположения о том, где может располагаться видеопамять и память ввода-вывода, и переконфигурирует трансляцию адресов блоков (BAT) таким образом, что это плохо сочетается с расположением памяти Wii (MEM1 начинается с 0x00000000, MEM2 начинается с 0x10000000). Чтобы обойти эти ограничения, пришло время модифицировать исходный код ядра и загрузить модифицированный бинарный файл ядра.

Найти подходящую среду разработки для сборки ядра ОС 25-летней давности потребовало определенных усилий. Вот к чему я пришёл:

  • гостевая система Mac OS X Cheetah (работающая через QEMU), без графического интерфейса, на современной хост-системе macOS;

  • исходный код XNU находится в файловой системе хоста и доступен через NFS-сервер;

  • гостевая система получает доступ к исходному коду XNU через монтирование NFS;

  • хост использует SSH для управления гостевой системой;

  • редактирование исходного кода XNU на хосте, запуск сборки через SSH на гостевой системе, артефакты сборки оказываются в файловой системе, доступной как хосту, так и гостевой системе.

Для настройки зависимостей, необходимых для сборки ядра Mac OS X Cheetah на гостевой системе Mac OS X Cheetah, я следовал инструкциям здесь. Они в основном совпадали с тем, что мне нужно было сделать. Соответствующие исходные коды доступны на сайте Apple здесь.

После исправления настроек BAT и добавления нескольких небольших патчей для перенаправления вывода консоли на мой USB Gecko, у меня теперь работает вывод видео и отладочные логи в последовательном порту, что значительно упрощает дальнейшую разработку и отладку. Благодаря этой новой информации я смог убедиться, что виртуальная память, IOKit и подсистемы BSD были инициализированы и работали без сбоев. Это был важный шаг, который вселил уверенность в том, что я на правильном пути к созданию полноценной работающей системы.

Читатели, которые пытались запустить Mac OS X на ПК с помощью «хакинтошинга», могут узнать последнюю строку в журналах загрузки: ужасное сообщение «Всё ещё ожидаю корневое устройство». Это происходит, когда система не может найти корневую файловую систему, с которой можно продолжить загрузку. В моем случае это было ожидаемо: ядро ​​сделало всё, что могло, и было готово загрузить остальную часть системы Mac OS X из файловой системы, но оно не знало, где найти эту файловую систему. Чтобы продвинуться дальше, мне нужно было сообщить ядру, как читать с SD-карты Wii. Для этого требовалось заняться следующим этапом этого проекта: написанием драйверов.

Написание драйверов

Понимание модели драйверов IOKit

Драйверы Mac OS X создаются с использованием IOKit — набора программных компонентов, призванных упростить расширение ядра для поддержки различных аппаратных устройств. Драйверы пишутся на подмножестве C++ и широко используют концепции объектно-ориентированного программирования, такие как наследование и композиция. Предоставляется множество полезных функций, в том числе:

  • базовые классы и «семейства», реализующие общее поведение для различных типов оборудования;

  • многоуровневая архитектура среды выполнения, представляющая отношения поставщик-клиент;

  • поиск и сопоставление драйверов с оборудованием, присутствующим в дереве устройств;

  • абстракции для доступа к памяти устройства.

В IOKit существует два типа драйверов: конкретный драйвер устройства и «заглушка». Конкретный драйвер устройства — это объект, который управляет конкретным аппаратным компонентом. «Заглушка» — это объект, который служит точкой подключения для конкретного драйвера устройства, а также обеспечивает возможность взаимодействия этого подключённого драйвера с создавшим этот узел. Именно эта цепочка «драйвер-узел-драйвер» создает вышеупомянутые отношения «поставщик-клиент». Мне долго было трудно понять эту концепцию, и я нашёл полезный конкретный пример.

Настоящие компьютеры Mac могут иметь шину PCI с несколькими портами PCI. В этом примере рассмотрим сетевую карту, подключённую к одному из портов PCI. Драйвер IOPCIBridge отвечает за связь с аппаратным обеспечением шины PCI на материнской плате. Этот драйвер сканирует шину, создавая узлы IOPCIDevice (точки подключения) для каждого подключённого устройства, которое он обнаруживает. Гипотетический драйвер для подключённой сетевой карты (назовём ее SomeEthernetCard) может подключиться к узлу, используя его в качестве прокси для вызова функциональности PCI, предоставляемой драйвером IOPCIBridge на другой стороне. Драйвер SomeEthernetCard также может создавать собственные интерфейсы IOEthernetInterface, чтобы к нему могли подключаться компоненты сетевого стека IOKit более высокого уровня.

Разработчику драйвера сетевой карты PCI достаточно будет написать только SomeEthernetCard; код для низкоуровневой связи по шине PCI и код для высокоуровневого сетевого стека уже предоставляются существующими семействами драйверов IOKit. Пока SomeEthernetCard может подключаться к интерфейсу IOPCIDevice и публиковать собственные интерфейсы IOEthernetInterface, он может располагаться между двумя существующими семействами драйверов в стеке, используя все возможности, предоставляемые IOPCIFamily, и одновременно удовлетворяя потребности IONetworkingFamily.

Представление аппаратного обеспечения Wii

В отличие от компьютеров Mac той же эпохи, Wii не использует PCI для подключения различных компонентов своего оборудования к материнской плате. Вместо этого применяется собственная система на кристалле (SoC) под названием Hollywood. Через Hollywood можно получить доступ ко многим аппаратным компонентам: графическому процессору, SD-карте, Wi-Fi, Bluetooth, контроллерам прерываний, USB-портам и многому другому. Hollywood также содержит сопроцессор ARM, получивший прозвище Starlet, который предоставляет доступ к аппаратным функциям основному процессору PowerPC через межпроцессорное взаимодействие (IPC).

Уникальная компоновка аппаратного обеспечения и протокол связи означали, что я не мог использовать существующее семейство драйверов IOKit, такое как IOPCIFamily. Вместо этого мне нужно было реализовать эквивалентный драйвер для SoC Hollywood, создав «выступы», представляющие собой точки подключения для всего содержащегося в нём оборудования. В итоге я остановился на таком расположении драйверов и значков (обратите внимание, что здесь показана лишь часть драйверов, которые пришлось написать):

Теперь, когда у меня появилось представление о том, как представить аппаратное обеспечение Wii в IOKit, я начал работу над своим драйвером Hollywood.

Написание драйвера Hollywood

Я начал с создания нового заголовочного файла C++ и файла реализации для драйвера NintendoWiiHollywood. Его «характеристики» драйвера позволили сопоставить его с узлом в дереве устройств с именем «hollywood». После того, как драйвер был сопоставлен и запущен, пришло время опубликовать значки для всех его дочерних устройств.

Вновь опираясь на дерево устройств как на источник достоверной информации о том, какое оборудование находится под узлом Hollywood, я перебрал все дочерние узлы узла Hollywood, создавая и публикуя для каждого из них заглушки NintendoWiiHollywoodDevice:

bool NintendoWiiHollywood::publishBelow(OSIterator *iter)
{
  IORegistryEntry *next;
  IOService *nub;
  
  if (!iter)
  {
    return false;
  }

  // loop through all children of /hollywood
  while ((next = (IORegistryEntry *)iter->getNextObject()))
  {
    // create a nub
    nub = createNub(next);
    if (!nub)
    {
      continue;
    }

    // publish nubs so that drivers can attach to them
    if (nub->attach(this))
    {
      nub->registerService();
    }
    
    nub->release();
  }

  iter->release();
  
  return true;
}

IOService *NintendoWiiHollywood::createNub(IORegistryEntry *from)
{
  NintendoWiiHollywoodDevice *nub = new NintendoWiiHollywoodDevice;

  if (nub && nub->init(from, gIODTPlane))
  {
    // give the nub a reference back to its hollywood "provider"
    nub->hollywood = this;
    return nub;
  }

  if (nub)
  {
    nub->release();
  }

  return 0;
}

После создания и публикации nub-устройств NintendoWiiHollywoodDevice система сможет подключать к ним другие драйверы устройств, например, драйвер SD-карты.

Написание драйвера для SD-карты

Далее я перешёл к написанию драйвера, позволяющего системе читать и записывать данные с SD-карты Wii. Этот драйвер должен был позволить системе продолжить загрузку, поскольку в данный момент она зависла в поиске корневой файловой системы для загрузки дополнительных файлов автозагрузки.

Я начал с создания подкласса IOBlockStorageDevice, который содержит множество абстрактных методов, предназначенных для реализации подклассами:

virtual IOReturn doAsyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks, IOStorageCompletion completion) = 0;
virtual IOReturn doSyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks) = 0;
virtual IOReturn doEjectMedia(void) = 0;
virtual IOReturn doFormatMedia(UInt64 byteCapacity) = 0;
virtual UInt32 doGetFormatCapacities(UInt64 *capacities, UInt32 capacitiesMaxCount) const = 0;
virtual IOReturn doLockUnlockMedia(bool doLock) = 0;
virtual IOReturn doSynchronizeCache(void) = 0;
virtual char *getVendorString(void) = 0;
virtual char *getProductString(void) = 0;
virtual char *getRevisionString(void) = 0;
virtual char *getAdditionalDeviceInfoString(void) = 0;
virtual IOReturn reportBlockSize(UInt64 *blockSize) = 0;
virtual IOReturn reportEjectability(bool *isEjectable) = 0;
virtual IOReturn reportLockability(bool *isLockable) = 0;
virtual IOReturn reportMaxReadTransfer(UInt64 blockSize, UInt64 *max) = 0;
virtual IOReturn reportMaxWriteTransfer(UInt64 blockSize, UInt64 *max) = 0;
virtual IOReturn reportMaxValidBlock(UInt64 *maxBlock) = 0;
virtual IOReturn reportMediaState(bool *mediaPresent, bool *changedState) = 0;
virtual IOReturn reportPollRequirements(bool *pollRequired, bool *pollIsExpensive) = 0;
virtual IOReturn reportRemovability(bool *isRemovable) = 0;
virtual IOReturn reportWriteProtection(bool *isWriteProtected) = 0;

Для большинства этих методов я мог реализовать их с помощью жёстко заданных значений, соответствующих аппаратному обеспечению SD-карты Wii; строка поставщика, размер блока, максимальный размер передачи при чтении и записи, возможность извлечения и многие другие возвращают постоянные значения и были тривиальны в реализации.

Наиболее интересными для реализации были методы, которые фактически взаимодействовали с вставленной в данный момент SD-картой: получение ёмкости SD-карты, чтение с SD-карты и запись на SD-карту:

virtual IOReturn doAsyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks, IOStorageCompletion completion) = 0;
virtual IOReturn doSyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks) = 0;
virtual IOReturn reportMaxValidBlock(UInt64 *maxBlock) = 0;

Для связи с SD-картой я использовал функциональность межпроцессного взаимодействия (IPC), предоставляемую MINI, работающим на сопроцессоре Starlet. Записывая данные по определённым зарезервированным адресам памяти, драйвер SD-карты мог отправлять команды MINI. Затем MINI выполнял эти команды, передавая результаты обратно путём записи по другому зарезервированному адресу памяти, который драйвер мог отслеживать.

MINI поддерживает множество полезных типов команд. Драйвер SD-карты использует следующие команды:

  • IPC_SDMMC_SIZE: возвращает количество секторов на вставленной в данный момент SD-карте;

  • IPC_SDMMC_READ: считывает данные из сектора в буфер памяти;

  • IPC_SDMMC_WRITE: записывает данные из буфера памяти в сектор.

С помощью этих трёх типов команд можно было реализовать чтение, запись и проверку ёмкости, что позволило удовлетворить основные требования подкласса блочных запоминающих устройств.

Как и в большинстве программных проектов, редко что-то получается с первого раза. Для исследования проблем я использовал в качестве основного инструмента отладки отправку сообщений журнала в последовательный отладчик через вызовы IOLog. С помощью этого метода я мог видеть, какие методы вызывались в моём драйвере, какие значения передавались и какие значения моя реализация IPC отправляла и получала от MINI, — но у меня не было возможности устанавливать точки останова или динамически анализировать выполнение во время работы ядра.

Одна из самых сложных ошибок, с которыми я столкнулся, была связана с кэшированной памятью. Когда драйвер SD-карты пытается прочитать данные с SD-карты, команда, которую он отправляет MINI (работающей на процессоре ARM), включает адрес памяти, по которому следует сохранить загруженные данные. После того, как MINI завершит запись в память, драйвер SD-карты (работающий на процессоре PowerPC) может не увидеть обновленное содержимое, если эта область отображается как кэшируемая. В этом случае PowerPC будет читать из своих кэш-линий, а не из ОЗУ, возвращая устаревшие данные вместо нового загруженного содержимого. Чтобы обойти это, драйвер SD-карты должен использовать некэшированную память для своих буферов.

После нескольких дней исправления ошибок я достиг нового рубежа: IOBlockStorageDriver, подключённый к моему драйверу SD-карты, начал публиковать IOMedia-бугорки, представляющие логические разделы, присутствующие на SD-карте. Благодаря этим бугоркам высокоуровневые части системы смогли подключиться к SD-карте и начать её использовать. Важно отметить, что система теперь смогла найти корневую файловую систему, с которой можно было продолжить загрузку, и я больше не застревал на отметке «Всё ещё ожидание корневого устройства».

Теперь мои журналы загрузки выглядели так:

Waiting on <dict ID="0"><key>IOProviderClass</key><string ID="1">IOService</string><key>BSD Name</key><string ID="2">disk0s4</string></dict>
NintendoWiiSDCard: started
Got boot device = IOService:/NintendoWiiPE/hollywood/NintendoWiiHollywood/sdhc@D070000/NintendoWiiSDCard/IOBlockStorageDriver/Nintendo Nintendo Wii SD Media/IOApplePartitionScheme/Untitled 4@4
BSD root: disk0s4, major 14, minor 3
devfs on /dev

После нескольких дополнительных попыток исправления ошибок (попутно) мне удалось загрузиться не в однопользовательский режим:

И, наконец, удалось пройти весь процесс запуска в режиме подробного вывода информации, который завершается сообщением: «Запуск завершён»:

На этом этапе система пыталась найти драйвер фреймбуфера, чтобы отобразить графический интерфейс Mac OS X. Как указано в логах, WindowServer был недоволен — чтобы исправить это, мне нужно было написать собственный драйвер фреймбуфера.

Написание драйвера фреймбуфера

Фреймбуфер — это область оперативной памяти, которая хранит данные пикселей, используемые для создания изображения на дисплее. Эти данные обычно состоят из значений цветовых компонентов для каждого пикселя. Чтобы изменить отображаемое, новые данные пикселей записываются в фреймбуфер, который затем отображается при следующем обновлении дисплея. Для Wii буфер кадров обычно располагается где-то в MEM1, поскольку он немного быстрее, чем MEM2. Я решил разместить свой буфер кадров в последнем мегабайте MEM1 по адресу 0x01700000. При разрешении 640x480 и 16 битах на пиксель пиксельные данные для буфера кадров удобно помещаются менее чем в один мегабайт памяти.

На ранних этапах загрузки Mac OS X использует предоставленный загрузчиком адрес буфера кадров для отображения простой загрузочной графики через video_console.c. В случае загрузки в режиме подробного вывода в буфер кадров записываются растровые изображения символов шрифта для создания визуального журнала того, что происходит во время запуска. После достаточной загрузки система больше не может использовать этот начальный код буфера кадров; рабочий стол, оконный сервер, док-станция и все остальные процессы, связанные с графическим интерфейсом пользователя Mac OS X Aqua, требуют реального драйвера фреймбуфера, поддерживающего IOKit.

Для решения этой задачи я создал подкласс IOFramebuffer. Подобно подклассу IOBlockStorageDevice для драйвера SD-карты, IOFramebuffer также имел несколько абстрактных методов, которые мой подкласс фреймбуфера мог реализовать:

virtual class IODeviceMemory* getApertureRange(IOPixelAperture aperture);
virtual const char* getPixelFormats();
virtual IOItemCount getDisplayModeCount();
virtual IOReturn getDisplayModes(IODisplayModeID *);
virtual IOReturn getInformationForDisplayMode(long int, IODisplayModeInformation *);
virtual UInt64 getPixelFormatsForDisplayMode(long int, long int);
virtual IOReturn getPixelInformation(long int, long int, long int, IOPixelInformation *);
virtual IOReturn getCurrentDisplayMode(IODisplayModeID *, IOIndex *);
virtual IOReturn setGammaTable(UInt32, UInt32, UInt32, void *);
virtual IOReturn setDisplayMode(IODisplayModeID, IOIndex);
virtual IOReturn setApertureEnable(IOPixelAperture, IOOptionBits);
virtual IOReturn newUserClient(task_t, void *, UInt32, IOUserClient **);
virtual bool isConsoleDevice(void);

Опять же, большинство из них было тривиально реализовать и просто требовало возврата жёстко закодированных значений, совместимых с Wii, которые точно описывали оборудование. Один из наиболее важных методов для реализации — это getApertureRange, который возвращает экземпляр IODeviceMemory, базовый адрес и размер которого описывают местоположение буфера кадров в памяти:

IODeviceMemory* NintendoWiiFramebuffer::getApertureRange(IOPixelAperture aperature)
{
  // 0x01700000, 640x480 resoluton, 2 bytes (16 bits) per pixel
  return IODeviceMemory::withRange(0x01700000, 640 * 480 * 2);
}

После возврата корректного экземпляра памяти устройства из этого метода система смогла перейти от текстового буфера вывода на ранней стадии загрузки к буферу вывода, способному отображать полный графический интерфейс Mac OS X. Мне даже удалось загрузить установщик Mac OS X:

Внимательные читатели могут заметить некоторые проблемы:

  • текстовый буфер вывода в режиме подробного отображения по-прежнему активен, из-за чего отображается текст и прокручивается буфер вывода;

  • всё пурпурного цвета.

Исправление проблемы с тем, что видеоконсоль на ранней стадии загрузки всё ещё записывает текст в буфер вывода, было простым: сообщить системе, что наш новый буфер вывода IOKit совпадает с тем, который использовался ранее, вернув true из метода isConsoleDevice:

bool NintendoWiiFramebuffer::isConsoleDevice(void)
{
  return true;
}

Исправление некорректных цветов оказалось гораздо сложнее, поскольку связано с фундаментальной несовместимостью между видеооборудованием Wii и графическим кодом, используемым Mac OS X.

Аппаратное обеспечение видеокодера Nintendo Wii оптимизировано для вывода аналогового телевизионного сигнала и, как следствие, ожидает 16-битные пиксельные данные YUV в своем буфере кадров. Это проблема, поскольку Mac OS X ожидает, что буфер кадров будет содержать пиксельные данные RGB. Если буфер кадров, отображаемый Wii, содержит пиксельные данные, отличные от YUV, то цвета будут совершенно неправильными.

Чтобы обойти эту несовместимость, я вдохновился проектом Wii Linux, который решил эту проблему много лет назад. Стратегия заключается в использовании двух буферов кадров: буфера кадров RGB, с которым взаимодействует Mac OS X, и буфера кадров YUV, который видеооборудование Wii выводит на подключённый дисплей. Драйвер кадрового буфера 60 раз в секунду преобразует пиксельные данные в RGB-кадровом буфере в данные пикселей YUV, помещённые в буфер кадров, отображаемый видеооборудованием Wii:

После реализации стратегии с двумя буферами кадров мне удалось загрузить систему Mac OS X с правильными цветами — впервые Mac OS X работала на Nintendo Wii:

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

Добавление поддержки USB

Для включения ввода с USB-клавиатуры и мыши мне нужно было заставить работать задние USB-порты Wii под управлением Mac OS X — в частности, требовалось запустить низкоскоростной контроллер USB 1.1 OHCI. Я надеялся использовать код из IOUSBFamily — набора драйверов USB, который абстрагирует большую часть сложности взаимодействия с USB-оборудованием. Конкретный драйвер, который мне нужно было запустить, — это AppleUSBOHCI — драйвер, который обрабатывает взаимодействие именно с типом контроллера USB, используемого в Wii.

Мои надежды быстро сменились разочарованием, когда я столкнулся с несколькими препятствиями.

Препятствие 1:

Исходный код IOUSBFamily для Mac OS X Cheetah и Puma по какой-то причине не входит в и без того обширную коллекцию релизов с открытым исходным кодом, предоставляемых Apple. Это означало, что мои возможности по отладке проблем или несовместимости оборудования будут серьёзно ограничены. По сути, если бы стек USB не заработал волшебным образом без каких-либо настроек или модификаций (спойлер: конечно же, не заработал), диагностика проблемы была бы крайне затруднительной без доступа к исходному коду.

Препятствие 2:

AppleUSBOHCI не соответствовал ни одному оборудованию в дереве устройств и, следовательно, не запускался, поскольку его драйвер настаивал на том, чтобы его класс поставщика (подключённый к нему элемент) был IOPCIDevice. Как я уже выяснил, Wii определённо не использует IOPCIFamily, а это значит, что элементы IOPCIDevice никогда не будут созданы, и AppleUSBOHCI не к чему будет подключаться.

Моим решением для обхода этой проблемы стало создание нового элемента NintendoWiiHollywoodDevice, названного NintendoWiiHollywoodPCIDevice, который является подклассом IOPCIDevice. Благодаря тому, что NintendoWiiHollywood опубликовала заглушку, унаследовавшую от IOPCIDevice, и изменила настройки драйвера AppleUSBOHCI в его файле Info.plist, чтобы использовать NintendoWiiHollywoodPCIDevice в качестве класса-провайдера, мне удалось добиться соответствия и запустить его.

Чтобы выяснить, как AppleUSBOHCI использует свою заглушку для PCI-устройства, я использовал комбинацию логирования во время выполнения, анализа дизассемблированного кода и анализа исходного кода IOUSBFamily для Mac OS X 10.2 Jaguar (которые являются первыми доступными от Apple). К моему облегчению, связь с PCI-оборудованием через заглушку для PCI-устройства была ограничена — AppleUSBOHCI требовала от PCI-оборудования базовый адрес контроллера USB-хоста, который она получала с помощью команд PCI. Мне удалось перехватить эти команды в моей поддельной заглушке для PCI и вернуть базовый адрес оборудования OHCI Wii.

Благодаря этим обходным путям AppleUSBOHCI теперь работал, однако мои USB-порты по-прежнему не реагировали.

Препятствие 3:

Следующее моё открытие заключалось в том, что AppleUSBOHCI предполагает порядок байтов little-endian для чтения и записи регистров. После некоторых исследований я узнал, что это на самом деле довольно стандартное поведение для оборудования OHCI, даже если хост-оборудование является системой big-endian (как в случае с системами PowerPC, такими как Wii и компьютеры Mac на базе PowerPC). Так почему же это не работало на Wii?

Несовместимость сводится к разнице в том, как IOUSBFamily и Wii обрабатывают различия в порядке байтов между USB-оборудованием и хост-процессором — в случае IOUSBFamily данные переставляются байтами программно при работе с регистрами OHCI, в то время как в случае Wii данные переставляются байтами аппаратно через переставленные байтовые линии, из-за чего регистры OHCI автоматически отображаются как big-endian при чтении или записи. Эта система на Wii известна как обратный little-endian.

Чтобы обойти это, мне нужно было предотвратить «двойной обмен», который происходил, удалив программный обмен байтами, выполняемый IOUSBFamily, — но без доступа к исходному коду это было бы непросто. В очередной раз столкнувшись со сложной проблемой во время путешествия, я потратил несколько часов в самолёте, используя Ghidra для поиска и удаления любых инструкций по обмену байтами. Процесс быстро стал запутанным, поскольку в стеке USB есть несколько мест, где допустимы операции обмена байтами, и их не следует удалять.

В итоге мой вручную исправленный бинарный файл AppleUSBOHCI оказался хрупким, почти не поддающимся редактированию и почти наверняка некорректным. Неудивительно, что он не сработал, и мои USB-порты продолжали оставаться неактивными.

Решение:

На самом деле мне нужен был доступ к исходному коду IOUSBFamily для Mac OS X Cheetah. Если бы у меня был Cheetah, я бы смог избавиться от зависимости от IOPCIFamily, удалить все ненужные программные перестановки байтов и, надеюсь, создать рабочую версию, которая работала бы с аппаратным обеспечением Wii.

После нескольких дней поисков на старых форумах, просмотра сайтов в Wayback Machine и попыток доступа к старым FTP-серверам я решил вернуться к одному из тех мест, где раньше обращался за помощью в интернете: IRC.

И действительно, в репозитории CVS, предоставленном @bbraun (с synack.net), были все необходимые файлы для сборки IOUSBFamily для Mac OS X Cheetah. Если вы это читаете — спасибо за помощь незнакомцу из интернета :)

После того, как IOUSBFamily был пропатчен и собран из исходного кода, моя USB-клавиатура и мышь смогли управлять системой, превратив Wii в работающий компьютер Mac OS X.

Делаем вещи лучше ™

Улучшение загрузчика

Для поддержки сценария полной установки Mac OS X мне потребовалось добавить поддержку загрузки с разных разделов на одной SD-карте (один для установщика, другой для установленной системы). Я переработал меню загрузки, чтобы оно отображало все загрузочные разделы, позволяя пользователю выбрать тот, с которого он хочет загрузиться, перебирая доступные варианты.

Для отображения доступных разделов мне потребовалось проанализировать карту разделов Apple (APM) в секторе 1 SD-карты. После обработки данных я смог получить смещения, типы файловых систем и имена каждого раздела на диске:

Далее я хотел добавить возможность загрузки с неизмененного установщика и системного раздела — это позволило бы избежать необходимости замены драйверов или ядра после установки или обновления Mac OS X. Для этого мне нужно было сделать загрузчик, а не ядро, ответственным за загрузку всех драйверов, специфичных для Wii. К счастью, Mac OS X поддерживает внедрение драйверов через загрузчик через узел /chosen/memory-map в дереве устройств. Этот узел содержит записи для каждого загруженного загрузчиком драйвера:

/
└── chosen
	└── memory-map
    	├── Driver-4d6000
      	├── Driver-4d7000
        ├── Driver-4d8000
        ├── Driver-4d9000
        ├── Driver-4da000
            etc.

Каждая запись содержит адрес, указывающий на структуру записи драйвера в памяти:

typedef struct driver_info  {
    char *info_plist_start;
    u32 info_plist_size;
    void *bin_start;
    u32 bin_size;
} driver_info_t;

Которая сама содержит указатели на бинарные файлы драйверов и файлы Info.plist, загруженные в память. Для загрузки драйверов и построения узла карты памяти дерева устройств загрузчик рекурсивно ищет в разделе поддержки FAT32 любые пакеты расширений ядра (kext), загружая пары бинарных файлов и Info.plist для каждого найденного пакета. Вот пример структуры пакета kext:

SomeDriver.kext
	└── Contents
    	├── Info.plist
        └── MacOS
        	└── SomeDriver
        └── PlugIns
        	└── SomeOtherDriver.kext

После реализации загрузки драйверов в загрузчике я смог загружаться с неизменённых разделов установщика и системы Mac OS X, что упростило процесс установки и сделало Wii ещё больше похожей на настоящий Mac.

Упрощение ядра

Благодаря переносу драйверов из ядра, количество необходимых модификаций ядра для запуска системы на Wii сократилось до следующих:

  • исправленная настройка BAT для адреса ввода-вывода Wii и памяти кадрового буфера;

  • поддержка получения базового адреса ввода-вывода из узла дерева устройств с именем «hollywood»;

  • исправления согласованности кэша кадрового буфера.

Отделение драйверов от ядра упрощает понимание ядра, сокращает время сборки при разработке драйверов и открывает путь для поддержки таких систем, как Mac OS X 10.1 Puma, в которой несколько семейств драйверов были перенесены из ядра в корневую файловую систему.

Заключительные мысли

Есть что-то глубоко удовлетворяющее в достижении чего-то, в чем вы поначалу даже не были уверены.

Идея этого проекта впервые пришла мне в голову в 2013 году — когда я был второкурсником колледжа. Более десяти лет она оставалась на заднем плане; легко откладывать подобные проекты, особенно когда ваша основная работа уже связана с решением технических проблем.

В прошлом году, когда я увидел, что Windows NT портирована на Wii, я почувствовал прилив мотивации. Даже если бы мой недостаток опыта на начальном уровне привёл к неудаче, попытка реализовать этот проект всё равно стала бы возможностью узнать что-то новое.

В итоге я узнал (и достиг) гораздо большего, чем ожидал, — и, что, возможно, ещё важнее, мне напомнили, что стоит браться именно за проекты, которые кажутся недостижимыми.