Mac OS X Internals: A Systems Approach
10.6. Creating Kernel Extensions
In this section, we will discuss how kernel extensions are created and loaded. Note that Apple urges third-party programmers to avoid programming in the kernel unless absolutely necessary. A legitimate reason for a device driver to reside in the kernel is if it handles a primary interrupt or if its primary client is kernel resident. 10.6.1. A Generic Kernel Extension
Let us create a trivial kextone that implements only the start and stop entry points. Once we can compile and load the kext, we will extend it to implement a couple of sysctl entries. We will call our kext DummySysctl. We begin with an Xcode project instantiated from the template for generic kernel extensions. Since sysctl implementation is a BSD-only endeavor, we need to specify our kext's dependency on the BSD KPI. We will use the kernel version of the printf() function to print messages in our kext. Therefore, we need libkern, which provides printf(). The key contents of the kext's Info.plist file are as follows: ... <plist version="1.0"> <dict> ... <key>CFBundleExecutable</key> <string>DummySysctl</string> <key>CFBundleIdentifier</key> <string>com.osxbook.kext.DummySysctl</string> ... <key>OSBundleLibraries</key> <dict> <key>com.apple.kpi.bsd</key> <string>8.0.0</string> <key>com.apple.kpi.libkern</key> <string>8.0.0</string> </dict> </dict> </plist>
At least certain values in the stock Info.plist file generated by Xcode must be modified or added before the compiled kext can be successfully loaded. The CFBundleIdentifier key should be set to a reverse-DNS-style name for the kext. The OSBundleLibraries key is used to enumerate a kext's dependencies. This key's value is a dictionaryspecifically, an OSDictionarythat has been serialized into the XML property list format. It may contain an empty dictionary. Beginning with Mac OS X 10.4, a kext can declare dependencies either on new-style kernel programming interfaces (KPIs) or on compatibility interfaces. KPI dependencies are specified through com.apple.kpi.* identifiers, whereas the others are specified through com.apple.kernel.* identifiers. The former start from version 8.0.0 (Mac OS X 10.4 and newer), whereas the latter end at version 7.9.9 (Mac OS X 10.3 and older). The kextstat command can be used to list the interfaces available in the current kernelthe interfaces correspond to the "fake" kexts that represent built-in kernel components such as Mach, BSD, libkern, and the I/O Kit.
A kext can also declare a dependency on a specific version of the entire kernel by using the com.apple.kernel identifier. Although this approach would provide the kext access to all available kernel interfaces, including internal ones, it is not recommended because Apple does not guarantee binary compatibility across kernel versions to such kexts. $ kextstat | egrep -e 'com.apple.(kernel|kpi)' 1 1 0x0 0x0 0x0 com.apple.kernel (8.6.0) 2 11 0x0 0x0 0x0 com.apple.kpi.bsd (8.6.0) 3 12 0x0 0x0 0x0 com.apple.kpi.iokit (8.6.0) 4 12 0x0 0x0 0x0 com.apple.kpi.libkern (8.6.0) 5 12 0x0 0x0 0x0 com.apple.kpi.mach (8.6.0) 6 10 0x0 0x0 0x0 com.apple.kpi.unsupported (8.6.0) 11 60 0x0 0x0 0x0 com.apple.kernel.6.0 (7.9.9) 12 1 0x0 0x0 0x0 com.apple.kernel.bsd (7.9.9) 13 1 0x0 0x0 0x0 com.apple.kernel.iokit (7.9.9) 14 1 0x0 0x0 0x0 com.apple.kernel.libkern (7.9.9) 15 1 0x0 0x0 0x0 com.apple.kernel.mach (7.9.9)
You can view the list of symbols corresponding to a KPI identifier by running nm on the corresponding "pseudo-extension"the various pseudo-extensions reside as plug-ins within the System kext. $ cd /System/Library/Extensions/System.kext/PlugIns $ ls AppleNMI.kext IOSystemManagement.kext ApplePlatformFamily.kext Libkern.kext BSDKernel.kext Libkern6.0.kext BSDKernel6.0.kext Mach.kext IOKit.kext Mach6.0.kext IOKit6.0.kext System6.0.kext IONVRAMFamily.kext Unsupported.kext $ nm BSDKernel.kext/BSDKernel ... U _vnode_iterate U _vnode_lookup U _vnode_mount ...
Besides core kernel components, kexts can depend on various I/O Kit families by using identifiers such as com.apple.iokit.IOGraphicsFamily, com.apple.iokit.IONetworkingFamily, com.apple.iokit.IOPCIFamily, and com.apple.iokit.IOStorageFamily. Figure 1013 shows the source for the kext. The start function is called when the kext is loaded, and the stop function is called when it is unloaded. Xcode inserts skeletal implementations of these functions in the automatically generated C file for the Generic Kernel Extension project template. We have added a printf() statement to both functions. Figure 1013. Source for the DummySysctl kernel extension
Let us now compile the kext. The benefits of Xcode are most evident in the compilation stage, since a manual compilation would need to specify the appropriate combination of compiler arguments, environment variables, linker arguments, and so on. Note that it is possible to initiate an Xcode build from the command line using the xcodebuild program. On successful compilation, the target kext bundle is created in a subdirectory of the build/ directory within the Xcode project directory. $ xcodebuild -list Information about project "DummySysctl": Targets: DummySysctl (Active) Build Configurations: Debug (Active) Release If no build configuration is specified "Release" is used. $ xcodebuild -configuration Debug -target DummySysctl === BUILDING NATIVE TARGET DummySysctl WITH CONFIGURATION Debug === ... ** BUILD SUCCEEDED ** $ ls build/Debug DummySysctl.kext Since loading a kext requires the kext bundle's contents to have root and wheel as the owner and group, respectively, a typical compile-test-debug cycle would involve copying the kext bundle from the build directory to a temporary locationsay, to /tmp/and using the chown command on the copy. As we noted earlier, besides ownership, the modifiability of objects within the bundle also mattersthe bundle's contents must not be writable by any user except root. $ sudo rm -rf /tmp/DummySysctl.kext # remove any old bundles $ cp -pr build/DummySysctl.kext /tmp/ # copy newly compiled bundle to /tmp $ sudo chown -R root:wheel /tmp/DummySysctl.kext We can use kextload to load the kext manually. $ sudo kextload -v /tmp/DummySysctl.kext kextload: extension /tmp/DummySysctl.kext appears to be valid kextload: loading extension /tmp/DummySysctl.kext kextload: sending 1 personality to the kernel kextload: /tmp/DummySysctl.kext loaded successfully
If the kext fails to load, the -t (test) option of kextload may provide information about possible problems. For example, suppose we specified an unavailable version of a dependencysay, version 7.9.9 for com.apple.kpi.libkernthen the -t option would be helpful in identifying the cause of the problem. $ sudo kextload -v /tmp/DummySysctl.kext kextload: extension /tmp/DummySysctl.kext appears to be valid kextload: loading extension /tmp/DummySysctl.kext kextload: cannot resolve dependencies for kernel extension /tmp/DummySysctl.kext $ sudo sysctl -v -t /tmp/DummySysctl.kext ... kernel extension /tmp/DummySysctl.kext has problems: ... Missing dependencies { "com.apple.kpi.libkern" = "A valid compatible version of this dependency cannot be found" }
When the -t option is specified, kextload neither loads the kext nor sends its personality to the kernel. It only performs a series of tests on the kext and determines whether it is loadable. The tests include validation, authentication, and dependency resolution.
Besides dependency resolution failures, other reasons for a kext's failure to load include incorrect file permissions, a flawed bundle structure, a missing CFBundleIdentifier property in the kext's Info.plist file, and a missing or syntactically invalid Info.plist file. We can use the kextstat command to check whether our kext is currently loaded in the kernel. $ kextstat Index Refs Address Size Wired Name (Version) <Linked Against> 1 1 0x0 0x0 0x0 com.apple.kernel (8.6.0) 2 11 0x0 0x0 0x0 com.apple.kpi.bsd (8.6.0) 3 12 0x0 0x0 0x0 com.apple.kpi.iokit (8.6.0) 4 12 0x0 0x0 0x0 com.apple.kpi.libkern (8.6.0) ... 133 0 0x5cbca000 0x2000 0x1000 com.osxbook.kext.DummySysctl (1.0.0d1) <4 2>
The value 133 in the kextstat output indicates the index at which the kext is loaded. The kernel uses these indices for tracking interkext dependencies. The second value, which is 0 in our case, shows the number of references to this kext. A nonzero reference indicates that one or more kexts are using this kext. The next value, 0x5cbca000, is the kext's load address in the kernel's virtual address space. The next two values, 0x2000 and 0x1000, represent the amounts (in bytes) of kernel memory and wired kernel memory, respectively, used by the kext. The final value in the column is a list of indices of all other kexts that this kext references. We see that DummySysctl references two kexts: the ones loaded at indices 4 (com.apple.kpi.libkern) and 2 (com.apple.kpi.bsd). We can unload the kext manually by using the kextunload command. $ sudo kextunload -v /tmp/DummySysctl.kext kextunload: unload kext /tmp/DummySysctl.kext succeeded
In a deployment scenario, one does not have to run kextload or kextunload manuallykexts are loaded automatically when they are needed and unloaded when they are not being used.
The output from the printf() statements we inserted in our kext should appear in /var/log/system.log. $ grep DummySysctl_ /var/log/system.log Mar 14 17:32:48 g5x4 kernel[0]: DummySysctl_start Mar 14 17:34:48 g5x4 kernel[0]: DummySysctl_stop
10.6.2. Implementing Sysctl Variables Using a Generic Kext
Let us extend our generic kext from Section 10.6.1 to implement a sysctl node with two variables: an integer and a string. We will call the new node osxbook, and it will have the following properties.
When our sysctl kext is loaded, the kernel's sysctl hierarchy would look like the one shown in Figure 1014, with possibly other top-level categories depending on the kernel version. Figure 1014. The kernel's sysctl hierarchy
The most general way to create a sysctl variable is to use the SYSCTL_PROC() macro, which allows a handler function to be specified for the sysctl. The handler is called when the variable is accessed for reading or writing. There exist data-type-specific macros such as SYSCTL_UINT() for unsigned integers and SYSCTL_STRING() for strings. The sysctls defined using these macros are served by predefined type-specific functions such as sysctl_handle_int() and sysctl_handle_string(). We will use SYSCTL_PROC() to define our sysctl variables, with our own handler functions, although we will simply call the predefined handlers from our handlers. Figure 1015 shows the updated contents of DummySysctl.c. Note that we register the three sysctl entriesdebug.osxbook, debug.osxbook.uint32, and debug.osxbook.stringin the kext's start routine and unregister them in the stop routine. Figure 1015. Implementing sysctl nodes
Let us compile and load the kext to test it. Once it is loaded, the sysctl command can be used to get and set the values of our sysctl variables. $ sysctl debug ... debug.osxbook.uint32: 0 debug.osxbook.string: $ sysctl -w debug.osxbook.uint32=64 debug.osxbook.uint32: 0 -> 64 $ sysctl debug.osxbook.uint32 debug.osxbook.uint32: 64 $ sysctl -w debug.osxbook.string=kernel debug.osxbook.string: sysctl: debug.osxbook.string: Operation not permitted $ sudo sysctl -w debug.osxbook.string=kernel debug.osxbook.string: -> kernel $ sysctl debug.osxbook.string debug.osxbook.string: kernel
10.6.3. I/O Kit Device Driver Kext
As we saw in Section 10.4.2, an I/O Kit driver is a kext that uses C++ in its implementationit runs in the kernel's C++ runtime environment provided by the I/O Kit. The Info.plist file of an I/O Kit driver kext contains one or more driver personality dictionaries. Moreover, unlike a generic kext, the driver implementor does not provide start/stop routines for the kext because, as we saw earlier, these routines are used as hooks to initialize/terminate the C++ runtime. However, an I/O Kit driver has several other entry points. Depending on the type and nature of the driver, many of these entry points can be optional or mandatory. The following are examples of I/O Kit driver entry points.
Let us implement an I/O Kit driver kext by starting with the IOKit Driver Xcode template. We will call our driver DummyDriver. Xcode will generate a header file (DummyDriver.h), a C++ source file (DummyDriver.cpp), and an Info.plist file, besides other project-related files. As we will see, it takes somewhat more work to create a trivial I/O Kit driver than a trivial generic kext. Figure 1016 shows the key contents of the driver's property list file. Figure 1016. An I/O Kit driver's personality and dependencies
As in the case of DummySysctl, we identify our driver kext using a reverse-DNS-style name. The IOKitPersonalities property, which is an array of personality dictionaries, contains properties for matching and loading the driver. Our driver contains only one personality called DummyPersonality_0. The IOClass key in the personality specifies the name of the driver's primary class, based on the driver kext's bundle identifier for uniformity. Note, however, that the name of the driver class cannot contain dots. Therefore, we translate dots to underscores while naming our driver's class. DummyDriver neither controls any hardware nor implements any real functionality. In order for it to load and match successfully, we specify its IOProviderClass property as IOResources, which is a special nub that can be matched against any driver. Note that it is possible for real drivers that do not attach to any hardware to match against IOResources. The BootCache kext (BootCache.kext)[9] is an exampleit operates within the Mach and BSD portions of the kernel but implements minimal glue code for the I/O Kit. It also specifies IOResources as its provider class. Figure 1017 shows the personality specification from the BootCache kext's Info.plist file. [9] We discussed BootCache in Section 4.14. Figure 1017. The driver personality of the BootCache kernel extension
IOMatchCategory is a special property that allows multiple drivers to match a single nub. Examples of kexts whose driver personalities specify this property include AppleRAID, IOFireWireFamily, IOGraphicsFamily, IOSerialFamily, IOStorageFamily, and IOUSBFamily. Note that a kext that specifies an IOResourceMatch property is eligible for loading after the subsystem named by the value of IOResourceMatch has been published as available.
Figure 1018. An excerpt from the iPod driver's property list file
The base class for most I/O Kit families and drivers is IOService. We will also subclass IOService to implement com_osxbook_driver_DummyDriver. Our driver's source references two macros that are defined by the I/O Kit.
In general, all subclasses of OSObject use these macros or variants of them.
We implement several class methods in our dummy driver to examine when and in which order they are called. However, we need not implement any logic in these methodswe can simply log a message and forward the invocation to the corresponding superclass method. Figure 1019 shows the contents of DummyDriver.h. Figure 1019. Header file for the DummyDriver I/O Kit driver
Figure 1020 shows the contents of DummyDriver.cpp. Note how the OSDefineMetaClassAndStructors() macro is used: The first argument is the literal name of the driver's class (the same as the value of the IOClass property in the personality), and the second argument is the literal name of the driver's superclass. Figure 1020. Implementation of the DummyDriver I/O Kit driver's class
Let us load the driver manually using kextload and unload it using kextunload. $ sudo kextload -v DummyDriver.kext kextload: extension DummyDriver.kext appears to be valid kextload: notice: extension DummyDriver.kext has debug properties set kextload: loading extension DummyDriver.kext kextload: DummyDriver.kext loaded successfully kextload: loading personalities named: kextload: DummyPersonality_0 kextload: sending 1 personality to the kernel kextload: matching started for DummyDriver.kext $ sudo kextunload -v /tmp/DummyDriver.kext kextunload: unload kext /tmp/DummyDriver.kext succeeded
We can now look in /var/log/system.log for messages logged by DummyDriver. The following excerpt from the log shows the sequence in which the I/O Kit calls the driver's methods. init # kextload attach probe detach attach start ... stop # kextunload detach free We see active matching in action as the I/O Kit calls attach(), probe(), and detach(), in that order. Since our probe() implementation returns success, the I/O Kit proceeds to start the driver. Had we returned a failure from probe(), the next method to be called would have been free(). Figure 1021 shows a more general view of how the I/O Kit calls driver methods in a driver's lifecycle. Figure 1021. The sequence of I/O Kit driver methods called in a driver's lifecycle
While debugging an I/O Kit driver, it is possible to only load the kext and defer the matching phase. The -l option instructs kextload not to start the matching process. Moreover, the -s option can be used to instruct kextload to create symbol files for the kext and its dependencies.[10] This allows the programmer to set up the debugger before initiating the matching process, which can be performed later using the -m option of kextload. If the matching succeeds, the driver will be started eventually. Section 10.8.4 provides an example of this debugging approach. [10] When a kext is loaded, its symbols are relocated.
|
Категории