Developing a Device Driver to Run on an Intel-Based Macintosh

This chapter provides an overview of some of the issues related to developing a universal binary version of an I/O Kit device driver. Before you read this chapter, be sure to read Universal Binary Programming Guidelines, Second Edition. That document covers architectural differences and byte-ordering formats and provides comprehensive guidelines for code modification and building universal binaries. Most of the guidelines in that document apply to kernel extensions as well as to applications.

Before you build your device driver as a universal binary, make sure that:

The Intel side of your universal I/O Kit driver must be built using GCC 4.0 and the OS X v10.4u (Universal) SDK available in Xcode 2.2.1 or later. This allows your driver to load and run on Intel-based Macintosh computers running OS X v10.4.4 or later.

To determine the compiler version and SDK to use when building the PowerPC side of your universal I/O Kit driver, decide which versions of OS X you plan to target:

For more details on building a universal binary version of your driver, including Xcode project settings, see Technical Note TN2163: Building Universal I/O Kit Drivers.

Byte Swapping

As a device driver developer, you are keenly aware of the native byte-ordering format of the device with which you're working. This familiarity, coupled with the fact that the PowerPC processor uses big-endian byte ordering, may mean that your code makes byte-order assumptions that are not appropriate for a universal binary. If, for example, a device driver communicates with a natively little-endian device (such as a USB device), it may perform little-to-big byte swaps because it was developed to run in a PowerPC-based Macintosh. Such hard-coded byte swaps will cause problems when the device driver is run in an Intel-based Macintosh.

To avoid these problems, search for hard-coded byte swaps in your device driver and replace them with the conditional byte-swapping macros defined in libkern/OSByteOrder.h. These macros are designed to perform the most efficient byte swap for the given situation. For example, using the OSSwapConst macros (and the OSSwap*Const variants) in OSByteOrder.h to swap constant values prevents byte-swapping at runtime.

Handling Architectural Differences

As described in Universal Binary Programming Guidelines, Second Edition, there are differences in the PowerPC and x86 architectures that can prevent code developed for one architecture from running properly in the other architecture. This section highlights two architectural differences that have consequences device driver developers need to be aware of.

In an Intel-based Macintosh, an integer divide-by-zero operation is fatal, whereas in a PowerPC-based Macintosh (in which the operation returns 0), it is not. Although it is not likely your device driver purposely performs a divide-by-zero operation, it may occur as a consequence of other operations. Examine your code for places where this might happen and add code to prevent it.

Device drivers often define software data structures that mirror hardware data structures. It's important to realize that some data types have one size in one architecture and a different size in the other. For example, the bool data type is one byte in the x86 architecture and four bytes in the PowerPC architecture. If your device driver uses a data structure that contains a bool value, consider replacing it with a fixed-size data type to avoid alignment problems.

Viewing Values in the Device Tree Plane

In a PowerPC-based Macintosh, Open Firmware populates the device tree plane of the I/O Registry with device hierarchy information. In an Intel-based Macintosh, the device tree plane may contain entirely different values. In fact, the device tree planes of different PowerPC-based Macintosh computers may contain different values.

Because you cannot predict what values an individual computer's device tree plane will contain, your device driver should not rely on the existence of specific values or locations. Instead, it should use APIs provided by I/O Kit classes, such as IORegistryEntry, to access values in the I/O Registry.

Interrupt Sharing in an Intel-Based Macintosh

It's important for your device driver to pass along any interrupts it might receive that aren't directed at it. In a PowerPC-based Macintosh each internal PCI slot has its own dedicated interrupt line, which means that a driver might be able to get away with ignoring interrupts that aren't destined for it, even though this is not recommended. In an Intel-based Macintosh, however, it is likely that a driver will share an interrupt line with other drivers. Under these circumstances, ignoring interrupts for other drivers can cause problems for these drivers and for the user.

It's possible that your driver might receive a very large number of interrupts destined for other drivers. Besides being sure to pass them on, your driver should not consider the arrival of such interrupts as an event that requires some sort of error logging. If, for example, your driver uses IOLog to log every spurious interrupt it receives, it will quickly overflow the console log and may degrade system performance. (For more information on using IOLog, see “Using IOLog.”)

Using the OSSynchronizeIO Function

Your PowerPC device driver may use the OSSynchronizeIO function to, for example, enforce in-order execution of a write operation. In a PowerPC-based Macintosh, this function issues an eieio instruction. In an Intel-based Macintosh, however, this function does nothing because the x86 architecture supports strong memory ordering by default.

A universal binary version of a device driver that uses the OSSynchronizeIO function should continue to do so to ensure that it still runs correctly on a PowerPC-based Macintosh.

Accessing I/O Space

In a PowerPC-based Macintosh, PCI I/O space is mapped into memory. A PCI device driver accesses the I/O space using the ioRead* functions of the IOPCIDevice class (for more on these functions, see IOPCIDevice documentation in Kernel Framework Reference). In a PowerPC-based Macintosh, these functions translate into memory accesses and in an Intel-based Macintosh they translate into the appropriate x86 instructions. If you're developing a universal binary version of a PCI driver, therefore, you can use the ioRead* functions to access I/O space and your driver will run in both PowerPC-based and Intel-based Macintosh computers.

For other devices that use I/O space, however, such as an ATA controller, you may have to add x86-specific instructions that execute when the driver is running on an Intel-based Macintosh.

Debugging on an Intel-Based Macintosh

For the most part, debugging a device driver running on an Intel-based Macintosh computer is the same as debugging a driver running on a PowerPC-based Macintosh computer. Even though an Intel-based Macintosh computer uses EFI (extensible firmware interface) instead of the Open Firmware used in a PowerPC-based Macintosh computer, you still need to set the appropriate NVRAM (nonvolatile RAM) variables to enable two-machine debugging (for more information on two-machine debugging, see “Two-Machine Debugging”). To change NVRAM variables on any Macintosh computer running OS X, you use the nvram utility as root on the command line. For example, you can set the following kernel debug flag:

nvram boot-args="debug=0x14e"

Intel-based Macintosh computers also provide a way to store a persistent set of boot arguments on a particular machine. The set of boot arguments is not required for kernel debugging, but it can make it easier for you if you often perform netbooting or if you boot from different partitions and you’d like to have different boot arguments associated with each partition.

The boot arguments are contained in a property list file named com.apple.Boot.plist, which is located in /Library/Preferences/SystemConfiguration. The com.apple.Boot.plist file contains the following keys:

If you perform netbooting, be sure to make a copy of the com.apple.Boot.plist file and place it in the same folder that contains the kernel on the netboot server. A typical development scenario is to have a kernel, an mkext file (a previously cached set of device drivers for hardware involved in the boot process), and a com.apple.Boot.plist file for each netboot configuration you work with.