The basics of working with IOKit. Subtleties of driver programming
As part of my support for the Mac OS X development blog, I present my article on low-level development for Mac OS X. Usually the topic of driver development is not so popular, however here Mac OS X stands out from a number of other operating systems. Yes, writing drivers for Mac OSX is easy! Easier than ever before!
An excursion into the depths of theory
Very often I hear phrases that Mac OS X is Linux. Or that OS X is based on the FreeBSD kernel and therefore it is easy to port drivers from one system to another. And no! Very often, the development concept for Mac OS X contradicts classical principles, for example, instead of traditional spin locks, Apple suggests using the IOCommandGate primitive to synchronize access. The use of nuclear memory is also strictly regulated. A sample advice from Apple looks like this: “Allocate as much memory in the kernel as you need to use, and not byte more. Prefer the bulk of the work with large memory buffers to be transferred to user mode. ”Heart, i.e. the core, Mac OS X is XNU. XNU is a hybrid core that combines many advantages, as well as disadvantages, monolithic and microkernel OS. Actually, the XNU consists of the mach microkernel developed by the Carnegie Melone Institute, the Posix subsystem from BSD 4.3 (later this part was synchronized with the last branch of FreeBSD, added by Apple itself, as well as by 3 persons), and the object-oriented IOKit framework on which it relies the responsibility of interacting with Mac hardware. It is worth mentioning that the mach kernel provides the following services to the operating system: thread and process scheduler, pre-emptive, virtual memory mechanism, memory protection, mach-IPC (via message passing), kernel debugger.
Something About IOKit
So, what is IOKit like? Like many other developments, IOKit is a legacy of NeXT Computer (ingenious people apparently worked in NeXT). IOKit is an object-oriented framework that implements the Mac OS X driver model. IOKit was written in C ++, or rather, in its stripped-down version - Embedded C ++ [1]. Simply put, this is the good old C ++ without multiple inheritance, RTTI, patterns, and exceptions. Also in IOKit it is forbidden to define constructors and destructors of classes. However, if you really want to, you can use everything except RTTI and exceptions, but Apple will not pat you on the head for this! IOKit was previously written in Objective-C, like the Cocoa framework, and was called the Driver Kit. However, to simplify the development of drivers by 3 persons, the Driver Kit was rewritten in C ++. However, according to one of the main developers of IOKit, Godfrey van der Linden (Godfrey van der Linden), this decision was erroneous. However, in IOKit, you can see with an unaided glance the legacy of Objective-C, for example, link counting and retain / release mechanisms, class naming (OS and IO prefixes), class method naming, and much more. Another interesting fact: Godfrey van der Linden is a student at Nicta University, the same Australian research institute that opened the Darbat research project [2].In order to get down to business you just need the installed Xcode SDK, the Xcode IDE itself, as well as Terminal.app to download drivers. When creating a new project, select the Generic IOKit Driver and Xcode will create an empty project for you. You just have to add the driver code, compile it, and download using the following lines (it is assumed that the driver is called SampleDriver.kext):
sudo chmod –R 0755 ./SampleDriver.kext
sudo chown –R root:wheel ./SampleDriver.kext
sudo kextload –t ./SampleDriver.kext
As can be seen from the above lines: the driver is loaded with root privileges using the third-party kextload program (from the kext utils set, which in turn is included in xnu utils). The driver, like any Mac OS X application (with the exception of simple console applications), is a bundle, i.e. a directory in which data that directly relates to the driver is stored, namely: the Info.plist file, the file with localized strings, and of course the binary driver file itself (SampleDriver.kext / Contents / MacOS / SampleDriver). Also, the driver may contain additional resources (SampleDriver.kext / Contents / Resources), or other drivers (SampleDriver.kext / Contents / PlugIns).
IOKit in its concept actively exploits two OOP paradigms: inheritance and polymorphism. Inheritance can reduce the amount of kernel memory used: any IOKit driver inherits a specific base class that is specific to each device stack in the system. For example, for LAN device drivers, this is IOEthernetController, WAN devices for IO80211Controller, sound cards for IOAudioDevice, etc. The mechanism of virtual functions allows the driver to easily override certain methods of the base class, thus realizing the necessary functionality.
Attempt at writing
Any class in the IOKit driver must be directly or indirectly inherited from the OSObject class. This class provides reference counting, pseudo-RTTI support through macros and additional meta-information, primitives for creating a class instance (overloaded new operator) in the IOKit environment, and much more. The simplest IOKit class is as follows:* .h file:
class MyIOKitClass: public OSObject
{
OSDeclareDefaultStructors (MyIOKitClass)
public:
virtual bool init ();
protected:
virtual void free ()
private:
void * fSimpleMember;
};
* .cpp file:
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 ();
}
Our class MyIOKitClass is inherited from the class OSObject, it overrides two methods:
- bool init () - this method will be called when a new instance of the class is created. The method recommends that you initialize the fields of the class, in general, everything that you would do in the constructor.
- void free () - this method will be called when the last release is called for the class instance and its reference count will be 0. That is the role of the method is the class destructor.
Also in overridden methods, it is necessary to call the methods of the ancestor class so that IOKit does the bulk of the work of initializing / destroying the class instance for you.
From OSObject, only auxiliary classes can be inherited that are necessary when implementing a larger system. The main driver class (and any other class that claims to provide certain services to the operating system) is directly or indirectly inherited from the IOService class.
IOService helps implement Plug'n'Play, Power Management (organizes interaction with the Power Domain operating system), interaction with IORegistry and other system drivers in your driver. IORegistry should also be mentioned here - this is nothing more than a dynamic tree of Mac OS X devices. The boot loader, the kernel of the OS, and also auxiliary drivers begin to build this tree. For example, the IOACPIPlatformExpert driver builds in IORegistry a tree of all PCI devices in the system, based on information from ACPI tables, as well as an APIC interrupt controller.
IOService has two fairly useful methods that you should probably implement in most of your drivers:
- bool start (IOService * provider) - this method will be called when your driver starts. Here you can put the code that will allocate resources necessary for the driver.
- void stop (IOService * provider) - this method will be called when your driver stops.
In the signature of the above methods, you might notice the provider parameter. And so, what does it mean, and what is it for? Everything is very simple, as it was said earlier, in Mac OS X there is a dynamic list of IORegistry devices. There is a bunch of nab driver in it. Any driver can register its services in the system and make them nubs for any other drivers. So, nab is a class that another driver of the system registered in IORegistry, and now this nab can be passed as a provider to your driver. For example, for PCI device drivers, the nab is an instance of the IOPCIDevice class. Information about the nab as well as device identifiers are set in the driver's Info.plist file. The following section of the Info.plist file illustrates this technique (this code was taken from my network driver):
We describe the most significant parameters:
- IOProviderClass - indicates the type of nab that IOKit will pass to the start method of our driver.
- IOClass is the class name of our driver. If we turn to our previous example, this is MyIOKitClass.
- CFBundleIdentifier - bundle identifier of our driver, in other words, this parameter uniquely identifies our driver in the system.
- IOPCIMatch - a list of DeviceID-VendorID PCI devices that our driver will service. For example, take 0x816810ec, 0x8168 is the DeviceID of our network controller, 0x10ec is VendorID, in this case Realtek.
The remaining parameters are the topic for another article;) The approximate code of our start method will look like this: 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;
}
I hope that this code is more or less clear to you, I’ll just add that OSDynamicCast is a macro that replaces the lack of RTTI in IOKit by using additional meta-information.
Do something useful?
All you need to do to create your network driver:- Create a skeleton for the driver, as described above.
- Inherit the driver class from the IOEthernetController class.
- Override the necessary methods in your class (total about 10-12).
- Set the necessary parameters in the Info.plist file.
And the driver is ready! As you can see, Mac OS X once again impresses us with grace. There are no huge * .inf files, no miniport system, and other unobvious things. Porting a driver for a network controller with Linux on Mac OS X took me about 7 days, while I also had to learn the basics of IOKit from scratch.
At this stage, I deliberately pause until the next article, unless of course someone is interested in this topic on the hub. I omitted such interesting things as: Power Management, work with IO ports and DMA, interaction with user space, etc. For those wishing to learn IOKit, I can recommend starting with the IOKit Fundamentals tutorial [3], and for those interested, I’ll list the topics about low-level development that they would be most interested in. Please leave your comments and suggestions;)
useful links
[1] Embedded C ++
[2] Darbat project
[3] IOKit Fundamentals