The Info.plist File

The Macintosh platform has always supported a plug-and-play design for devices, which requires no configuration after installing the driver. Mac OS X, with the help of the I/O Kit, is no exception. Unlike the kernel extension that we developed in the previous chapter, which loads as soon as it is added to the system, a driver is loaded only when one of the devices that it supports is connected to the computer. In this way, even though there may be hundreds of drivers installed on a system, only those that correspond to hardware that is actually connected to the computer will be loaded and taking up memory.

In Chapter 3, we saw that a kernel extension requires a property list file to define such things as its entry points. The property list is even more important for a kernel extension that implements an I/O Kit driver. For an I/O Kit driver, the property list specifies the list of hardware devices that the driver is able to support. Each device supported by a driver contains its own “personality” in the property list, which consists of a “matching dictionary” that consists of an array that describes each hardware device to match against. The driver will be loaded only if one of the hardware devices described in its matching dictionary is connected to the computer.

One of the most important values contained in each matching dictionary is the “IOProviderClass,” which defines the class type of the driver's provider, such as IOUSBDevice or IOPCIDevice. Whenever a new hardware device is connected to the computer, the I/O Kit creates an appropriate nub for that device, and then begins the process of finding a suitable driver for that nub. For example, a USB device connection is handled as follows:

  1. The user connects a USB device to the computer.
  2. A new instance of IOUSBDevice is created to represent the device.
  3. The I/O Kit iterates over all drivers that contain a matching dictionary listing a provider class of IOUSBDevice.
  4. The IOUSBDevice examines the entire contents of the matching dictionary for the driver.
  5. If the requested properties of the matching dictionary correspond to the properties of the device, the driver is added to a list of potential drivers for the device.

Importantly, it is the provider class that decides whether a driver is suitable for a particular hardware device. It does this by examining the properties from a potential driver's matching dictionary; however, the particular properties that are used will be specific to the driver family. For example, a USB driver may match against a specific vendor ID and product ID of the USB device, or may match against a generic class of device such as any USB keyboard. A PCI device may be matched on the vendor ID and device ID specified in the device's PCI configuration space or against any PCI class, such as a network card or a display card.

Following the preceding steps, the I/O Kit has narrowed the list of drivers for the device down to an array of potential matches. To determine the best driver for the hardware device, the I/O Kit uses the notion of a “probe score” for each driver. Each driver nominates a probe score that provides a relative measure, in some way, of that driver's suitability for the device. The driver with the highest probe score is the one that is ultimately chosen to work with the device.

The driver's probe score can be set in two ways. One way is for a driver to provide a probe score in its matching dictionary. For example, a company that manufactures a custom USB keyboard could provide a driver whose matching dictionary matches against the exact product ID of the company's keyboard, with a probe score that is higher than the system's default keyboard driver. Another way that the probe score can be set is through “active matching,” in which the I/O Kit instantiates each potential driver, temporarily attaches it to the hardware device, and provides it with a chance to interrogate the device and determine its probe score. During probe, the driver has full access to the hardware, so it can perform as much interrogation of the device as is required to determine its suitability to drive that device. The driver can adjust its probe score, or more commonly, can use the probe method to opt-out of matching against a device if it determines that it is unable to work with the connected hardware.

For example, a driver's implementation of probe could determine the version of firmware that is loaded on the device, and if the firmware is of a later version than that supported by the driver, it could refuse to load. Failing during the probe stage is more efficient than failing later on, when the driver has been selected as the driver for the device, because the I/O Kit does not need to continue on and start your driver, although, in both cases, the I/O Kit will continue by attempting to load the driver with the next highest probe score.

While in almost all cases only one driver is attached to a device, the I/O Kit does allow multiple drivers to be loaded for a single device. By adding an extra key to a driver's matching dictionary known as a “match category,” the I/O Kit will load the driver with the highest probe score in each match category and attach it to the device. If no match category is given in the driver's matching dictionary, a default category is assumed.

The matching process is recursive, and drivers may themselves be nubs that act as a provider to other classes. For example, the driver for a PCI card that implements a USB host controller would match against an IOPCIDevice, but would create IOUSBDevice instances of its own to represent devices that have been connected to its own ports. In this way, the IOUSBDevice instances created by the driver would in turn become the provider class for other drivers, as shown in Figure 4-2. Rather than instantiate a class of type IOUSBDevice directly, a driver of this type would likely provide its own implementation of a class that inherits from IOUSBDevice, which is shown in Figure 4-2 as “MyUSBDevice.”

Any driver that uses an instance of MyUSBDevice as a provider would talk to the provider through its standard IOUSBDevice interface, but the use of virtual methods would allow the MyUSBDevice implementation to override these methods and provide its own implementation.

