Pull to refresh

Основы работы с IOKit. Тонкости программирования драйверов

Reading time 8 min
Views 20K

В рамках поддержки блога разработки под Mac OS X, я представляю свою статью о низкоуровневой разработке под Mac OS X. Обычно тема разработки драйверов не столь популярна, однако и здесь Mac OS X выгодно выделяется из ряда прочих операционных систем. Да, писать драйвера для Mac O S X – просто! Проще чем когда-либо ранее!


Экскурс в глубины теории

Очень часто я слышу фразы, что Mac OS X – это Linux. Или же что OS X основано на ядре FreeBSD и посему драйвера легко портировать из одной системы в другую. А вот и нет! Очень часто концепция разработки под Mac OS X противоречит классическим принципам, к примеру вместо традиционных спин локов (spin lock) Apple предлагает использовать примитив IOCommandGate для синхронизации доступа. Также достаточно жестко регламентируется использование ядерной памяти. Примерный совет от Apple выглядит так: «Выделяйте столько памяти в ядре, сколько вам необходимо использовать и не байтом больше. Предпочитайте основную часть работы с большими буферами памяти переносить в пользовательский режим (user space)».

Сердцем, т.е. ядром, Mac OS X является XNU. XNU – это гибридное ядро, которое сочетает в себе множество преимуществ, а также недостатков, монолитных и микроядерных ОС. Собственно, XNU состоит из микроядра mach, разработанного институтом Карнеги Мелоуна, подсистемы Posix из BSD 4.3 (позже эта часть синхронизировалась с последней веткой FreeBSD, дописывалась самой Apple, а также 3-ми лицами), и объектно-ориентированного фреймворка IOKit, на который положена ответственность взаимодействовать с аппаратной частью Mac’ов. Стоит упомянуть, что ядро mach предоставляет операционной системе следующие сервисы: планировщик потоков и процессов, вытесняющую многозадачность (pre-emptive), механизм виртуальной памяти, защиту памяти, mach-IPC (через передачу сообщений), отладчик ядра.


Кое-что об IOKit

И так, что же из себя представляет IOKit? Как и многие другие наработки, IOKit – это наследие NeXT Computer (гениальные люди видимо работали в NeXT). IOKit – это объектно-ориентированный фреймворк реализующий драйверную модель Mac OS X. Написан IOKit на С++, вернее на его урезанной версии – Embedded C++ [1]. Проще говоря, это старый добрый С++ без множественного наследования, RTTI, шаблонов и исключений. Также в IOKit запрещается определять конструкторы и деструкторы классов. Впрочем, если очень хочется, то использовать можно все, кроме RTTI и исключений, но Apple за это вас по голове не погладит! Ранее IOKit был написан на Objective C, как и фреймворк Cocoa, и назывался Driver Kit. Однако, для упрощения разработки драйверов 3-ми лицами, Driver Kit переписали на C++. Впрочем, по словам одного из основных разработчиков IOKit, Годфри ван дер Линдена (Godfrey van der Linden), это решение было ошибочным. Тем не менее, в IOKit можно невооруженным взглядом увидеть наследие Objective C, к примеру механизм подсчета ссылок и retain/release, именование классов (приставки OS и IO), именование методов классов и многое другое. Еще один интересный факт: Godfrey van der Linden — студент университета Nicta, того самого австралийского НИИ, который открыл исследовательский проект Darbat [2].

Для того, чтобы приступить в делу вам достаточно установленного XCode SDK, собственно самого IDE XCode, а также Terminal.app для загрузки драйверов. При создании нового проекта, выберете Generic IOKit Driver и XCode создаст для вас пустой проект. Вам остается только добавить код драйвера, откомпилировать его, и загрузить с помощью следующих строк (предполагается, что драйвер называется SampleDriver.kext):

sudo chmod –R 0755 ./SampleDriver.kext
sudo chown –R root:wheel ./SampleDriver.kext
sudo kextload –t ./SampleDriver.kext


