Programming the Microsoft Windows Driver Model

The AddDevice Routine

In the preceding main section, I showed how you initialize a WDM driver when it s first loaded. In general, though, a driver might be called upon to manage more than one actual device. In the WDM architecture, a driver has a special AddDevice function that the PnP Manager can call for each such device. The function has the following skeleton:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { return STATUS_SOMETHING; // e.g., STATUS_SUCCESS }

The DriverObject argument points to the same driver object that you initialized in your DriverEntry routine. The pdo argument is the address of the physical device object at the bottom of the device stack, even if there are already filter drivers below.

The basic responsibility of AddDevice in a function driver is to create a device object and link it into the stack rooted in this PDO. The steps involved are as follows:

  1. Call IoCreateDevice to create a device object and an instance of your own device extension object.

  2. Register one or more device interfaces so that applications know about the existence of your device. Alternatively, give the device object a name and then create a symbolic link.

  3. Next initialize your device extension and the Flags member of the device object.

  4. Call IoAttachDeviceToDeviceStack to put your new device object into the stack.

Now I ll explain these steps in more detail. I ll show a complete example of AddDevice at the very end of this discussion.

NOTE

In the code snippets that follow, I ve deliberately left out all the error handling that should be there. That s so I could concentrate on the normal control flow through AddDevice. You mustn t imitate this programming style in a production driver but of course you already knew that. I ll discuss how to handle errors in the next chapter. Every code sample in the companion content has full error checking in place too.

Creating a Device Object

You create a device object by calling IoCreateDevice. For example:

PDEVICE_OBJECT fdo; NTSTATUS status = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &fdo);

The first argument (DriverObject) is the same value supplied to AddDevice as the first parameter. This argument establishes the connection between your driver and the new device object, thereby allowing the I/O Manager to send you IRPs intended for the device. The second argument is the size of your device extension structure. As I discussed earlier in this chapter, the I/O Manager allocates this much additional memory and sets the DeviceExtension pointer in the device object to point to it.

The third argument, which is NULL in this example, can be the address of a UNICODE_STRING providing a name for the device object. Deciding whether to name your device object and which name to give it requires some thought, and I ll describe these surprisingly complex considerations a bit further on in the section Should I Name My Device Object?

The fourth argument (FILE_DEVICE_UNKNOWN) is one of the device types defined in WDM.H. Whatever value you specify here can be overridden by an entry in the device s hardware key or class key. If both keys have an override, the device key has precedence. For devices that fit into one of the established categories, specify the right value in one of these places because some details about the interaction between your driver and the surrounding system depend on it. In fact, the device type is crucial for the correct functioning of a file system driver or a disk or tape driver. Additionally, the default security settings for your device object depend on this device type.

The fifth argument (FILE_DEVICE_SECURE_OPEN) provides the Characteristics flag for the device object. (See Table 2-3.) Most of these flags are relevant for mass storage devices. The flag bit FILE_AUTOGENERATED_DEVICE_NAME is for use by bus and multifunction drivers when creating PDOs. I ll discuss the importance of FILE_DEVICE_SECURE_OPEN later in this chapter in the section Should I Name My Device Object? Whatever value you specify here can be overridden by an entry in the device s hardware key or class key. If both keys have an override, the hardware key has precedence.

The sixth argument to IoCreateDevice (FALSE in my example) indicates whether the device is exclusive. The I/O Manager allows only one handle to be opened by normal means to an exclusive device. Whatever value you specify here can be overridden by an entry in the device s hardware key or class key. If both keys have an override, the hardware key has precedence.

NOTE

The exclusivity attribute matters only for whatever named device object is the target of an open request. If you follow Microsoft s recommended guidelines for WDM drivers, you won t give your device object a name. Open requests will then target the PDO, but the PDO will not usually be marked exclusive because the bus driver generally has no way of knowing whether you need your device to be exclusive. The only time the PDO will be marked exclusive is when there s an Exclusive override in the device s hardware key or the class key s Properties subkey. You re best advised, therefore, to avoid relying on the exclusive attribute altogether. Instead, make your IRP_MJ_CREATE handler reject open requests that would violate whatever restriction you require.

The last argument (&fdo) points to a location where IoCreateDevice will store the address of the device object it creates.

If IoCreateDevice fails for some reason, it returns a status code and doesn t alter the PDEVICE_OBJECT described by the last argument. If it succeeds, it returns a successful status code and sets the PDEVICE_OBJECT pointer. You can then proceed to initialize your device extension and do the other work associated with creating a new device object. Should you discover an error after this point, you should release the device object and return a status code. The code to accomplish these tasks would be something like this:

NTSTATUS status = IoCreateDevice(...); if (!NT_SUCCESS(status)) return status; if (<some other error discovered>) { IoDeleteDevice(fdo); return status; }