images

Figure 4-2. An example of a driver that is also a nub, creating objects that serve as the provider class to other drivers

Another example of a driver that acts as a nub, which is in fact a far more common scenario, is a driver that accepts connections from user applications. As mentioned earlier, for each connection that a user application makes to a driver, the I/O Kit instantiates a new object known as a “user client” to handle the control requests from the application and to pass them on to the driver. Like the main driver class, the user client class is also an I/O Kit service and it inherits from the same IOService base class as any other driver object. Each user client uses the main driver object as its provider class. Unlike the main driver, however, a user client doesn't need to go through the matching stage because the driver nominates the specific class name of its user client.

Listing 4-1. The Driver Personalities for a Hypothetical External Disk, Containing Matching Dictionaries for both FireWire and USB Connections

<key>IOKitPersonalities</key>
<dict>
        <key>MyExternalDiskFireWire</key>
        <dict>
                <key>CFBundleIdentifier</key>
                <string>com.mycompany.driver.MyExternalDiskDriver</string>
                <key>IOClass</key>
                <string>com_mycompany_driver_MyExternalDiskDriver</string>
                <key>IOProviderClass</key>
                <string>IOFireWireUnit</string>
                <key>Unit_SW_Version</key>
                <integer>1111</integer>
                <key>Unit_Spec_ID</key>
                <integer>2222</integer>
        </dict>
        <key>MyExternalDiskUSB</key>
        <dict>
                <key>CFBundleIdentifier</key>
                <string>com.mycompany.driver.MyExternalDiskDriver</string>
                <key>IOClass</key>
                <string>com_mycompany_driver_MyExternalDiskDriverUSB</string>
                <key>IOProviderClass</key>
                <string>IOUSBDevice</string>
                <key>idProduct</key>
                <integer>3333</integer>
                <key>idVendor</key>
                <integer>4444</integer>
                <key>IOProbeScore</key>
                <integer>9000</integer>

        </dict>
</dict>

Listing 4-1 shows the matching dictionary for a hypothetical external disk device that features both FireWire and USB connections. As such, it contains two entries in its matching dictionary, the first of which matches against a specific FireWire device, and another that matches against a specific USB device. The driver class com_mycompany_driver_MyExternalDiskDriver will be instantiated and given a chance to probe the device whenever a FireWire device with a unit software version of 1111 and a unit spec ID of 2222 is plugged in to the computer. Likewise, the driver class com_mycompany_driver_MyExternalDiskDriverUSB will be instantiated and given a chance to probe the device whenever a USB device with a product ID of 3333 and a vendor ID of 4444 is plugged into the computer. The USB device will have a default probe score of 9000, which should make it the preferred driver for this device.

The Driver Class

As we saw in the previous section, when the I/O Kit loads a driver, it does so by instantiating a class that is designated in the driver's property list. This class must be a subclass of the IOService class, either directly or by subclassing a class that is itself a child of the IOService class. The IOService class provides virtual methods that are called at various points during the lifetime of the driver—for example, when it is loaded and initialized, when it should probe its provider, and when the driver is stopped. Because these methods are declared as virtual methods in the definition of the IOService class, they can be easily overridden in the custom driver class that inherits from IOService.

At this point, it may be a good time to put what you have learned into practice by creating a simple I/O Kit driver. To begin, open Xcode and create a new project based on the “IOKit Driver” template. When prompted for a product name, enter “IOKitTest”. You can use the company identifier “com.osxkernel”, which is a domain name that has been registered for the purposes of this book. Xcode will create a project for you with two files, a C++ implementation file named “IOKitTest.cpp” and its corresponding header file named “IOKitTest.h”.

Let's begin by declaring the class definition of our driver. Given that we are implementing a generic driver, and not one that provides specialized functionality such as a serial port or disk storage, we will define our driver's main class as a subclass of IOService and not one of the more specialized classes that the I/O Kit provides. Enter in the text from Listing 4-2 as the contents of IOKitTest.h.

Listing 4-2. The “IOKitTest.h” Tutorial

#include <IOKit/IOService.h>

class com_osxkernel_driver_IOKitTest : public IOService
{
        OSDeclareDefaultStructors(com_osxkernel_driver_IOKitTest)
        
public:
        virtual bool    init (OSDictionary* dictionary = NULL);
        virtual void    free (void);
        
        virtual IOService*      probe (IOService* provider, SInt32* score);
        virtual bool    start (IOService* provider);
        virtual void    stop (IOService* provider);
};