Как видно из вышеприведенных строк: загрузка драйвера выполняется с правами root с помощью сторонней программы kextload (из набора kext utils, который в свою очередь входит в xnu utils). Драйвер, как и любое приложение Mac OS X (за исключением простых консольных приложений), представляет из себя bundle, т.е. директорию в которой хранятся данные, которые непосредственно относятся к драйверу, а именно: Info.plist файл, файл с локализированными строками и конечно же сам бинарный файл драйвера (SampleDriver.kext/Contents/MacOS/SampleDriver). Также драйвер может содержать дополнительные ресурсы (SampleDriver.kext/Contents/Resources), либо другие драйверы (SampleDriver.kext/Contents/PlugIns).

IOKit в своей концепции активно эксплуатирует две парадигмы ООП: наследование и полиморфизм. Наследование позволяет уменьшить количество использованной ядерной памяти: любой IOKit драйвер, наследует определенный базовый класс, который специфичен для каждого стека устройств в системе. Например для драйверов LAN устройств – это IOEthernetController, WAN устройств – IO80211Controller, звуковых карт – IOAudioDevice и т.д. Механизм виртуальных функций, позволяет драйверу легко переопределить определенные методы базово класса, таким образом реализуя необходимую функциональность.


Проба пера

Любой класс в IOKit драйвере должен быть прямо или косвенно унаследован от класса OSObject. Этот класс обеспечивает подсчет ссылок, поддержку псевдо-RTTI за счет макросов и дополнительной метаинформации, примитивы для создания инстанса класса (перегруженный оператор new) в среде IOKit и многое другое. Самый простой IOKit класс выглядит следующим образом:

*.h файл:
class MyIOKitClass: public OSObject
{
OSDeclareDefaultStructors(MyIOKitClass)

public:
virtual bool init();

protected:
virtual void free()

private:
void *fSimpleMember;

};


*.cpp файл:
bool MyIOKitClass::init()
{
if (!OSObject::init())
return false;

// TO-DO: Add basic initialization here
fSimpleMember = NULL;

return true;
}
void MyIOKitClass::free()
{
// TO-DO: Add deinitialization code here
if (fSimpleMember)
{
// ...
}

OSObject::free();
}

Наш класс MyIOKitClass унаследован от класса OSObject, в нем переопределены два метода:
  1. bool init() – данный метод будет вызван тогда, когда будет создан новый инстанс класса. В методе рекомендуется проводить начальную инициализацию полей класса, в общем все то, что вы бы сделали в конструкторе.
  2. void free() – данный метод будет вызван во время того, когда будет вызван последний release для экземпляра класса и его счетчик ссылок будет равен 0. Т.е. роль метода – деструктор класса.


Также в переопределенных методах необходимо вызывать методы класса-предка, для того, чтобы IOKit выполнял основную часть работы по инициализации/уничтожению инстанса класса за вас.

От OSObject могут быть унаследованы лишь вспомогательные классы, которые необходимы при реализации более крупной системы. Главный класс драйвера (да и любой другой класс, который претендует на то, чтобы предоставлять операционной системе определенные сервисы) прямо или косвенно наследуется от класса IOService.

IOService помогает реализовать в вашем драйвере поддержку Plug’n’Play, Power Management (организовывает взаимодействие с Power Domain операционной системы), взаимодействие с IORegistry и другими драйверами системы. Здесь следует также упомянуть про IORegistry – это не что иное, как динамическое дерево устройств Mac OS X. Строить это дерево начинает загрузчик ОС (boot loader), ядро ОС, а также вспомогательные драйвера. К примеру драйвер IOACPIPlatformExpert строит в IORegistry дерево всех PCI устройств в системе, основываясь на информации из ACPI таблиц, а также APIC контроллера прерываний.

IOService имеет два достаточно полезных метода, которые вам наверняка следует реализовывать в большинстве ваших драйверов:
  1. bool start( IOService *provider ) – данный метод будет вызван во время старта вашего драйвера. Здесь вы можете поместить код, который будет заниматься выделением ресурсов, необходимых драйверу.
  2. void stop( IOService *provider ) – этот метод будет вызван во время останова вашего драйвера.


