As part of supporting blog development under Mac OS X, I submit my article on low-level development under Mac OS X. Usually, driver development is not so popular, but 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!
Excursion into the depths of the theory
Very often I hear phrases that Mac OS X is Linux. Or that OS X is based on the FreeBSD kernel and therefore the drivers are easy to port from one system to another. And no! Very often, the concept of development for Mac OS X contradicts the classical principles, for example, instead of traditional spin locks (spin lock) Apple suggests using the IOCommandGate primitive to synchronize access. Also quite strictly regulated the use of nuclear memory. Apple's approximate advice looks like this: “Allocate as much memory in the kernel as you need to use and not byte more. Prefer the bulk of work with large memory buffers to transfer to user mode (user space). "
The heart, i.e. the kernel, Mac OS X is XNU. XNU is a hybrid kernel that combines many advantages, as well as disadvantages, of monolithic and microkernel OSs. Actually, XNU consists of the mach microkernel developed by the Carnegie Malone Institute, the Posix subsystem from BSD 4.3 (later this part was synchronized with the last FreeBSD branch, added by Apple itself, as well as by 3 persons), and the IOKit object-oriented framework responsibility to interact with Mac hardware. It is worth mentioning that the mach core provides the following services to the operating system: a thread and process scheduler that pushes multitasking (pre-emptive), a virtual memory mechanism, memory protection, mach-IPC (through message passing), a kernel debugger.
')
Something about IOKit
And so, what exactly is IOKit? Like many other things, IOKit is a legacy of NeXT Computer (brilliant people apparently worked in NeXT). IOKit is an object-oriented framework implementing the Mac OS X driver model. IOKit is written in C ++, or rather, in its reduced 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, then you can use everything except RTTI and exceptions, but Apple will not caress your head for it! 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 the naked eye the legacy of Objective C, for example, the mechanism of reference counting and retain / release, the naming of classes (OS and IO prefixes), the naming of class methods, and much more. Another interesting fact: Godfrey van der Linden is a student at the University of Nicta, the same Australian research institute, who opened the research project Darbat [2].
In order to get down to business you just have the Xcode SDK installed, the IDE Xcode itself, as well as Terminal.app for downloading the drivers. When creating a new project, select Generic IOKit Driver and Xcode will create an empty project for you. All you have to do is add the driver code, compile it, and load it with 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 rights using the third-party kextload program (from the utils kext set, which in turn is included in xnu utils). A driver, like any Mac OS X application (with the exception of simple console applications), is a bundle, i.e. the directory in which data is stored that directly relates to the driver, 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 paradigms of the PLO: inheritance and polymorphism. Inheritance allows you to reduce the amount of used nuclear memory: any IOKit driver inherits a certain base class that is specific to each stack of devices in the system. For example, for LAN device drivers, these are IOEthernetController, WAN devices — IO80211Controller, sound cards — IOAudioDevice, etc. The mechanism of virtual functions allows the driver to easily override certain methods of the base class, thus implementing 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, support for pseudo-RTTI at the expense of macros and additional meta-information, primitives for creating an instance of the class (overloaded operator new) in the IOKit environment, and much more. The simplest IOKit class looks like this:
* .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 OSObject class, it overrides two methods:
- bool init () - this method will be called when a new instance is created. The method recommends initial initialization of the class fields, in general, everything that you would have done in the constructor.
- void free () - this method will be called at the time when the last release is called for an instance of the class and its reference count will be 0. That is, the role of the method is the destructor of the class.
Also in the overridden methods, you must call the methods of the ancestor class in order for IOKit to do the bulk of the work of initializing / destroying the class instance for you.
Only auxiliary classes can be inherited from OSObject, which 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 to implement in your driver support Plug'n'Play, Power Management (organizes interaction with the Power Domain operating system), interaction with IORegistry and other system drivers. Here I should also mention IORegistry - this is nothing more than the dynamic tree of Mac OS X devices. The boot loader, the OS kernel, and the auxiliary drivers start building 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 the APIC interrupt controller.
IOService has two quite useful methods that you probably should implement in most of your drivers:
- bool start (IOService * provider) - this method will be called during the start of your driver. Here you can put the code that will be engaged in the allocation of resources required by the driver.
- void stop (IOService * provider) - this method will be called during the stop of your driver.
In the signature of the above methods, you might notice the provider parameter. And so, what does it mean, and what is it for? It's very simple, as mentioned earlier, in Mac OS X there is a dynamic list of IORegistry devices. There is a bunch 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 system driver has registered with IORegistry, and now this nab can be passed as a provider to your driver. For example, for PCI device drivers, a nabom is an instance of the IOPCIDevice class. Nabe information and device identifiers are specified in the Info.plist driver file. The following section of the Info.plist file illustrates this technique (this code was taken from my network driver):
< 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 >We describe the most significant parameters:
- IOProviderClass - indicates the type of naba that IOKit will pass to the start method of our driver.
- IOClass is the name of our driver class. If you refer 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 is 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 subject to 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 will add only that OSDynamicCast is a macro that replaces the absence 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 its class (about 10-12 in total).
- Set the necessary parameters in the Info.plist file.
And the driver is ready! As you can see, Mac OS X once again amazes us with grace. There are no huge * .inf files, no miniport system, and other non-obvious things. Porting a driver for a network controller with Linux on Mac OS X took me about 7 days, despite the fact that I also had to learn the basics of IOKit from scratch.
At this stage, I deliberately pause until the next article, if, of course, someone will be interested in this topic on the desktop. I omitted such interesting things as: Power Management, work with IO ports and DMA, interaction with user space, etc. I can recommend IOKit Fundamentals [3] for those who wish to learn IOKit, and for interested users, a list of low-level development topics that they would be most interested in. Please leave your feedback and suggestions;)
useful links
[1]
Embedded C ++[2]
Darbat project[3]
IOKit Fundamentals