I ll explain the NTSTATUS status codes and the NT_SUCCESS macro in the next chapter.

Naming Devices

Windows XP uses a centralized Object Manager to manage many of its internal data structures, including the driver and device objects I ve been talking about. David Solomon and Mark Russinovich present a fairly complete explanation of the Object Manager and namespace in Chapter 3, System Mechanisms, of Inside Windows 2000, Third Edition (Microsoft Press, 2000). Objects have names, which the Object Manager maintains in a hierarchical namespace. Figure 2-16 is a screen shot of my DEVVIEW application showing the top level of the name hierarchy. The objects displayed as folders in this screen shot are directory objects, which can contain subdirectories and regular objects. The objects displayed with other icons are examples of these regular objects. (In this respect, DEVVIEW is similar to the WINOBJ utility that you ll find in the BIN\WINNT directory of the Platform SDK. WINOBJ can t give you information about device objects and drivers, though, which is why I wrote DEVVIEW in the first place.)

Figure 2-16. Using DEVVIEW to view the namespace.

Device objects can have names that conventionally live in the \Device directory. Names for devices serve two purposes in Windows XP. Giving your device object a name allows other kernel-mode components to find it by calling service functions such as IoGetDeviceObjectPointer. Having found your device object, they can send you IRPs.

The other purpose of naming a device object is to allow applications to open handles to the device so they can send you IRPs. An application uses the standard CreateFile API to open a handle, whereupon it can use ReadFile, WriteFile, and DeviceIoControl to talk to you. The pathname an application uses to open a device handle begins with the prefix \\.\ rather than with a standard Universal Naming Convention (UNC) name such as C:\MYFILE.CPP or \\FRED\C-Drive\HISFILE.CPP. Internally, the I/O Manager converts this prefix to \??\ before commencing a name search. To provide a mechanism for connecting names in the \?? directory to objects whose names are elsewhere (such as in the \Device directory), the Object Manager implements an object called a symbolic link.

The name \?? has a special meaning in Windows XP. Confronted with this name, the Object Manager first searches a portion of the kernel namespace that is local to the current user session. To see how this works, establish two or more sessions and start DEVVIEW in one of them. Expand the \Sessions folder, and you will eventually see folders for each user. Figure 2-18, which appears later in this chapter, provides an example. If the local search isn t successful, the Object Manager then searches the \GLOBAL?? folder.

Symbolic Links

A symbolic link is a little bit like a desktop shortcut in that it points to some other entity that s the real object of attention. One use of symbolic links in Windows XP is to connect the leading portion of MS-DOS-style names to devices. Figure 2-17 shows a portion of the \GLOBAL?? directory, which includes a number of symbolic links. Notice, for example, that C and other drive letters in the MS-DOS file-naming scheme are actually links to objects whose names are in the \Device directory. These links allow the Object Manager to jump somewhere else in the namespace as it parses through a name. So if I call CreateFile with the name C:\MYFILE.CPP, the Object Manager will take this path to open the file:

Figure 2-17. The \GLOBAL?? directory with several symbolic links.

  1. Kernel-mode code initially sees the name \??\C:\MYFILE.CPP. The Object Manager special-cases the ?? name to mean the DosDevices directory for the current session. (In Figure 2-18, this directory is one of the subdirectories of \Sessions\0\DosDevices.)

  2. The Object Manager doesn t find C: in the session DosDevices directory, so it follows a symbolic link named Global to the GLOBAL?? directory.

  3. The Object Manager now looks up C: in the \GLOBAL?? directory. It finds a symbolic link by that name, so it forms the new kernel-mode pathname \Device\HarddiskVolume1\MYFILE.CPP and parses that.

  4. Working with the new pathname, the Object Manager looks up Device in the root directory and finds a directory object.

  5. The Object Manager looks up HarddiskVolume1 in the \Device directory. It finds a device object by that name.

At this point in the process, the Object Manager will create an IRP that it will send to the driver or drivers for HarddiskVolume1. The IRP will eventually cause some file system driver or another to locate and open a disk file. Describing how a file system driver works is beyond the scope of this book, but the sidebar Opening a Disk File will give you a bit of the flavor.

Opening a Disk File

The overall process that occurs when an application opens a disk file is complicated almost beyond belief. To continue the example in the text, the driver for HarddiskVolume1 would be a file system driver such as NTFS.SYS, FASTFAT.SYS, or CDFS.SYS. How a file system realizes that a particular disk volume belongs to it and initializes to handle the volume is itself a saga of Norse proportions. That would have already happened before an application could get far enough to call CreateFile with a particular volume letter in the pathname, though, so we can ignore that process.

