Êíèãà: Writing Windows WDM Device Drivers
The Wdm1 Driver Code
Ðàçäåëû íà ýòîé ñòðàíèöå:
The Wdm1 Driver Code
Table 4.4 lists all the source files used by the first driver called Wdm1. Table 4.5 lists all the build files that have already been described. In W2000 the build output files have slightly different names. These files are on the book CD-ROM.
This chapter looks at only some of the source files. As far as possible, the minimum possible functionality has been implemented in Wdm1. For example, a stub function has been written to handle Win32 create file requests. These stub functions usually make each request succeed.
If you were to put this Wdm1 driver to the test, it would not work in some circumstances. The succeeding chapters explain how the driver works and how its functionality has been enhanced to make it work better, as well as showing how to call the driver from Win32 code.
Table 4.4 Wdm1 source files
Wdm1.h |
Driver header |
Init.cpp |
Entry and unload code |
Pnp.cpp |
Plug and Play and Power handling code |
Dispatch.cpp |
Main IRP dispatch routines |
DebugPrint.c |
DebugPrint code |
DebugPrint.h |
DebugPrint header |
Wdm1.rc |
Version resource |
GUIDs.h |
GUID definition |
Ioctl.h |
IOCTL definition |
resource.h |
Visual Studio resource editor header |
Wdm1free.inf |
Free build installation instructions |
Wdm1checked.inf |
Checked build installation instructions |
Table 4.5 Wdm1 build files
SOURCES |
build instructions |
makefile.inc |
Post build steps for makefile |
makefile |
Standard makefile |
MakeDrvr.bat |
Makefile project batch file |
build.log |
build results log output |
build.err |
build errors output |
build.wrn |
build warnings output |
Compiler Options
The code includes some directives to the compiler that need some explaining.
The extern "C" directive is used to ensure that the compiler uses the correct linkage to reference kernel routines. The driver entry point, DriverEntry, must have extern "C" to ensure it is found.
The #pragma code_seg preprocessor directive is used to force routines into certain code segments. The INIT code segment is discarded after the driver has initialized itself, and the PAGE segment contains code that can be paged out of kernel memory. Using segments helps to lower a driver's memory usage.
Only routines that run at PASSIVE_LEVEL IRQL can be paged from memory. All the dispatch routines in the Wdm1 driver operate at PASSIVE_LEVEL so they can be put in the PAGE segment. Routines that are not given a segment are never paged from memory.
If writing C code, you can use the alloc_text pragma instead to set the code segment of named routines.
Header Files
The Wdm1.h header file is included in all the source files. Wdm1.h first includes the main DDK header file for WDM projects, wdm.h. If you were writing NT style drivers, you would use the similar ntddk.h. You may find that you need to include some other DDK header files for particular types of drivers.
Next, a device extension structure is defined. This structure is where a driver can hold any information it needs about a device (more on this later).
The GUIDs.H header defines a Globally Unique Identifier (GUID) for the Wdm1 device interface. The next chapter shows how this GUID is used by Win32 user mode applications to find Wdm1 devices. I used the guidgen utility to generate this GUID in the DEFINE_GUID format. I defined several consecutive GUIDs at once for all the examples in this book.
Finally, the IOCTL.H header defines the IOCTL codes that Wdm1 supports. These are explained in Chapter 7.
Driver Entry Module
Init.cpp contains the driver entry point. Listing 4.5 shows this routine, which must be called DriverEntry and use C linkage.
The Plug and Play Manager locates the correct driver and calls DriverEntry to initialize the driver (at PASSIVE_IRQL). In DriverEntry, the main job is to store a series of call back routine pointers in the passed DriverObject. This DRIVER_OBJECT structure is used by the operating system to store any information relevant to the driver. A separate structure is used later to store information about each device.
In Wdm1, DriverEntry sets a whole series of callback routines. These routines are called by the kernel when a device is added and when IRPs need to be sent to the driver. The WdmUnload routine, later in Init.cpp, does nothing at this stage.
Finally, DriverEntry returns an NTSTATUS value of STATUS_SUCCESS. Almost all driver routines have to return a NTSTATUS value, from the list in the NTSTATUS.H DDK header file. Note that these error codes do not correspond to Win32 error codes. The kernel does the necessary mapping between the two types of error code.
Listing 4.5 DriverEntry routine
extern "C"
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) {
NTSTATUS status = STATUS_SUCCESS;
//…
// Export other driver entry points…
DriverObject->DriverExtension->AddDevice = Wdm1AddDevice;
DriverObject->DriverUnload = Wdm1Unload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = Wdm1Create;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = Wdm1Close;
DriverObject->MajorFunction[IRP_MJ_PNP] = Wdm1Pnp;
DriverObject->MajorFunction[IRP_MJ_POWER] = Wdm1Power;
DriverObject->MajorFunction[IRP_MJ_READ] = Wdm1Read;
DriverObject->MajorFunction[IRP_MJ_WRITE] = Wdm1Write;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = Wdm1DeviceControl;
DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = Wdm1SystemControl;
// …
return status;
}
Version Resource
Wdm1.rc simply defines a version resource block with version and copyright information.
Accessing the Registry
The RegistryPath parameter to DriverEntry contains the registry key of the driver. The Wdm1 code does not currently use its RegistryPath parameter. However, it is common for drivers to use its registry key to store parameters for the whole driver. Therefore, I present the ReadReg routine shown in Listing 4.6. This reads two values from the driver's registry path Parameters subkey. The first value is a ULONG obtained from the value named UlongValue. The second value is a string obtained from the default value for the Parameters key.
Eventually ReadReg needs to call the RtlQueryRegistryValues kernel routine to read both the registry values in one fell swoop. One of the parameters is the absolute registry path, as a NULL-terminated wide string. The driver registry path is supplied in a UNICODE_STRING structure. Although this contains a wide string buffer, it may not necessarily be NULL-terminated. Unfortunately, this means that we have to laboriously make a copy of the string, simply to add on that dratted NULL-terminating character.
The first section of ReadReg does this job. It works out the length of buffer required and uses ExAllocatePool to allocate the memory from the paged pool. The RtlCopyMemory function is used to copy the bulk of the string over and RtlZeroMemory zeroes that all-important last character. You can do the copy and zero by hand if you want, though it should be more efficient to call the kernel functions.
Listing 4.6 ReadReg
void ReadReg(IN PUNICODE_STRING DriverRegistryPath) {
// Make zero terminated copy of driver registry path
USHORT FromLen = DriverRegistryPath->Length;
PUCHAR wstrDriverRegistryPath = (PUCHAR)ExAnocatePool(PagedPool, FromLen+sizeof(WCHAR));
if( wstrDriverRegistryPath==NULL) return;
RtlCopyMemory(wstrDriverRegistryPath, DriverRegistryPath->Buffer, FromLen);
RtlZeroMemory(wstrDriverRegistryPath+FromLen, sizeof(WCHAR));
// Initialise our ULONG and UNICODE_STRING values
ULONG UlongValue = –1;
UNICODE_STRING UnicodeString;
UnicodeString.Buffer = NULL;
UnicodeString.MaximumLength = 0;
UnicodeString.Length = 0;
// Build up our registry query table
RTL_QUERY_REGISTRY_TABLE QueryTable[4];
RtlZeroMemory(QueryTable, sizeof(QueryTable));
QueryTable[0].Name = L"Parameters";
QueryTable[0].Flags = RTL_QUERY_REGISTRY_SUBKEY;
QueryTable[0].EntryContext = NULL;
QueryTable[1].Name = L"UlongValue";
QueryTable[1].Flags = RTL_QUERY_REGISTRY_DIRECT;
OueryTable[1].EntryContext = &UlongValue;
QueryTable[2].Name = L""; // Default value
QueryTable[2].Flags = RTL_QUERY_REGISTRY_DIRECT;
QueryTable[2].EntryContext = &UnicodeString;
// Issue query
NTSTATUS status = RtlQueryRegistryValues(RTL_REGISTRY_ABSOLUTE, (PWSTR)wstrDriverRegistryPath, QueryTable, NULL, NULL);
// Print results
DebugPrint("ReadReg %x: UlongValue %x UnicodeString %T", status.UlongValue, &UnicodeString);
// Do not forget to free buffers
if (UnicodeString.Buffer!=NULL) ExFreePool(UnicodeString.Buffer);
ExFreePool(wstrDriverRegistryPath);
}
The UNICODE_STRING Structure
This is how the UNICODE_STRING type is defined.
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
The Buffer field points to a wide 16-bit character buffer. The character string is not usually NULL-terminated. Instead, the Length field gives the current size of the string in bytes. The MaximumLength field gives the maximum size of the string that can fit in the buffer in bytes. This design is used to avoid reallocating string buffers too often. However, it does make manipulating Unicode strings a bit awkward. Table 4.6 shows all the kernel routines that you can use with Unicode strings.
Just in case you were interested, you do not have to use these kernel routines to access Unicode strings. You can fiddle with a string structure however you like, as long as it is in a valid format when passed to the kernel.
Table 4.6 UNICODE_STRING functions
RtlAnsiStringToUnicodeString |
Converts an ANSI string to a Unicode string, optionally allocating a buffer. |
RtlAppendUnicodeStringToString |
Append one Unicode string to another, up to the length ofthe destination buffer. |
RtlAppendUnicodeToString |
Append a wide string to a Unicode string, up to the length of the destination buffer. |
RtlCompareUnicodeString |
Compares two Unicode strings, optionally case-insensitive. |
RtlCopyUnicodeString |
Copies one Unicode string to another, up to the length of the destination buffer. |
RtlEqualUnicodeString |
Returns TRUE if the two Unicode strings are equal, optionally case-insensitive. |
RtlFreeUnicodeString |
Frees the Unicode string buffer memory from the pool. |
RtlInitUnicodeString |
Sets the Unicode string buffer to point to the given wide string, and sets the length fields to match. |
RtlIntegerToUnicodeString |
Converts a ULONG value to a Unicode string in the specified base. The string buffer must have been initialized beforehand. |
RtlPrefixUnicodeString† |
Sees if one Unicode string is a prefix of another, optionally case-insensitive. |
RtlUnicodeStringToAnsiString |
Converts a Unicode string into ANSI. If you ask for the destination ANSI buffer to be allocated, free it eventually with RtlFreeAnsiString. |
RtlUnicodeStringToInteger |
Converts a Unicode string to an integer. |
RtlUpcaseUnicodeString† |
Converts a Unicode string into uppercase, optionally allocating a buffer. |
†NT/W2000 only
ReadReg declares the variables that will receive the values from the registry. For a UNICODE_STRING, its Buffer, Length, and MaximumLength fields have to be initialized. In this case, these fields are initialized to NULL and zero. The call to RtlQueryRegistryValues will allocate a Buffer and set the length fields.
In most cases, a UNICODE_STRING's buffer needs to be set up correctly. If you want to store an unchanging wide string value in a Unicode string, use the RtlInitUnicodeString function. The Unicode string Buffer is set to point to the passed string and the Length and Maximum-Length strings are set to the length of the string[10].
RtlInitUnicodeString(&UnicodeString, L"DeviceWdm1");
If you wish to work with the contents of a Unicode string, you need to provide the wide string buffer. Use code like this.
const int MAX_CHARS = 30;
UNICODE_STRING UnicodeString;
WCHAR UnicodeStringBuffer[MAX_CHARS];
UnicodeString.Buffer = UnicodeStringBuffer;
UnicodeString.MaximumLength = MAX_CHARS*2;
UnicodeString.Length = 0;
In this case, the buffer is on the stack. If you want to use the Unicode string after this routine has completed, you must allocate the buffer from pool memory. Do not forget to free this memory once you have finished using the string.
Calling RtlQueryRegistryValues
You must send a query table array to RtlQueryRegistryValues. This array details the actions that you want to do. The last entry in the query table must be zeroed to indicate the end of the list. This is achieved when the entire query table is zeroed using RtlZeroMemory in ReadReg.
There is a wide range of options available when querying the registry. The Flags field in each query table element indicates the action you want to do. The first query listed previously uses RTL_QUERY_REGISTRY_SUBKEY, which means that the Name field contains the subkey for subsequent queries.
The following queries both set the Flags field to RTL_QUERY_REGISTRY_DI RECT. In this case, the Name field contains the registry value name, and the EntryContext field contains a pointer to the variable to receive the value. You have to trust that no one has fiddled with the value types so that a string is returned when you were expecting a ULONG.
A safer approach (not used here) is to pass the name of a callback routine in the QueryRoutine field and set the Flags field to zero. Your routine is called for each value found, and indicates the type of the found value.
The remaining parameters to RtlQueryRegistryValues give even more flexibility. RTL_REGISTRY_ABSOLUTE indicates that the Path parameter is an absolute registry path. Various other useful options can be given (e.g., if the Path is relative to the HKLMSystcmCurrentControlSetServices key).
RtlQueryRegistryValues only returns STATUS_SUCCESS if all the queries were processed correctly and all the registry values were found. The code in ReadReg simply displays the return status and the values retrieved. If using this routine for real, you will probably want to store the values in global variables.
Operating system version
You can use a registry setting to determine at run time whether you are running in W98 or not. In NT and W2000 the following registry value is available, HKLMSystemCurrentControlSetControlProductOptionsProductType. The value is "WinNT" for the Workstation/Professional Windows version and either "LanmanNT" or "ServerNT" for the Server/Enterprise version. In W98 this registry value should not be available.
- Testing Wdm3 Events
- LPT Printer Driver Application
- Ñèñòåìíûå ïåðåìåííûå ROWS_AFFECTED, GDSCODE, SQLCODE, TRANSACTIONJD, CONNECTIONJD
- JDBC Òóðå 4 DRIVER
- 4.4.4 The Dispatcher
- About the author
- Chapter 7. The state machine
- Appendix E. Other resources and links
- Appendix J. Example scripts code-base
- Example NAT machine in theory
- The final stage of our NAT machine
- Compiling the user-land applications