In the remainder of this chapter, I'm going to discuss some miscellaneous topics that might be useful in various parts of your driver. I'll begin by describing how you access the registry database, which is where you can find various configuration and control information that might affect your code or your hardware. I'll go on to describe how you access disk files and other named devices. A few words will suffice to describe how you can perform floating-point calculations in a WDM driver. Finally, I'll describe a few of the features you can embed in your driver to make it easier to debug your driver in the unlikely event () it shouldn't work correctly the first time you try it out.
Windows NT and Windows 98 record configuration and other important information in a database called the registry. WDM drivers can call the functions listed in Table 3-9 to access the registry. If you've done user-mode programming involving registry access, you might be able to guess how to use these functions in a driver. I found the kernel-mode support functions sufficiently different, however, that I think it's worth describing how you might use them.
Table 3-9. Service functions for registry access.
Service Function | Description |
---|---|
IoOpenDeviceRegistryKey | Open special key associated with a PDO |
IoOpenDeviceInterfaceRegistryKey | Open a registry key associated with a registered device interface |
RtlDeleteRegistryValue | Delete a registry value |
RtlQueryRegistryValues | Read several values from the registry |
RtlWriteRegistryValue | Write a value to the registry |
ZwClose | Close handle to a registry key |
ZwCreateKey | Create a registry key |
ZwDeleteKey | Delete a registry key |
ZwEnumerateKey | Enumerate subkeys |
ZwEnumerateValueKey | Enumerate values within a registry key |
ZwFlushKey | Commit registry changes to disk |
ZwOpenKey | Open a registry key |
ZwQueryKey | Get information about a registry key |
ZwQueryValueKey | Get a value within a registry key |
ZwSetValueKey | Set a value within a registry key |
In this section, I'll discuss, among other things, the ZwXxx family of routines and RtlDeleteRegistryValue, which provide the basic registry functionality that suffices for most WDM drivers.
Before you can interrogate values in the registry, you need to open the key that contains them. You use ZwOpenKey to open an existing key. You use ZwCreateKey either to open an existing key or to create a new key. Either function requires you to first initialize an OBJECT_ATTRIBUTES structure with the name of the key and (perhaps) other information. The OBJECT_ATTRIBUTES structure has the following declaration:
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES; |
Rather than initialize an instance of this structure by hand, it's easiest to use the macro InitializeObjectAttributes, which I'm about to show you.
Suppose, for example, that we wanted to open the service key for our driver. The I/O Manager gives us the name of this key as a parameter to DriverEntry. So, we could write code like the following:
1 2 3 |
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { ... OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(&oa, RegistryPath, 0, NULL, NULL); HANDLE hkey; status = ZwOpenKey(&hkey, KEY_READ, &oa); if (NT_SUCCESS(status)) { ... ZwClose(hkey); } ... } |
Even though we often refer to the registry as being a database, it doesn't have all of the attributes that have come to be associated with real databases. It doesn't allow for committing or rolling back changes, for example. Furthermore, the access rights you specify when you open a key (KEY_READ in the previous example) are for security checking rather than for the prevention of incompatible sharing. That is, two different processes can have the same key open after specifying write access (for example). The system does guard against destructive writes that occur simultaneously with reads, however, and it does guarantee that a key won't be deleted while someone has an open handle to it.
In addition to ZwOpenKey, Windows 2000 provides two other functions for opening registry keys.
IoOpenDeviceRegistryKey allows you to open one of the special registry keys associated with a device object:
HANDLE hkey; status = IoOpenDeviceRegistryKey(pdo, flag, access, &hkey); |
where pdo is the address of the physical device object (PDO) at the bottom of your particular driver stack, flag is an indicator for which special key you want to open (see Table 3-10), and access is an access mask such as KEY_READ.
Table 3-10. Registry key codes for IoOpenDeviceRegistryKey.
Flag Value | Selected Registry Key |
---|---|
PLUGPLAY_REGKEY_DEVICE | The hardware (instance) subkey of the Enum key |
PLUGPLAY_REGKEY_DRIVER | The software (service) key |
IoOpenDeviceInterfaceRegistryKey opens the key associated with an instance of a registered device interface:
HANDLE hkey; status = IoOpenDeviceInterfaceRegistryKey(linkname, access, &hkey); |
where linkname is the symbolic link name of the registered interface and access is an access mask like KEY_READ.
The interface registry key is a subkey of HKLM\System\CurrentControlSet\Control\DeviceClasses that persists from one session to the next. It's a good place to store parameter information that you want to share with user-mode programs, because user-mode code can call SetupDiOpenDeviceInterfaceRegKey to gain access to the same key.
In Chapter 12, "Installing Device Drivers," I'll discuss how your installation script can insert values into the hardware and interface keys, and how application programs can access these values.
Usually, you open a registry key because you want to retrieve a value from the database. The basic function you use for that purpose is ZwQueryValueKey. For example, to retrieve the ImagePath value in the driver's service key—I don't actually know why you'd want to know this, but that's not my department—you could use the following code:
UNICODE_STRING valname; RtlInitUnicodeString(&valname, L"ImagePath"); size = 0; status = ZwQueryValueKey(hkey, &valname, KeyValuePartialInformation, NULL, 0, &size); if (status == STATUS_OBJECT_NOT_FOUND || size == 0) <handle error>; PKEY_VALUE_PARTIAL_INFORMATION vpip = (PKEY_VALUE_PARTIAL_INFORMATION) ExAllocatePool(PagedPool, size); if (!vpip) <handle error>; status = ZwQueryValueKey(hkey, &valname, KeyValuePartialInformation, vpip, size, &size); if (!NT_SUCCESS(status)) <handle error>; <do something with vpip->Data> ExFreePool(vpip); |
Here, we make two calls to ZwQueryValueKey. The purpose of the first call is to determine how much space we need to allocate for the KEY_VALUE_PARTIAL_INFORMATION structure we're trying to retrieve. The second call retrieves the information. I left the error checking in this code fragment because the errors didn't work out in practice the way I expected them to. In particular, I initially guessed that the first call to ZwQueryValueKey would return STATUS_BUFFER_TOO_SMALL if I passed it a NULL buffer pointer. It didn't do that, though. The important failure code is STATUS_OBJECT_NAME_NOT_FOUND, which indicates that the value doesn't actually exist. Hence, I test for that value only. If there's some other error that prevents ZwQueryValueKey from working, the second call will uncover it.
The so-called "partial" information structure you retrieve in this way contains the value's data and a description of its data type:
typedef struct _KEY_VALUE_PARTIAL_INFORMATION { ULONG TitleIndex; ULONG Type; ULONG DataLength; UCHAR Data[1]; } KEY_VALUE_PARTIAL_INFORMATION, *PKEY_VALUE_PARTIAL_INFORMATION; |
Type is one of the registry data types listed in Table 3-11. (Additional data types are possible but not interesting to device drivers.) DataLength is the length of the data value, and Data is the data itself. TitleIndex has no relevance to drivers. Here are some useful facts to know about the various data types:
NOTE
RtlQueryRegistryValues is a complex routine for which I'm not providing an example here. The DDK samples contain several drivers that use it.
Table 3-11. Types of registry values useful to WDM drivers.
Data Type Constant | Description |
---|---|
REG_BINARY | Variable-length binary data |
REG_DWORD | Unsigned long integer in natural format for the platform |
REG_DWORD_BIG_ENDIAN | Unsigned long integer in big-endian format |
REG_EXPAND_SZ | Null-terminated Unicode string containing %-escapes for environment variable names |
REG_MULTI_SZ | One or more null-terminated Unicode strings, followed by an extra null |
REG_SZ | Null-terminated Unicode string |
To set a registry value, you must have KEY_SET_VALUE access to the parent key. I used KEY_READ earlier, which wouldn't give you such access. You could use KEY_WRITE or KEY_ALL_ACCESS, although you thereby gain more than the necessary permission. Then call ZwSetValueKey. For example:
RtlInitUnicodeString(&valname, L"TheAnswer"); ULONG value = 42; ZwSetValueKey(hkey, &valname, 0, REG_DWORD, &value, sizeof(value)); |
To delete a value in an open key, you can use RtlDeleteRegistryValue in the following special way:
RtlDeleteRegistryValue(RTL_REGISTRY_HANDLE, (PCWSTR) hkey, L"TheAnswer"); |
RtlDeleteRegistryValue is a general service function whose first argument can designate one of several special places in the registry. When you use RTL_REGISTRY_HANDLE, as I did in this example, you indicate that you've already got an open handle to the key within which you want to delete a value. You specify the key (with a cast to make the compiler happy) as the second argument. The third and final argument is the null-terminated Unicode name of the value you want to delete. This is one time when you don't have to create a UNICODE_STRING structure to describe the string.
You can delete only those keys that you've opened with at least DELETE permission (which you get with KEY_ALL_ACCESS). You call ZwDeleteKey:
ZwDeleteKey(hkey); |
The key lives on until all handles are closed, but subsequent attempts to open a new handle to the key or to access the key by using any currently open handle will fail with STATUS_KEY_DELETED. Since you have an open handle at this point, you must be sure to call ZwClose sometime. (The DDK documentation entry for ZwDeleteKey says the handle becomes invalid. It doesn't—you must still close it by calling ZwClose.)
A complicated activity you can carry out with an open registry key is to enumerate the elements (subkeys and values) that the key contains. To do this, you'll first call ZwQueryKey to determine a few facts about the subkeys and values, such as their number, the length of the largest name, and so on. ZwQueryKey has an argument that indicates which of three types of information you want to retrieve about the key. These types are named basic, node, and full. To prepare for an enumeration, you'd be interested first in the full information:
typedef struct _KEY_FULL_INFORMATION { LARGE_INTEGER LastWriteTime; ULONG TitleIndex; ULONG ClassOffset; ULONG ClassLength; ULONG SubKeys; ULONG MaxNameLen; ULONG MaxClassLen; ULONG Values; ULONG MaxValueNameLen; ULONG MaxValueDataLen; WCHAR Class[1]; } KEY_FULL_INFORMATION, *PKEY_FULL_INFORMATION; |
This structure is actually of variable length, since Class[0] is just the first character of the class name. It's customary to make one call to find out how big a buffer you need to allocate and a second call to get the data, as follows:
ULONG size; ZwQueryKey(hkey, KeyFullInformation, NULL, 0, &size); PKEY_FULL_INFORMATION fip = (PKEY_FULL_INFORMATION) ExAllocatePool(PagedPool, size); ZwQueryKey(hkey, 0, KeyFullInformation, bip, size, &size); |
Were you now interested in the subkeys of your registry key, you could perform the following loop calling ZwEnumerateKey:
for (ULONG i = 0; i < fip->SubKeys; ++i) { ZwEnumerateKey(hkey, i, KeyBasicInformation, NULL, 0, &size); PKEY_BASIC_INFORMATION bip = (PKEY_BASIC_INFORMATION) ExAllocatePool(PagedPool, size); ZwEnumerateKey(hkey, i, KeyBasicInformation, bip, size, &size); <do something with bip->Name> ExFreePool(bip); } |
The key fact you discover about each subkey is its name, which shows up as a counted Unicode string in the KEY_BASIC_INFORMATION structure you retrieve inside the loop:
typedef struct _KEY_BASIC_INFORMATION { LARGE_INTEGER LastWriteTime; ULONG Type; ULONG NameLength; WCHAR Name[1]; } KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION; |
The name isn't null-terminated; you must use the NameLength member of the structure to determine its length. Don't forget that the length is in bytes! The name isn't the full registry path either: it's the just the name of the subkey within whatever key contains it. This is actually lucky, because you can easily open a subkey given its name and an open handle to its parent key.
To accomplish an enumeration of the values in an open key, employ the following method:
ULONG maxlen = fip->MaxValueNameLen + sizeof(KEY_VALUE_BASIC_INFORMATION); PKEY_VALUE_BASIC_INFORMATION vip = (PKEY_VALUE_BASIC_INFORMATION) ExAllocatePool(PagedPool, maxlen); for (ULONG i = 0; i < fip->Values; ++i) { ZwEnumerateValueKey(hkey, i, KeyValueBasicInformation, vip, maxlen, &size); <do something with vip->Name> } ExFreePool(vip); |
Allocate space for the largest possible KEY_VALUE_BASIC_INFORMATION structure that you'll ever retrieve based on the MaxValueNameLen member of the KEY_FULL_INFORMATION structure. Inside the loop, you'll want to do something with the name of the value, which comes to you as a counted Unicode string in this structure:
typedef struct _KEY_VALUE_BASIC_INFORMATION { ULONG TitleIndex; ULONG Type; ULONG NameLength; WCHAR Name[1]; } KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION; |
Once again, having the name of the value and an open handle to its parent key is just what you need to retrieve the value, as shown in the previous section.
There are variations on ZwQueryKey and on these two enumeration functions that I haven't discussed. You can, for example, obtain full information about a subkey when you call ZwEnumerateKey. I showed you only how to get the basic information that includes the name. You can retrieve data values only, or names plus data values, from ZwEnumerateValueKey. I showed you only how to get the name of a value.
It's sometimes useful to be able to read and write regular disk files from inside a WDM driver. Perhaps you need to download a large amount of microcode to your hardware, or perhaps you need to create your own extensive log of information for some purpose. There's a set of ZwXxx routines to help you do these things.
The first step in accessing a disk file is to open a handle by calling ZwCreateFile. The full description of this function in the DDK is relatively complex because of all the ways in which it can be used. I'm going to show you two simple scenarios, however, that are useful if you just want to read or write a file whose name you already know.
To open an existing file so that you can read it, follow this example:
NTSTATUS status; OBJECT_ATTRIBUTES oa; IO_STATUS_BLOCK iostatus; HANDLE hfile; // the output from this process PUNICODE_STRING pathname; // you've been given this InitializeObjectAttributes(&oa, pathname, OBJ_CASE_INSENSITIVE, NULL, NULL); status = ZwCreateFile(&hfile, GENERIC_READ, &oa, &iostatus, NULL, 0, FILE_SHARE_READ, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); |
To create a new file, or to open and truncate to zero length an existing file, replace the call to ZwCreateFile in the previous fragment with this one:
status = ZwCreateFile(&hfile, GENERIC_WRITE, &oa, &iostatus, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OVERWRITE_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); |
In these fragments, we set up an Object Attributes structure whose main purpose is to point to the full pathname of the file we're about to open. We specify the OBJ_CASE_INSENSITIVE attribute because the Win32 file system model does not treat case as significant in a pathname. Then we call ZwCreateFile to open the handle.
The first argument to ZwCreateFile (&hfile) is the address of the HANDLE variable where ZwCreateFile will return the handle it creates. The second argument (GENERIC_READ or GENERIC_WRITE) specifies the access we need to the handle to perform either reading or writing. The third argument (&oa) is the address of the OBJECT_ATTRIBUTES structure containing the name of the file. The fourth argument points to an IO_STATUS_BLOCK that will receive a disposition code indicating how ZwCreateFile actually implemented the operation we asked it to perform. When we open a read-only handle to an existing file, we expect the Status field of this structure to end up equal to FILE_OPENED. When we open a write-only handle, we expect it to end up equal to FILE_OVERWRITTEN or FILE_CREATED, depending on whether the file did or did not already exist. The fifth argument (NULL) can be a pointer to a 64-bit integer that specifies the initial allocation size for the file. This argument matters only when you create or overwrite a file, and omitting it as I did here means that the file grows from zero length as you write data. The sixth argument (0 or FILE_ATTRIBUTE_NORMAL) specifies file attribute flags for any new file that you happen to create. The seventh argument (FILE_SHARE_READ or 0) specifies how the file can be shared by other threads. If you're opening for input, you can probably tolerate having other threads read the file simultaneously. If you're opening for sequential output, you probably don't want other threads trying to access the file at all.
The eighth argument (FILE_OPEN or FILE_OVERWRITE_IF) indicates how to proceed if the file either already exists or doesn't. In the read-only case, I specified FILE_OPEN because I expected to open an existing file and wanted a failure if the file didn't exist. In the write-only case, I specified FILE_OVERWRITE_IF because I wanted to overwrite any existing file by the same name or create a brand new file as necessary. The ninth argument (FILE_SYNCHRONOUS_IO_NONALERT) specifies additional flag bits to govern the open operation and the subsequent use of the handle. In this case, I indicated that I'm going to be doing synchronous I/O operations (wherein I expect the read or write function not to return until the I/O is complete). The tenth and eleventh arguments (NULL and 0) are, respectively, an optional pointer to a buffer for extended attributes and the length of that buffer.
You expect ZwCreateFile to return STATUS_SUCCESS and to set the handle variable. You can then carry out whatever read or write operations you please by calling ZwReadFile or ZwWriteFile, and then you close the handle by calling ZwClose:
ZwClose(hfile); |
You can perform synchronous or asynchronous reads and writes, depending on the flags you specified to ZwCreateFile. In the simple scenarios I've outlined, you would do synchronous operations that don't return until they've completed. For example:
PVOID buffer; ULONG bufsize; status = ZwReadFile(hfile, NULL, NULL, NULL, &iostatus, buffer, bufsize, NULL, NULL); -or- status = ZwWriteFile(hfile, NULL, NULL, NULL, &iostatus, buffer, bufsize, NULL, NULL); |
These calls are analogous to a nonoverlapped ReadFile or WriteFile call from user mode. When the function returns, you might be interested in iostatus.Information, which will hold the number of bytes transferred by the operation.
If you plan to read an entire file into a memory buffer, you would probably want to call ZwQueryInformationFile to determine the total length of the file:
FILE_STANDARD_INFORMATION si; ZwQueryInformationFile(hfile, &iostatus, &si, sizeof(si), FileStandardInformation); ULONG length = si.EndOfFile.LowPart; |
There are times when integer arithmetic just isn't sufficient to get your job done and you need to perform floating-point calculations. On an Intel processor, the math coprocessor is also where Multimedia Extensions (MMX) instructions execute. Historically, there have been two problems with drivers carrying out floating-point calculations. The operating system will emulate a missing coprocessor, but the emulation is expensive and normally requires a processor exception to trigger it. Handling exceptions, especially at elevated IRQLs, can be difficult in kernel mode. Additionally, on computers that have hardware coprocessors, the CPU architecture might require a separate, expensive operation to save and restore the coprocessor state during context switches. Therefore, conventional wisdom has forbidden kernel-mode drivers from using floating-point calculations.
Windows 2000 and Windows 98 provide a way around past difficulties. First of all, a system thread—see Chapter 9—running at or below DISPATCH_LEVEL is free to use the math coprocessor all it wants. In addition, a driver running in an arbitrary thread context at or below DISPATCH_LEVEL can use these two system calls to bracket its use of the math coprocessor:
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); KFLOATING_SAVE FloatSave; NTSTATUS status = KeSaveFloatingPointState(&FloatSave); if (NT_SUCCESS(status)) { ... KeRestoreFloatingPointState(&FloatSave); } |
These calls, which must be paired as shown here, save and restore the "nonvolatile" state of the math coprocessor for the current CPU—that is, all the state information that persists beyond a single operation. This state information includes registers, control words, and so on. In some CPU architectures, no actual work might occur because the architecture inherently allows any process to perform floating-point operations. In other architectures, the work involved in saving and restoring state information can be quite substantial. For this reason, Microsoft recommends that you avoid using floating-point calculations in a kernel-mode driver unless necessary.
What happens when you call KeSaveFloatingPointState depends, as I said, on the CPU architecture. To give you an idea, on an Intel-architecture processor, this function saves the entire floating-point state by executing an FSAVE instruction. It can save the state information either in a context block associated with the current thread or in an area of dynamically allocated memory. It uses the opaque FloatSave area to record "meta" information about the saved state to allow KeRestoreFloatingPointState to correctly restore the state later.
KeSaveFloatingPointState will fail with STATUS_ILLEGAL_FLOAT_CONTEXT if there's no real coprocessor present. (All CPUs of a multi-CPU computer must have coprocessors, or else none of them may, by the way.) Your driver will therefore need alternative code to carry out whatever calculations you had in mind, or else you'll want to decline to load (by failing DriverEntry) if the computer doesn't have a coprocessor.
My drivers always have bugs. Maybe you're as unlucky as I am. If so, you'll find yourself spending lots of time with a debugger trying to figure out what your code is doing or not doing correctly or incorrectly. I won't discuss the potentially divisive subject of which debugger is best or the noncontroversial but artistic subject of how to debug a driver. But you can do some things in your driver code that will make your life easier.
When you build your driver, you select either the "checked" or the "free" build environment. (Readers may now thank me for not making a bad joke about how the opposite of "checked" ought really to be named "striped" or something like that.) In the checked build environment, the preprocessor symbol DBG equals 1, whereas it equals 0 in the free build environment. So, one of the things you can do in your own code is to provide additional code that will take effect only in the checked build:
#if DBG <extra debugging code> #endif |
One of the most useful debugging techniques ever invented is to simply print messages from time to time. I used to do this when I was first learning to program (in FORTRAN on a computer made out of vacuum tubes, no less), and I still do it today. DbgPrint is a kernel-mode service routine you can call to display a formatted message in whatever output window your debugger provides. Another way to see the output from DbgPrint calls is to download the DbgView utility from http://www.sysinternals.com. Instead of directly referencing DbgPrint in your code, it's often easier to use the macro named KdPrint, which calls DbgPrint if DBG is true and generates no code at all if DBG is false:
KdPrint(("KeReadProgrammersMind failed with code %X\n", status)); |
You use two sets of parentheses with KdPrint because of the way it's defined. The first argument is a string with %-escapes where you want to substitute values. The second, third, and following arguments provide the values to go with the %-escapes. The macro expands into a call to DbgPrint, which internally uses the standard run-time library routine _vsnprintf to format the string. You can, therefore, use the same set of %-escape codes that are available to application programs that call this routine.
Another useful debugging technique relies on the ASSERT macro:
ASSERT(1 + 1 == 2); |
In the checked build of your driver, ASSERT generates code to evaluate the Boolean expression. If the expression is false, ASSERT will try to halt execution in the debugger so that you can see what's going on. If the expression is true, your program continues executing normally.
If you debug with Soft-Ice/W from Compuware (formerly Nu-Mega Technologies, Inc.), the ASSERT macro in the DDK isn't as useful as it might be. First of all, it relies on calling RtlAssert, which does nothing in the free version of the operating system. (You should test your driver in the checked build, but you can debug it perfectly well in the free build.) Second, if it does generate a debug exception, it does so inside RtlAssert rather than in the execution context of your code, which makes it more difficult for you to inspect local variables. You can replace the DDK ASSERT macro (for x86 only, which is the only place Soft-Ice/W currently runs anyway) to overcome these problems as follows:
#if DBG && defined(_X86_) #undef ASSERT #define ASSERT(e) if(!(e)){DbgPrint("Assertion failure in "\ _ _)FILE_ _) ", line %d: " #e "\n", _ _LINE_ _);\ _asm int 1\ } #endif |
Also remember to issue the Soft-Ice/W command i1here on so that the INT 1 traps from your ASSERT macros actually cause the debugger to halt. A possible disadvantage to replacing ASSERT like this is that you will bugcheck even in the free build of the operating system if you're not running a debugger when one of these ASSERTs fails.