The file system driver will locate the topmost device object in the storage stack that includes the physical disk drive on which the C volume happens to be mounted. The I/O Manager and file system driver share management of a Volume Parameters Block (VPB) that ties the storage stack and the file system s volume stack together. In principle, the file system driver sends IRPs to the storage driver in order to read directory entries that it can search while parsing the pathname specified by the original CreateFile call. In practice, the file system calls the kernel cache manager, which fulfills requests from an in-memory cache if possible and makes recursive calls to the file system driver to fill cache buffers. Deadlock prevention and surprise dismount handling during this process require heroic efforts, including a mechanism whereby a file system driver can stash a pointer to an automatic variable (that is, one allocated on the call/return stack) to be used in deeper layers of recursion within the same thread.

Luckily, you needn t worry about VPBs and other complications arising from the way file system drivers work in any driver besides one for a storage device. I won t say any more about this in the book.

If we were dealing with a device name such as COM1, the driver that ended up receiving the IRP would be the driver for \Device\Serial0. How a device driver handles an open request is definitely within the scope of this book, and I ll be discussing it in this chapter (in the section Should I Name My Device Object? ) and in Chapter 5, when I ll talk about IRP processing in general.

A user-mode program can create a symbolic link in the local (session) namespace by calling DefineDosDevice, as in this example (see Figure 2-18):

BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "\\Device\\Beep");

Figure 2-18. Symbolic link created by DefineDosDevice.

You can create a symbolic link in a WDM driver by calling IoCreateSymbolicLink,

IoCreateSymbolicLink(linkname, targname);

where linkname is the name of the symbolic link you want to create and targname is the name to which you re linking. Incidentally, the Object Manager doesn t care whether targname is the name of any existing object: someone who tries to access an object by using a link that points to an undefined name simply receives an error. If you want to allow user-mode programs to override your link and point it somewhere else, you should call IoCreateUnprotectedSymbolicLink instead.

The kernel-mode equivalent of the immediately preceding DefineDosDevice call is this:

UNICODE_STRING linkname; UNICODE_STRING targname; RtlInitUnicodeString(&linkname, L"\\DosDevices\\barf"); RtlInitUnicodeString(&targname, L"\\Device\\Beep"); IoCreateSymbolicLink(&linkname, &targname);

Should I Name My Device Object?

Deciding whether to give your device object a name requires, as I said earlier, a little thought. If you give your object a name, it will be possible for any kernel-mode program to try to open a handle to your device. Furthermore, it will be possible for any kernel-mode or user-mode program to create a symbolic link to your device object and to use the symbolic link to try to open a handle. You might or might not want to allow these actions.

The primary consideration in deciding whether to name your device object is security. When someone opens a handle to a named object, the Object Manager verifies that they have permission to do so. When IoCreateDevice creates a device object for you, it assigns a default security descriptor based on the device type you specify as the fourth argument. The I/O Manager uses three basic categories to select a security descriptor:

You can see that anyone will be able to access a nondisk device for both reading and writing if the driver gives the device object a name at the time it calls IoCreateDevice. This is because the default security allows nearly full access and because no security check at all is associated with creating a symbolic link the security checks happen at open time, based on the named object s security descriptor. This is true even if other device objects in the same stack have more restrictive security.

NOTE

IoCreateDeviceSecure, a function in the .NET DDK, allows you to specify a nondefault security descriptor in situations in which no override is in the registry. This function is too new for us to describe it more fully here.

DEVVIEW will show you the security attributes of the device objects it displays. You can see the operation of the default rules I just described by examining a file system, a disk device, and any other random device.

The PDO also receives a default security descriptor, but it s possible to override it with a security descriptor stored in the hardware key or in the Properties subkey of the class key. (The hardware key has precedence if both keys specify a descriptor.) Even lacking a specific security override, if either the hardware key or the class key s Properties subkey overrides the hardware type or characteristics specification, the I/O Manager constructs a new default security descriptor based on the new type. The I/O Manager does not, however, override the security setting for any of the other device objects above the PDO. Consequently, for the overrides (and the administrative actions that set them up) to have any effect, you shouldn t name your device object. Don t despair though applications can still access your device by means of a registered interface, which I ll discuss soon.

You need to know about one last security concern. As the Object Manager parses its way through an object name, it needs only FILE_TRAVERSE access to the intermediate components of the name. It performs a full security check only on the object named by the final component. So suppose you have a device object reachable under the name \Device\Beep or by the symbolic link \??\Barf. A user-mode application that tries to open \\.\Barf for writing will be blocked if the object security has been set up to deny write access. But if the application tries to open a name like \\.\Barf\ExtraStuff that has additional name qualifications, the open request will make it all the way to the device driver (in the form of an IRP_MJ_CREATE I/O request) if the user merely has FILE_TRAVERSE permission, which is routinely granted. (In fact, most systems even run with the option to check for traverse permission turned off.) The I/O Manager expects the device driver to deal with the additional name components and to perform any required security checks with regard to them.