The contents of the header file should be fairly straightforward, with the possible exception of the macro OSDeclareDefaultStructors. As you will recall from Chapter 3, the I/O Kit is implemented in a subset of the C++ language that does not include exceptions and runtime type information. The macro OSDeclareDefaultStructors is needed as a consequence of both of these limitations; it provides the declaration of the class's constructor and destructor and metadata that provides the custom implementation of the I/O Kit's version of runtime type information. We discuss this in greater depth later in this chapter.

images Note You may be wondering why we used such an elaborate name for our class. The kernel has a global namespace into which all symbols (including class names, functions, and global variables) exported by any active kernel extensions are loaded. The kernel will refuse to load an extension that contains symbols that collide with an extension that is already loaded, and so to avoid this, Apple recommends that all global functions, classes, and variables are decorated with a reverse-DNS naming scheme.

The implementation of the driver class should be placed in the file named “IOKitTest.cpp.” The contents of this file are given in Listing 4-3.

Listing 4-3. The “IOKitTest.cpp” Tutorial

#include "IOKitTest.h"
#include <IOKit/IOLib.h>

// Define the superclass.
#define super IOService

OSDefineMetaClassAndStructors(com_osxkernel_driver_IOKitTest, IOService)


bool com_osxkernel_driver_IOKitTest::init (OSDictionary* dict)
{
        bool res = super::init(dict);
        IOLog("IOKitTest::init ");
        return res;
}

void com_osxkernel_driver_IOKitTest::free (void)
{
        IOLog("IOKitTest::free ");    
        super::free();
}

IOService* com_osxkernel_driver_IOKitTest::probe (IOService* provider, SInt32* score)
{
        IOService *res = super::probe(provider, score);
        IOLog("IOKitTest::probe ");    
        return res;
}

bool com_osxkernel_driver_IOKitTest::start (IOService *provider)
{
        bool res = super::start(provider);      
        IOLog("IOKitTest::start ");    
        return res;
}

void com_osxkernel_driver_IOKitTest::stop (IOService *provider)
{
        IOLog("IOKitTest::stop ");    
        super::stop(provider);
}

images NoteIt is a convention of the I/O Kit to define a macro named “super” as the name of the superclass of the current class. This allows a method to be delegated to the superclass implementation easily, as is shown in Listing 4-3.

Finally, we need to define the driver's matching dictionary and library dependencies through the Info.plist file. Add a new dictionary key named “IOKitTest” to the IOKitPersonalities dictionary that contains the following values:

images

We also need to add two entries to the OSBundleLibraries dictionary:

images

The final version of the project's property list is shown in Figure 4-3.

images

Figure 4-3. The property list, including the matching dictionary, for the IOKitTest tutorial

Given that the purpose of a driver is to control hardware, and that the I/O Kit will load a driver only when its hardware device is present, you may be wondering how it is possible to test this driver. Thankfully, the I/O Kit provides a special nub known as IOResources that can be used as the provider class of a driver that has no hardware device, such as the tutorial driver listed here. In a system, there will be multiple drivers matching against the IOResources nub, and so to allow more than one driver to attach itself to IOResources, the IOMatchCategory key in the driver's matching dictionary must be defined.

Since the I/O Kit allows a nub to have one driver attached to it per match category, specifying a unique category allows the driver to load and doesn't prevent other drivers from matching against the IOResources provider class after we have loaded. To complete the matching dictionary, we need to specify matching criteria that are unique for the provider class. If the provider class is a USB device, this may take the form of a USB product ID and vendor ID. In the case of our tutorial, the provider class is IOResources. A single key named “IOResourceMatch” is the matching criteria used by IOResources. In the sample driver's matching dictionary, this value is set to “IOKit”. This tells the provider class to defer loading the driver until the IOKit subsystem has been fully loaded and initialized during system startup.

You can now build the IOKitTest project, which can be loaded using the same “kextload” command that was used in Chapter 3. If you open the “Console” utility and examine the contents of the “kernel.log” file, you should see that the various methods of the driver's class have been called.