В сигнатуре вышеприведенных методов, вы могли заметить параметр provider. И так, что же он значит, и для чего нужен? Все очень просто, как было сказано ранее, в Mac OS X существует динамический список устройств IORegistry. В нем существует связка наб-драйвер. Любой драйвер, может регистрировать в системе свои сервисы и делать их набами (nubs) для любых других драйверов. Итак, наб – это класс, который другой драйвер системы зарегистрировал в IORegistry, и теперь этот наб может быть передан как провайдер в ваш драйвер. К примеру, для драйверов PCI устройств набом является инстанс класса IOPCIDevice. Информация о набе, а также идентификаторы устройства задаются в Info.plist файле драйвера. Следующая секция Info.plist файла иллюстрирует этот прием (данный код был взят из моего сетевого драйвера):

   <dict>
      <key>Realtek RTL8111B/RTL8168 NIC</key>
      <dict>
        <key>CFBundleIdentifier</key>
         <string>rtl.r1000.nic.ext</string>
         <key>IOClass</key>
         <string> RealtekR1000</string>
         <key>IOPCIMatch</key>
         <string>0x816910ec 0x816710ec 0x816810ec 0x813610ec</string>
         <key>IOProbeScore</key>
         <integer>500</integer>
         <key>IOProviderClass</key>
         <string>IOPCIDevice</string>
      </dict>
   </dict>



Опишем наиболее значимые параметры:
  • IOProviderClass – указывает тип наба, который IOKit передаст в метод start нашего драйвера.
  • IOClass – это имя класса нашего драйвера. Если обратится к нашему предыдущему примеру — это MyIOKitClass.
  • CFBundleIdentifier – bundle идентификатор нашего драйвера, проще говоря этот параметр однозначно идентифицирует наш драйвер в системе.
  • IOPCIMatch – список DeviceID-VendorID PCI устройств, которые будет обслуживать наш драйвер. К примеру возьмем 0x816810ec, 0x8168 – это DeviceID нашего сетевого контроллера, 0x10ec – это VendorID, в данном случае Realtek.


Остальные параметры – тема для еще одной статьи ;) Примерный же код нашего метода start будет выглядеть так:bool RealtekR1000::start(IOService *provider)
{
   if (!IOEthernetController::start(pciDev))
      return false;

   IOPCIDevice *pciDev = NULL;

   pciDev = OSDynamicCast(IOPCIDevice, provider);
   if (!pciDev)
      return false;

   pciDev->retain();   
   pciDev->open(this);

   // Add initialization of device here

   return true;
}


Я надеюсь, что данный код более-менее понятен для вас, добавлю лишь, что OSDynamicCast – это макрос, который заменяет отсутствие RTTI в IOKit за счет использования дополнительной метаинформации.


Сделаем что-нибудь полезное?

Все что вам необходимо сделать, чтобы создать свой сетевой драйвер:
  • Создать скелет для драйвера, как было описано выше.
  • Унаследовать класс драйвера от класса IOEthernetController.
  • Переопределить необходимые методы в своем классе (всего около 10-12).
  • Задать необходимые параметры в Info.plist файле.


И драйвер готов! Как видим, Mac OS X в очередной раз поражает нас изяществом. Здесь нет никаких огромных *.inf файлов, никакой системы минипортов, и прочих неочевидных вещей. Портирование драйвера для сетевого контроллера с Linux в Mac OS X заняло у меня около 7 дней, при том, что я должен был также освоить основы IOKit с нуля.


На этом этапе я намеряно делаю паузу до следующей статьи, если конечно же кому-то будет интересна данная тематика на хабре. Я опустил такие интересные вещи, как: Power Management, работа с IO портами и DMA, взаимодействие с user space и т.д. Желающим освоить IOKit могу порекомендовать начать с туторила IOKit Fundamentals [3], а заинтересованным – список тем о низкоуровневой разработке, которые им были бы наиболее интересны. Прошу оставлять свои отклики и пожелания ;)


Полезные ссылки


[1] Embedded C++
[2] Darbat project
[3] IOKit Fundamentals
Tags:
Hubs:
+32
Comments 23
Comments Comments 23

Articles