To avoid the security concern I just described, you can supply the flag FILE_DEVICE_SECURE_OPEN in the device characteristics argument to IoCreate Device. This flag causes Windows XP to verify that someone has the right to open a handle to a device even if additional name components are present.

The Device Name

If you decide to name the device object, you ll normally put the name in the \Device branch of the namespace. To give it a name, you have to create a UNICODE_STRING structure to hold the name, and you have to specify that string as an argument to IoCreateDevice:

UNICODE_STRING devname; RtlInitUnicodeString(&devname, L"\\Device\\Simple0"); IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...);

I ll discuss the use of RtlInitUnicodeString in the next chapter.

NOTE

Starting in Windows XP, device object names are case insensitive. In Windows 98/Me and in Windows 2000, they are case sensitive. Be sure to spell \Device exactly as shown if you want your driver to be portable across all the systems. Note also the spelling of \DosDevices, particularly if your mother tongue doesn t inflect the plural form of nouns!

Conventionally, drivers assign their device objects a name by concatenating a string naming their device type ( Simple in this code fragment) with a 0-based integer denoting an instance of that type. In general, you don t want to hard-code a name as I just did you want to compose it dynamically using string-manipulation functions like the following:

UNICODE_STRING devname; static LONG lastindex = -1; LONG devindex = InterlockedIncrement(&lastindex); WCHAR name[32]; _snwprintf(name, arraysize(name), L"\\Device\\SIMPLE%2.2d", devindex); RtlInitUnicodeString(&devname, name); IoCreateDevice(...);

I ll explain the various service functions used in this code fragment in the next couple of chapters. The instance number you derive for private device types might as well be a static variable, as shown in the code fragment.

Notes on Device Naming

The \GLOBAL?? directory used to be named \DosDevices. The change was made to move the often-searched directory of user-mode names to the front of the alphabetical list of directories. Windows 98/Me doesn t recognize the name \?? or \GLOBAL??.

Windows 2000 defines a symbolic link named \DosDevices that points to the \?? directory. Windows XP treats \DosDevices differently depending on the process context at the time you create an object. If you create an object, such as a symbolic link, in a system thread, \DosDevices refers to \GLOBAL??, and you end up with a global name. If you create an object in a user thread, \DosDevices refers to \??, and you end up with a session-specific name. In most situations, a device driver creates symbolic links in its AddDevice function, which runs in a system thread, and so ends up with globally named objects in all WDM environments simply by putting the symbolic link in \DosDevices. If you create a symbolic link at another time, you should use \GLOBAL?? in Windows XP and \Dos Devices in earlier systems. See Appendix A for a discussion of how to distinguish between WDM platforms.

A quick-and-dirty shortcut for testing is to name your device object in the \DosDevices directory, as many of the sample drivers in the companion content do. A production driver should name the device object in \Device, however, to avoid the possibility of creating an object that ought to be global in a session-private namespace.

In previous versions of Windows NT, drivers for certain classes of devices (notably disks, tapes, serial ports, and parallel ports) called IoGetConfigurationInformation to obtain a pointer to a global table containing counts of devices in each of these special classes. A driver would use the current value of the counter to compose a name like Harddisk0, Tape1, and so on and would also increment the counter. WDM drivers don t need to use this service function or the table it returns, however. Constructing names for the devices in these classes is now the responsibility of a Microsoft type-specific class driver (such as DISK.SYS).

Device Interfaces

The older method of naming I just discussed naming your device object and creating a symbolic link name that applications can use has two major problems. We ve already discussed the security implications of giving your device object a name. In addition, the author of an application that wants to access your device has to know the scheme you adopted to name your devices. If you re the only one writing the applications that will be accessing your hardware, that s not much of a problem. But if many different companies will be writing applications for your hardware, and especially if many hardware companies are making similar devices, devising a suitable naming scheme is difficult.

To solve these problems, WDM introduces a new naming scheme for devices that is language-neutral, easily extensible, usable in an environment with many hardware and software vendors, and easily documented. The scheme relies on the concept of a device interface, which is basically a specification for how software can access hardware. A device interface is uniquely identified by a 128-bit GUID. You can generate GUIDs by running the Platform SDK utilities UUIDGEN or GUIDGEN both utilities generate the same kind of number, but they output the result in different formats. The idea is that some industry group gets together to define a standard way of accessing a certain kind of hardware. As part of the standard-making process, someone runs GUIDGEN and publishes the resulting GUID as the identifier that will be forever after associated with that interface standard.

More About GUIDs