The order in which the methods of the driver class are called is as follows:

  1. init(). This method is guaranteed to be called before any other method in the class. Its purpose is the same as a constructor of a C++ class. A driver's init() method should first call the implementation provided by the superclass, and if this fails, it should abort immediately. This method is passed one parameter, a copy of the matching dictionary corresponding to the selected driver personality in the Info.plist file. If this method succeeds, it should return true; otherwise, it should return false.
  2. probe(). This method is called during matching to give the driver a chance to examine the hardware device, which is passed to the method through the argument named “provider.” Although the parameter “provider” is a pointer to an IOService, it can be cast to the more specialized provider class specified in the matching dictionary (such as IOUSBDevice). The driver's implementation of probe() should first call the superclass's implementation, and if this succeeds, perform any investigation of the hardware required to determine whether the driver is able to control it. If the driver is unable to control the hardware, it should return NULL from the probe() method; otherwise, it should return an instance of the IOService subclass that should control this device. In almost all cases, this method will return the current IOService instance (“this”).
  3. start(). If the previous call to probe() succeeded, and the driver has been chosen as being the best suited to control the hardware device (based on its probe score), its start() method is called. The implementation should first call the superclass's implementation of start(), and if this fails, it should abort immediately. The driver should use the start() method to configure the hardware for operation, and should initialize any resources that it needs while running. If for any reason the method fails and the driver is unable to go on to control the hardware, the method should return false. The I/O Kit will then provide the driver with the next highest probe score a chance to control the device.
  4. stop(). This method isn't called until either the device is removed or the driver is manually unloaded. This method is the opposite of start(); any configuration or allocation that was performed in the start() method should be released when stop() is called. Finally, the implementation should call the superclass's implementation of stop().
  5. free(). Finally, before the driver's object is destroyed, the I/O Kit calls its free() method. Its purpose is the same as a destructor of a C++ class. This provides the driver with a chance to release any resources that were allocated in its init() method. This method is called even if a driver was never selected as the best match for a particular device. The implementation should end by calling the superclass's implementation of free().

A consequence of limited exception support is that rather than using the traditional C++ approach of performing object initialization in the class's constructor and throwing an exception if an error occurs, the initialization of I/O Kit objects is performed in a custom method named “init,” which returns a Boolean value to signal success.

IORegistryExplorer

Apple provides a very useful tool for visualizing the drivers loaded on a system, known as “IORegistryExplorer.” This is included as part of the Xcode tools. IORegistryExplorer displays a graphical representation of the drivers that are currently loaded on the system and their relationship to other drivers. This relationship is shown as a hierarchical representation, with a provider class having a parent relationship to the clients that are connected to it.

IORegistryExplorer shows a representation of an entity known as the I/O Registry. If you're coming from a Windows background, don't confuse the I/O Registry with the Windows Registry. The I/O Registry is a tree of I/O Kit objects that is created when the system is started, and then it dynamically grows or shrinks as hardware devices and their corresponding drivers are loaded or unloaded from the system. Unlike the Windows Registry, the I/O Registry is never written to disk or saved between reboots of the computer.

The IOService plane includes all objects in the I/O Registry. As such, it can be a little overwhelming when trying to locate a particular driver. To help find a particular driver in the I/O Registry, IORegistryExplorer provides a search field that can be used to filter the objects shown to those whose name matches a particular string.

IORegistryExplorer also displays the property table for each driver object. When the I/O Kit loads a driver, it initializes its property table to the contents of the matching dictionary that corresponds to the driver personality that was loaded; this corresponds to the OSDictionary object that was passed to the driver class's init() method. As the driver runs, it may manipulate its property list by adding or removing additional key/value pairs or by changing the value of a particular key. These changes are local to the driver instance (and so if the same driver is loaded multiple times for several devices in the system, they each have their own property table). Figure 4-4 shows IORegistryExplorer for the sample IOKit driver that we developed earlier.

images

Figure 4-4. IORegistryExplorer displaying the IOKitTest sample. The property table shown on the right was initialized from the contents of the driver's matching dictionary in its Info.plist file.

Objects in the I/O Registry are organized into several planes. Each plane shows only those objects that share certain functionality. When launched, IORegistryExplorer will default to showing the relationship of objects in the “IOService” plane, which includes all objects in the I/O Registry. Some of the other planes that are commonly used include:

  • IODeviceTree: This is a static plane that reflects the hardware configuration of the system; it does not change as hardware devices are connected to the system. The contents of the device tree largely depend on the system motherboard and will consist of a static snapshot of the computer's configuration at boot time, including PCI slots, built-in USB ports, and any hardware controllers that are on the motherboard, such as the CPU, memory, and USB controllers.
  • IOPower. This plane shows all driver objects that have implemented power management and will receive notifications from the I/O Kit when the system is switching to a different power state such as when entering sleep mode.
  • IOUSB. This plane shows all USB devices and hubs that are connected to the system. It includes the USB devices only; the corresponding driver that has been loaded for a device can be seen in the IOService plane.

The I/O Registry is also accessible on the command line through the tool “ioreg.” Unlike IORegistryExplorer, which is available if the Xcode tools have been installed, ioreg is a standard part of a Mac OS X installation and is useful when debugging on an end-user's machine or any other system in which the developer tools are unlikely to be installed.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.164.164