The GUIDs used to identify software interfaces are the same kind of unique identifier that s used in the Component Object Model (COM) to identify COM interfaces and in the Open Software Foundation (OSF) Distributed Computing Environment (DCE) to identify the target of a remote procedure call (RPC). For an explanation of how GUIDs are generated so as to be statistically unique, see page 66 of Kraig Brockschmidt s Inside OLE, Second Edition (Microsoft Press, 1995), which contains a further reference to the original algorithm specification by the OSF. I found the relevant portion of the OSF specification on line at http://www.opengroup.org/onlinepubs/9629399/apdxa.htm.

The mechanics of creating a GUID for use in a device driver involve running either UUIDGEN or GUIDGEN and then capturing the resulting identifier in a header file. GUIDGEN is easier to use because it allows you to choose to format the GUID for use with the DEFINE_GUID macro and to copy the resulting string onto the Clipboard. Figure 2-19 shows the GUIDGEN window. You can paste its output into a header file to end up with this:

// {CAF53C68-A94C-11d2-BB4A-00C04FA330A6} DEFINE_GUID(<<name>>, 0xcaf53c68, 0xa94c, 0x11d2, 0xbb, 0x4a, 0x0, 0xc0, 0x4f, 0xa3, 0x30, 0xa6);

You then replace <<name>> with something more mnemonic like GUID_DEVINTERFACE_SIMPLE and include the definition in your driver and applications.

Figure 2-19. Using GUIDGEN to generate a GUID.

I think of an interface as being analogous to the protein markers that populate the surface of living cells. An application desiring to access a particular kind of device has its own protein markers that fit like a key into the markers exhibited by conforming device drivers. See Figure 2-20.

Figure 2-20. Using device interfaces to match applications and devices.

Registering a Device Interface

A function driver s AddDevice function should register one or more device interfaces by calling IoRegisterDeviceInterface, as shown here:

#include <initguid.h>

#include "guids.h" NTSTATUS AddDevice(...) {

IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_SIMPLE, NULL, &pdx->ifname); }

  1. We re about to include a header (GUIDS.H) that contains one or more DEFINE_GUID macros. DEFINE_GUID normally declares an external variable. Somewhere in the driver, though, we have to actually reserve initialized storage for every GUID we re going to reference. The system header file INITGUID.H works some preprocessor magic to make DEFINE_GUID reserve the storage even if the definition of the DEFINE_GUID macro happens to be in one of the precompiled header files.

  2. I m assuming here that I put the GUID definitions I want to reference into a separate header file. This would be a good idea, inasmuch as user-mode code will also need to include these definitions and won t want to include a bunch of extraneous kernel-mode declarations relevant only to our driver.

  3. The first argument to IoRegisterDeviceInterface must be the address of the PDO for your device. The second argument identifies the GUID associated with your interface, and the third argument specifies additional qualified names that further subdivide your interface. Only Microsoft code uses this name subdivision scheme. The last argument is the address of a UNICODE_STRING structure that will receive the name of a symbolic link that resolves to this device object.

The return value from IoRegisterDeviceInterface is a Unicode string that applications will be able to determine without knowing anything special about how you coded your driver and will then be able to use in opening a handle to the device. The name is pretty ugly, by the way; here s an example that I generated for one of my sample devices: \\?\ROOT#UNKNOWN#0000#{b544b9a2-6995-11d3-81b5-00c04fa330a6}.

All that registration actually does is create the symbolic link name and save it in the registry. Later on, in response to the IRP_MN_START_DEVICE Plug and Play request we ll discuss in Chapter 7, you ll make the following call to IoSetDeviceInterfaceState to enable the interface:

IoSetDeviceInterfaceState(&pdx->ifname, TRUE);

In response to this call, the I/O Manager creates an actual symbolic link object pointing to the PDO for your device. You ll make a matching call to disable the interface at a still later time (just call IoSetDeviceInterfaceState with a FALSE argument), whereupon the I/O Manager will delete the symbolic link object while preserving the registry entry that contains the name. In other words, the name persists and will always be associated with this particular instance of your device; the symbolic link object comes and goes with the hardware.

Since the interface name ends up pointing to the PDO, the PDO s security descriptor ends up controlling whether people can access your device. That s good because it s the PDO s security that you control in the INF used to install the driver.

Enumerating Device Interfaces

Both kernel-mode and user-mode code can locate all the devices that happen to support an interface in which they re interested. I m going to explain how to enumerate all the devices for a particular interface in user mode. The enumeration code is so tedious to write that I eventually wrote a C++ class to make my own life simpler. You ll find this code in the DEVICELIST.CPP and DEVICELIST.H files that are part of the HIDFAKE and DEVPROP samples in Chapter 8. These files declare and implement a CDeviceList class, which contains an array of CDeviceListEntry objects. These two classes have the following declaration:

class CDeviceListEntry { public: CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname); CDeviceListEntry(){} CString m_linkname; CString m_friendlyname; }; class CDeviceList { public: CDeviceList(const GUID& guid); ~CDeviceList(); GUID m_guid; CArray<CDeviceListEntry, CDeviceListEntry&> m_list; int Initialize(); };

The classes rely on the CString class and CArray template class that are part of the Microsoft Foundation Classes (MFC) framework. The constructors for these two classes simply copy their arguments into the obvious data members:

CDeviceList::CDeviceList(const GUID& guid) { m_guid = guid; } CDeviceListEntry::CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname) { m_linkname = linkname; m_friendlyname = friendlyname; }

All the interesting work occurs in the CDeviceList::Initialize function. The executive overview of what it does is this: it will enumerate all of the devices that expose the interface whose GUID was supplied to the constructor. For each such device, it will determine a friendly name that we re willing to show to an unsuspecting end user. Finally it will return the number of devices it found. Here s the code for this function:

int CDeviceList::Initialize() {

HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL, DIGCF_PRESENT DIGCF_INTERFACEDEVICE); if (info == INVALID_HANDLE_VALUE) return 0; SP_INTERFACE_DEVICE_DATA ifdata; ifdata.cbSize = sizeof(ifdata); DWORD devindex;

for (devindex = 0; SetupDiEnumDeviceInterfaces(info, NULL, &m_guid, devindex, &ifdata); ++devindex) { DWORD needed;

SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL); PSP_INTERFACE_DEVICE_DETAIL_DATA detail = (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed); detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA); SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)}; SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did));

TCHAR fname[256]; if (!SetupDiGetDeviceRegistryProperty(info, &did, SPDRP_FRIENDLYNAME, NULL, (PBYTE) fname, sizeof(fname), NULL) && !SetupDiGetDeviceRegistryProperty(info, &did, SPDRP_DEVICEDESC, NULL, (PBYTE) fname, sizeof(fname), NULL)) _tcsncpy(fname, detail->DevicePath, 256); fname[255] = 0;

CDeviceListEntry e(detail->DevicePath, fname); free((PVOID) detail); m_list.Add(e); } SetupDiDestroyDeviceInfoList(info); return m_list.GetSize(); }

  1. This statement opens an enumeration handle that we can use to find all devices that have registered an interface that uses the same GUID.

  2. Here we call SetupDiEnumDeviceInterfaces in a loop to find each device.

  3. The only two items of information we need are the detail information about the interface and information about the device instance. The detail is just the symbolic name for the device. Since it s variable in length, we make two calls to SetupDiGetDeviceInterfaceDetail. The first call determines the length. The second call retrieves the name.

  4. We obtain a friendly name for the device from the registry by asking for either the FriendlyName or the DeviceDesc.

  5. We create a temporary instance named e of the CDeviceListEntry class, using the device s symbolic name as both the link name and the friendly name.

NOTE

You might be wondering how the registry comes to have a FriendlyName for a device. The INF file you use to install your device driver see Chapter 15 can have an HW section that specifies registry parameters for the device. You can provide a FriendlyName as one of these parameters, but bear in mind that every instance of your hardware will have the same name if you do. The MAKENAMES sample describes a DLL-based way of defining a unique friendly name for each instance. You can also write a CoInstaller DLL that will define unique friendly names.

If you don t define a FriendlyName, by the way, most system components will use the DeviceDesc string in the registry. This string originates in the INF file and will usually describe your device by manufacturer and model.

Sample Code

The DEVINTERFACE sample is a user-mode program that enumerates all instances of all known device interface GUIDs on your system. One way to use this sample is as a way to determine which GUID you need to enumerate to find a particular device.

Other Global Device Initialization

You need to take some other steps during AddDevice to initialize your device object. I m going to describe these steps in the order you should do them, which isn t exactly the same order as their respective logical importance. I want to emphasize that the code snippets in this section are even more fragmented than usual I m going to show only enough of the entire AddDevice routine to establish the surrounding context for the small pieces I m trying to illustrate.

Initializing the Device Extension

The content and management of the device extension are entirely up to you. The data members you place in this structure will obviously depend on the details of your hardware and on how you go about programming the device. Most drivers would need a few items placed there, however, as illustrated in the following fragment of a declaration:

typedef struct _DEVICE_EXTENSION {

PDEVICE_OBJECT DeviceObject;

PDEVICE_OBJECT LowerDeviceObject;

PDEVICE_OBJECT Pdo;

UNICODE_STRING ifname;

IO_REMOVE_LOCK RemoveLock;

DEVSTATE devstate; DEVSTATE prevstate; DEVICE_POWER_STATE devpower; SYSTEM_POWER_STATE syspower;

DEVICE_CAPABILITIES devcaps; } DEVICE_EXTENSION, *PDEVICE_EXTENSION;

  1. I find it easiest to mimic the pattern of structure declaration used in the official DDK, so I declared this device extension as a structure with a tag as well as a type and pointer-to-type name.

  2. You already know that you locate your device extension by following the DeviceExtension pointer from the device object. It s also useful in several situations to be able to go the other way to find the device object given a pointer to the extension. The reason is that the logical argument to certain functions is the device extension itself (since that s where all of the per-instance information about your device resides). Hence, I find it useful to have this DeviceObject pointer.

  3. I ll mention in a few paragraphs that you need to record the address of the device object immediately below yours when you call IoAttachDeviceToDeviceStack, and LowerDeviceObject is the place to do that.

  4. A few service routines require the address of the PDO instead of some higher device object in the same stack. It s very difficult to locate the PDO, so the easiest way to satisfy the requirement of those functions is to record the PDO address in a member of the device extension that you initialize during AddDevice.

  5. Whichever method (symbolic link or device interface) you use to name your device, you ll want an easy way to remember the name you assign. In this code fragment, I ve declared a Unicode string member named ifname to record a device interface name. If you were going to use a symbolic link name instead of a device interface, it would make sense to give this member a more mnemonic name, such as linkname.

  6. I ll discuss in Chapter 6 a synchronization problem affecting how you decide when it s safe to remove this device object by calling IoDeleteDevice. The solution to that problem involves using an IO_REMOVE_LOCK object that needs to be allocated in your device extension as shown here. AddDevice needs to initialize that object.

  7. You ll probably need a device extension variable to keep track of the current Plug and Play state and current power states of your device. DEVSTATE is an enumeration that I m assuming you ve declared elsewhere in your own header file. I ll discuss the use of all these state variables in later chapters.

  8. Another part of power management involves remembering some capability settings that the system initializes by means of an IRP. The devcaps structure in the device extension is where I save those settings in my sample drivers.

The initialization statements in AddDevice (with emphasis on the parts involving the device extension) would be as follows:

NTSTATUS AddDevice(...) { PDEVICE_OBJECT fdo; IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; pdx->DeviceObject = fdo; pdx->Pdo = pdo; IoInitializeRemoveLock(&pdx->RemoveLock, ...); pdx->devstate = STOPPED; pdx->devpower = PowerDeviceD0; pdx->syspower = PowerSystemWorking; IoRegisterDeviceInterface(..., &pdx->ifname); pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...); }

In this code snippet, STOPPED and DEVICE_EXTENSION are things I defined in one of my own header files.

Initializing the Default DPC Object

Many devices signal completion of operations by means of an interrupt. As you ll learn when I discuss interrupt handling in Chapter 7, there are strict limits on what your interrupt service routine (ISR) can do. In particular, an ISR isn t allowed to call the routine (IoCompleteRequest) that signals completion of an IRP, but that s exactly one of the steps you re likely to want to take. You utilize a deferred procedure call (DPC) to get around the limitations. Your device object contains a subsidiary DPC object that can be used for scheduling your particular DPC routine, and you need to initialize it shortly after creating the device object:

NTSTATUS AddDevice(...) { IoCreateDevice(...); IoInitializeDpcRequest(fdo, DpcForIsr); }

Setting the Buffer Alignment Mask

Devices that perform DMA transfers work directly with data buffers in memory. The HAL might require that buffers used for DMA be aligned to some particular boundary, and your device might require still more stringent alignment. The AlignmentRequirement field of the device object expresses the restriction it s a bit mask equal to 1 less than the required address boundary. You can round an arbitrary address down to this boundary with this statement:

PVOID address = ...; SIZE_T ar = fdo->AlignmentRequirement; address = (PVOID) ((SIZE_T) address & ~ar);

You round an arbitrary address up to the next alignment boundary like this:

PVOID address = ...; SIZE_T ar = fdo->AlignmentRequirement; address = (PVOID) (((SIZE_T) address + ar) & ~ar);

In these two code fragments, I used SIZE_T casts to transform the pointer (which may be 32 bits or 64 bits wide, depending on the platform for which you re compiling) into an integer wide enough to span the same range as the pointer.

IoCreateDevice sets the AlignmentRequirement field of the new device object equal to whatever the HAL requires. For example, the HAL for Intel x86 chips has no alignment requirement, so AlignmentRequirement is 0 initially. If your device requires a more stringent alignment for the data buffers it works with (say, because you have bus-mastering DMA capability with a special alignment requirement), you want to override the default setting. For example:

if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement) fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;

I ve assumed here that elsewhere in your driver is a manifest constant named MYDEVICE_ALIGNMENT that equals a power of 2 and represents the required alignment of your device s data buffers.

Miscellaneous Objects

Your device might well use other objects that need to be initialized during AddDevice. Such objects might include various synchronization objects, linked list anchors, scatter/gather list buffers, and so on. I ll discuss these objects, and the fact that initialization during AddDevice would be appropriate, in various other parts of this book.

Initializing the Device Flags

Two of the flag bits in your device object need to be initialized during AddDevice and never changed thereafter: the DO_BUFFERED_IO and DO_DIRECT_IO flags. You can set one (but only one) of these bits to declare once and for all how you want to handle memory buffers coming from user mode as part of read and write requests. (I ll explain in Chapter 7 what the difference between these two buffering methods is and why you d want to pick one or the other.) The reason you have to make this important choice during AddDevice is that any upper filter drivers that load afterwards will be copying your flag settings, and it s the setting of the bits in the topmost device object that s actually important. Were you to change your mind after the filter drivers loaded, they probably wouldn t know about the change.

Two of the flag bits in the device object pertain to power management. In contrast with the two buffering flags, these two can be changed at any time. I ll discuss them in greater detail in Chapter 8, but here s a preview. DO_POWER_PAGABLE means that the Power Manager must send you IRP_MJ_POWER requests at interrupt request level (IRQL) PASSIVE_LEVEL. (If you don t understand all of the concepts in the preceding sentence, don t worry I ll completely explain all of them in later chapters.) DO_POWER_INRUSH means that your device draws a large amount of current when powering on, so the Power Manager should make sure that no other inrush device is powering up simultaneously.

Building the Device Stack

Each filter and function driver has the responsibility of building up the stack of device objects, starting from the PDO and working upward. You accomplish your part of this work with a call to IoAttachDeviceToDeviceStack:

NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo; IoCreateDevice(..., &fdo); pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo); }

The first argument to IoAttachDeviceToDeviceStack (fdo) is the address of your own newly created device object. The second argument is the address of the PDO. The second parameter to AddDevice is this address. The return value is the address of whatever device object is immediately underneath yours, which can be the PDO or the address of some lower filter device object. Figure 2-21 illustrates the situation when there are three lower filter drivers for your device. By the time your AddDevice function executes, all three of their AddDevice functions have already been called. They have created their respective FiDOs and linked them into the stack rooted at the PDO. When you call IoAttachDeviceToDeviceStack, you get back the address of the topmost FiDO.

IoAttachDeviceToDeviceStack might conceivably fail by returning a NULL pointer. For this to occur, someone would have to remove the physical device from the system at just the point in time when your AddDevice function was doing its work, and the PnP Manager would have to process the removal on another CPU. I m not even sure these conditions are enough to trigger a failure. (Or else the driver under you could have forgotten to clear DO_DEVICE_INITIALIZING, I suppose.) You would deal with the failure by cleaning up and returning STATUS_DEVICE_REMOVED from your AddDevice function.

Figure 2-21. What IoAttachDeviceToDeviceStack returns.

Clear DO_DEVICE_INITIALIZING

Pretty much the last thing you do in AddDevice should be to clear the DO_DEVICE_INITIALIZING flag in your driver object:

fdo->Flags &= ~DO_DEVICE_INITIALIZING;

While this flag is set, the I/O Manager will refuse to attach other device objects to yours or to open a handle to your device. You have to clear the flag because your device object initially arrives in the world with the flag set. In previous releases of Windows NT, most drivers created all of their device objects during DriverEntry. When DriverEntry returns, the I/O Manager automatically traverses the list of device objects linked from the driver object and clears this flag. Since you re creating your device object long after DriverEntry returns, however, this automatic flag clearing won t occur, and you must do it yourself.

Putting the Pieces Together

Here is a complete AddDevice function, presented without error checking or annotations and including all the pieces described in the preceding sections:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo; NTSTATUS status = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &fdo); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_SIMPLE, NULL, &pdx->ifname); pdx->DeviceObject = fdo; pdx->Pdo = pdo; IoInitializeRemoveLock(&pdx->RemoveLock, 0, 0, 0); pdx->devstate = STOPPED; pdx->devpower = PowerDeviceD0; pdx->syspower = PowerSystemWorking; IoInitializeDpcRequest(fdo, DpcForIsr); if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement) fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1; KeInitializeSpinLock(&pdx->SomeSpinLock); KeInitializeEvent(&pdx->SomeEvent, NotificationEvent, FALSE); InitializeListHead(&pdx->SomeListAnchor); fdo->Flags = DO_BUFFERED_IO DO_POWER_PAGABLE; pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo); fdo->Flags &= ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }

Категории