52964.fb2
This chapter explains how to use device interfaces to make a driver available to the kernel and Win32 user-mode applications. The device interface code in the Wdm1 driver is described. The example Wdm1Test user-mode program shows how to use the Win32 functions to find device interfaces. Wdm1Test then sends some basic I/O requests to the Wdm1 driver.
A driver implements one or more devices. Each device represents an instance of a piece of real or virtual hardware. User-mode programs and the kernel access a driver's device through a device object.
Once a WDM driver's DriverEntry routine has completed, the driver has usually not created any devices. Instead, it has provided the kernel with various callback routine pointers. The Plug and Play (PnP) Manager calls the driver AddDevice callback routine each time the driver should create a device. The PnP Manager makes further calls to configure the device using the PnP IRP, as described in Chapter 8.
This chapter considers only two Plug and Play messages: when a device is added using AddDevice, and when it is removed. The Remove Device message is a PnP IRP with a minor function code of IRP_MN_REMOVE_DEVICE.
User mode programs access drivers using the Win32 CreateFile function, passing a string in the lpFileName parameter. This routine is normally used to access files on disk. However, it can also be used to access a variety of other devices, such as pipes, mailslots, communication resources, or the console. At the simplest level, specifying C0M1 opens the first serial port.
To access other devices, use \\.\ at the start of the lpFileName string. The following characters specify the device to open. For example, COM1 can also be opened as \\.\COM1. This form allows devices beyond COM9 to be opened. For example, opening C0M10 will not work but \\.\COM10 will work (assuming such a device exists).
To access a device created by a driver, you must know the symbolic link name of the device. A symbolic link name is simply the name of the device exposed by the driver. Creating these names is described later.
Suppose a driver called Dongle has created a device with a symbolic link of Dongle1. A Win32 program could open it using \\.\Dongle1 as the filename parameter passed to CreateFile.
There are two aspects to opening files that are important to understand. The first is that a device can be opened more than once, either in the same process or by different processes or threads. A driver can state that a Win32 thread gets exclusive access to the device until the handle is closed. Or the Win32 program can request exclusive access by setting the CreateFile dwShareMode parameter to zero. Alternative settings of dwShareMode permit other open requests if they specify read and/or write access. Even if a Win32 thread has exclusive access to a device, it can use overlapped I/O to issue more than one request to a driver at the same time.
Your device should cope with access through multiple handles, if it permits nonexclusive access. For example, if your device supports the notion of a file pointer, it has to survive "simultaneous" read or write requests at the same file pointer location. The usual technique is to serialize all requests so that they are processed in full, one by one, as described in Chapter 16.
The second important aspect of opening a device is that Win32 programs can use file or directory names after the device name. A driver must decide whether it supports the concept of "files" when a process opens a device handle. For example, \\.\Dongle1\dir1\file1 could be used in the call to CreateFile. It is up to you whether your driver interprets such information.
Figure 5.1 illustrates a possible device access situation. The Dongle driver has created two devices, called "Dongle1" and "Dongle2". Process A has opened handles to each of the Dongle devices. Process B has opened a handle to "file" on the "Dongle2" device.
Figure 5.1 Device access
Once a user mode program has opened a handle to your device, it can use various Win32 routines to access it, such as ReadFile, WriteFile, DeviceIoControl, and CloseHandle. Each of these calls results in a request being passed to your driver in the form of an I/O Request Packet (IRP). If a Win32 program terminates abruptly and finishes without calling CloseHandle, Windows ensures that this call is made (after cleaning up any pending I/O requests).
The Wdm1 handlers for these IRPs are described later, along with an introduction to the AddDevice and IRP_MJ_PNP handlers.
The kernel stores information about devices in device objects, DEVICE_OBJECT structures. The relevant DEVICE_OBJECT is passed to your driver callback routines for each interaction with a device.
For example, a Win32 call to ReadFile results in a Read IRP (i.e., with major function code IRP_MJ_READ) being sent to your device. The DriverEntry routine must set up a handler for the relevant callback. In the Wdm1 driver, this routine, Wdm1Read, is passed a pointer to the relevant device object and a pointer to the IRP.
The device object is "owned" by the kernel, but has a few fields that should be used by a device driver, as described later. However, a driver can define a block of memory called a device extension, which it can use for whatever it wants. The device extension structure for each Wdm1 device is defined in Wdm1.h as shown in Listing 5.1. Your device extension structures will probably start with the same fields as Wdm1.
Listing 5.1 Wdm1 Device Extension definition
typedef struct _WDM1_DEVICE_EXTENSION {
PDEVICE_OBJECT fdo;
PDEVICE_OBJECT NextStackDevice;
UNICODE_STRING ifSymlinkName;
} WDM1_DEVICE_EXTENSION, *PWDM1_DEVICE_EXTENSION;
A device is created using the IoCreateDevice kernel call. The DeviceExtensionSize parameter gives the size of the device extension required. IoCreateDevice allocates the memory for you from the nonpaged pool. Access the device extension through the device object DeviceExtension field. For example, Wdm1 accesses the its device extension using the following code.
PWDM1_DEVICE_EXTENSION dx = (PWDM1_DEVICE_EXTENSION)fdo->DeviceExtension;
A device object is deleted using IoDeleteDevice. This routine deallocates the device extension memory. Make sure that you clean up all your device-related objects before calling IoDeleteDevice.
Wdm1 creates and deletes devices in the Pnp.cpp module as shown in Listing 5.2. The chapter on Plug and Play explains when the Wdm1AddDevice and Wdm1Pnp routines are called.
The NT_SUCCESS macro is used to test whether the IoCreateDevice kernel call has completed. Do not forget to check that all calls to the kernel succeed. The NT_ERROR macro is not equivalent to !NT_SUCCESS. It is better to use !NT_SUCCESS, as it catches warning status values as well as errors.
Listing 5.2 Wdm1 Pnp.cpp Create and Delete device code
NTSTATUS Wdm1AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo) {
…
NTSTATUS status;
PDEVICE_OBJECT fdo;
// Create our Functional Device Object in fdo
status = IoCreateDevice(DriverObject, sizeof(WDM1_DEVICE_EXTENSION),
NULL, // No Name
FILE_DEVICE_UNKNOWN, 0,
FALSE, // Not exclusive
&fdo);
if (!NT_SUCCESS(status)) return status;
// Remember fdo in our device extension
PWDM1_DEVICE_EXTENSION dx = (PWDM1_DEVICE_EXTENSION)fdo->DeviceExtension;
dx->fdo = fdo;
…
// Attach to the driver stack below us
dx->NextStackDevice = IoAttachDeviceToDeviceStack{fdo,pdo);
// Set fdo flags appropriately
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
fdo->Flags |= DO_BUFFERED_IO;
…
}
NTSTATUS Wdm1Pnp( IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
…
// Device removed
if (MinorFunction==IRP_MN_REMOVE_DEVICE) {
// unattach from stack if (dx->NextStackDevice)
IoDetachDevice(dx->NextStackDevice);
// delete our fdo
IoDeleteDevice(fdo);
}
return status;
}
Wdm1AddDevice is called with two parameters, a pointer to the driver object and a pointer to a device object called the Physical Device Object (PDO).
Wdm1AddDevice calls IoCreateDevice to create the Wdm1 device using the parameters described in Table 5.2. The DeviceName parameter can be used to give a name to the device. However, as Wdm1 uses device interfaces, no name needs to be given, so NULL is passed here. Wdm1 devices do not correspond to any of the hardware device types, so FILE_DEVICE_UNKNOWN is given for DeviceType.
Table 5.1 IoCreateDevice function
NTSTATUS IoCreateDevice | (IRQL==PASSIVE_LEVEL) |
---|---|
Parameter | Description |
IN PDRIVER_OBJECT DriverObject | Driver object |
IN ULONG DeviceExtensionSize | Size of device extension required |
IN PUNICODE_STRING DeviceName | Name of device, or NULL |
IN DEVICE_TYPE DeviceType | The type of the device: one of the FILE_DEVICE_xxx values listed in the standard header WDM.H or NTDDK.H |
IN ULONG DeviceCharacteristics | Various constants ORed together to indicate removable media, read only, etc. |
IN BOOLEAN Exclusive | TRUE if only a single thread can access the device at a time |
OUT PDEVICE_OBJECT* DeviceObject | The returned device object. |
Table 5.2 IoRegisterDeviceInterface function
NTSTATUS IoRegisterDeviceInterface | (IRQL==PASSIVE_LEVEL) |
---|---|
Parameter | Description |
IN PDEVICE_OBJECT PhysicalDeviceObject | The device PDO |
IN CONST GUID *InterfaceClassGuid | The GUID being registered |
IN PUNICODE_STRING ReferenceString | Usually NULL. A reference string becomes part of the interface name and so can be used to distinguish between different interfaces to the same device. |
OUT PUNICODE_STRING SymbolicLinkName | The output interface symbolic link name. Do not forget to free the Unicode string buffer using RtlFreeUnicodeString when finished with it. |
If IoCreateDevice returns successfully, the DeviceObject parameter is set to point to the new device object. Wdm1 calls this its Functional Device Object (FDO). The Plug and Play chapter explains FDOs and PDOs in detail. Wdm1AddDevice returns an error if IoCreateDevice fails.
The Wdm1 device extension is now available through the FDO DeviceExtension field. Wdm1AddDevice stores a pointer to the FDO in its device extension.
Wdm1AddDevice now attaches the Wdm1 device to the device stack using IoAttachDeviceToDeviceStack, which must be called at PASSIVE_LEVEL[11]. The FDO and the given PDO are passed to this function. It returns a pointer to yet another device object that is also stored in the Wdm1 device extension. All this shenanigans are explained in the Plug and Play chapters.
Finally, two bits in the FDO Flags field must be changed. AddDevice routines must clear the DO_DEVICE_INITIALIZING flag. You do not need to clear this flag if you call IoCreateDevice in your DriverEntry routine. The DO_BUFFERED_IO flag is set in the Flags field. Again, this is covered later.
The Wdm1 driver must delete its FDO if it receives a Remove Device request. This is an IRP_ MJ_PNP_IRP with an IRP_MN_REMOVE_DEVICE minor version code. The code in Wdm1Pnp first calls IoDetachDevice to detach the FDO from the device stack. Finally, IoDeleteDevice is called to delete the FDO and its device extension.
IoDetachDevice and IoDeleteDevice must be called at IRQL PASSIVE_LEVEL
The Wdm1 AddDevice code calls IoCreateDevice to create a Wdm1 device object. There are two ways to provide a name that is available to Win32 programs. The old way is to provide an explicit symbolic link name. The newfangled approach is to use a device interface to identify the devices that support a defined API.
The IoCreateDevice call has a DeviceName parameter that you can use to give your device a name. This name identifies the device to the kernel, not to Win32.
You must create a symbolic link to make the kernel device name available to Win32. To do this the old way, call IoCreateSymbolicLink at IRQL PASSIVE_LEVEL, passing the desired symbolic link name and the kernel device name as parameters.
Explicit device names created using this technique usually have a device number at the end. By convention, kernel device name numbers increment from zero, while symbolic link names increment from one.
The best way to illustrate device naming is using the WinObj tool, which only runs in NT and Windows 2000. WinObj is supplied in the Platform SDK, not the W2000 DDK. The standard serial ports have kernel device names \device\Serial0, \device\Serial1, etc. In WinObj, these names appear in the \device folder.
Windows provides symbolic links "C0M1", "COM2", etc., to make these serial ports available to Win32. Symbolic links appear in the \?? folder in WinObj. You can double-click on each symbolic link to find out to which kernel device it refers. The screenshot in Figure 5.2 shows that COM3, \??\com3, is a symbolic link to kernel device Serial0.
The \?? folder used to be called \DosDevices, which you might come across in some old documentation.
Figure 5.2 WinObj tool screenshot
The Wdm1 driver could have given its first device object a kernel name of \device\ Wdm1device0 and called IoCreateSymbolicLink to create a symbolic link with the name \??\Wdm1dev1. A Win32 program would then have been able to open \\.\Wdm1dev1 using CreateFile.
Note that C strings represent each \ character as "\\", so you would have to pass " \\\\.\\Wdm1dev1" to CreateFile.
If this technique is used, you must keep track of how many devices are in use, so you can generate the correct names. If you use a global variable to keep the device count, you must use it carefully to ensure that simultaneous accesses do not corrupt its value. Achieve this using the InterlockedIncrement function that atomically increments a LONG value. This can be run at any IRQL and on paged memory. There are companion decrement, exchange, and compare functions.
Explicit kernel and symbolic link names are used most often in NT style drivers, which create devices in their DriverEntry routine. In this case, counting devices is easy, as you can be sure that no other part of your code will be trying to increment the device count.
You can use the QueryDosDevice Win32 function to get a list of all the symbolic link names currently available. For each symbolic link, you can call QueryDosDevice again to find the kernel name. QueryDosDevice is supposed to run in Windows 98, but I did not succeed in getting it to work. In NT and W2000, you can use the DefineDosDevice Win32 function to change symbolic links. You can experiment with both these commands using the dosdev utility in the W2000 DDK.
The Wdm1 driver uses a device interface to make its devices visible to Win32 programs. The idea is that each Wdm1 device makes a defined Application Programmer Interface (API) available. A Globally Unique Identifier (GUID) is used to identify this interface.
The device interface that each Wdm1 device exposes is the set of commands that a Win32 programmer can use to access the device. A Wdm1 device responds to Win32 CreateFile, ReadFile, WriteFile, DeviceIoControl, and CloseHandle calls in a defined way. For example, a Wdm1 device is nonexclusive, so more than one thread can open it at the same time.
The Wdm1 driver's job is to implement a memory buffer that is shared by all the Wdm1 devices. Any writes extend the buffer, if necessary. Reads do not extend the buffer. Four IOCTLs are supported: Zero buffer, remove buffer, get buffer size, and get buffer.
One benefit of device interfaces is that another driver could be written that also implements the Wdm1 API. As long as it does this faithfully, there is no reason why a Win32 program should notice the difference between the new driver and Wdm1.
While it seems unlikely that the Wdm1 device interface will be copied by others, this technique is very useful for other types of drivers. For example, audio miniport drivers should implement the IMiniport COM interface. This is identified using the IID_IMiniport GUID and means that the driver implements two functions, GetDescription and DataRangeIntersection.
Implementing a device interface for Wdm1 devices is straightforward. First, a new 16-byte GUID must be generated using the guidgen tool. Then, include code to register the Wdm1 device interface for each Wdm1 FDO device object.
Generating GUIDs
To get a suitable new GUID, run the guidgen tool in the Platform SDK or the VC++ executables directory. Figure 5.3 shows the window that appears. guidgen can generate GUIDs in formats suitable for four different uses. For driver header file definitions, choose the DEFINE_ GUID(…) option. Pressing the Copy button copies the necessary code to the clipboard. Paste it into your header and amend the text <<name>> to define your own identifier.
Microsoft says that all GUIDs that guidgen generates are unique.
If you need to allocate a series of identifiers, press the New GUID button as often as necessary and copy each one to the relevant header file. Each new GUID increments the last digit of the first eight characters of the GUID. For example, the next GUID after the one shown in the screenshot starts {F4886541… }. In this book, I often use the form {F4886540… } as an abbreviation for the full GUID {F4886540-749E-11d2-B677-00C0DFE4C1F3}.
Figure 5.3 guidgen tool
The definition of WDM1_GUID in GUIDs.h is shown in Listing 5.3. There is a final twist in the tail for GUID definitions. One of the driver modules, Pnp.cpp in Wdm1, must have the following preprocessor directive before including GUIDs.h to formally declare WDM1_GUID.[12]
#define INITGUID
Listing 5.3 WDM1_GUID definition in GUIDs.h
// Wdm1 device interface GUID
// {C0CF0640-5F6E-11d2-B677-00C0DFE4C1F31
DEFINE_GUID(WDM1_GUID, 0xc0cf0640, 0x5f6e, 0x11d2, 0xb6, 0x77, 0x0, 0xc0, 0xdf, 0xe4, 0xc1, 0xf3);
Device Interface Registration
Wdm1 does not give a kernel device name to its device object and does not use IoCreateSymbolicLink to create a device name accessible to Win32 programs. Instead, the Wdm1 AddDevice routine calls IoRegisterDeviceInterface to register its interface as shown in Listing 5.4. It does this after creating its device but before attaching it to the device stack. IoRegisterDeviceInterface creates a Win32 symbolic link name connection to the Wdm1 device object. This is stored in the device extension ifSymLinkNamefield.
The Wdm1AddDevice code checks the return value from IoRegisterDeviceInterface. If this call fails, it tidies up properly by deleting the FDO and returning the status error code.
The final step is to enable the device interface by calling IoSetDeviceInterfaceState, at IRQL PASSIVE_LEVEL In subsequent drivers, the device interface is only enabled when the PnP Start Device message has been received.
Listing 5.4 Wdm1 Pnp.cpp Device Interface code
// Register and enable our device interface
status = IoRegisterDeviceInterface(pdo, &WDM1_GUID, NULL, &dx->ifSymLinkName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(fdo);
return status;
}
IoSetDeviceInterfaceState(&dx->ifSymLinkName, TRUE);
Table 5.3 IoRegisterDeviceInterface function
NTSTATUS IoRegisterDeviceInterface | |
---|---|
Parameter | Description |
IN PDEVICE_OBJECT PhysicalDeviceObject | The device PDO |
IN CONST GUID *InterfaceClassGuid | The GUID being registered |
IN PUNICODE_STRING ReferenceString | Usually NULL. A reference string becomes part of the interface name and so can be used to distinguish between different interfaces to the same device. |
OUT PUNICODE_STRING SymbolicLinkName | The output interface symbolic link name. Do not forget to free the Unicode string buffer using RtlFreeUnicodeString when finished with it. |
The code in Wdm1Pnp to handle device removal has to be altered, as well. First, the device interface has to be disabled. Then, it must free the memory buffer allocated for the interface symbolic link name. It does not have to Unregister the device interface.
IoSetDeviceInterfaceState(&dx->ifSymLinkName, FALSE);
RtlFreeUnicodeString(&dx->ifSytnLinkName);
Device Interface Notes
As mentioned in the last chapter, registering a device interface creates a registry entry in the HKLM\System\CurrentControlSet\Control\DeviceClasses key. A subkey named after the GUID and further subkeys for each device implements that GUID. Drivers can use IoOpenDeviceInterfaceRegistryKey to open a handle to this registry key. For example, you might want to add a FriendlyName value to this key using an INF file AddInterface section, as described in Chapter 11. Win32 applications can use SetupDiOpenDeviceInterfaceRegKey to open this same key and retrieve the friendly name.
Device interface registrations can be removed from user mode, if necessary.
A driver can register that a device supports more than one device interface if desired.
IoGetDeviceInterfaces can be used by a driver to find a list of symbolic links to devices that support a specific device interface.
Behind the scenes, Windows generates a kernel device name for your device. This might be something like \devi ce\004059. The symbolic link name generated by IoRegisterDeviceInterface looks like \DosDevices\000000000000001c#{c0cf0640…}. The WinObj entry for the symbolic link looks like \??\Root#UNKN0WN#0000#{C0CF0640…}.
We are ready — at last — to access a Wdm1 device. The Wdm1Test program opens a connection to the Wdm1 driver and puts it through its paces. Remember that all the calls in this section are to Win32 routines, not to the kernel.
The source code for the Wdm1Test program is in the Wdm1\exe directory. The book software has a Visual Studio WDM Book workspace, which includes a project for the Wdm1Test program. The Wdm1Test code in Wdm1Test.cpp is listed at the end of the chapter.
Wdm1Test is a standard Win32 console application. When run, it appears in a DOS box. As it is a standard Win32 program, you can debug it in Visual Studio as normal (e.g., step through the code).
There are two points to note about the Wdm1Test project. The first is that it includes the c:\98ddk\inc\win98\setupapi.h header file. The VC++ 5 version of this file is seriously out of date, so the code specifically includes the Windows 98 DDK version. The second special setting required is to ensure that c:\98ddk\lib\i386\free\setupapi.lib is listed in the Link property page Output/library modules section of the Project settings.
The GetDeviceViaInterface routine in Wdm1Test.cpp opens a handle to a device, given the device interface's GUID, as shown in Listing 5.5. GetDeviceViaInterface has a second parameter, instance, which is the zero-based index into the count of available devices. The total number of devices cannot be determined in advance; simply call GetDeviceViaInterface until NULL is returned.
The Wdm1Test main function calls GetDeviceViaInterface with WDM1_GUID and 0 as parameters to open the first available Wdm1 device. Special steps must be taken again to ensure that WDM1_GUID is declared properly. In one module #include "initguid.h" before including the GUID header file.
Listing 5.5 GetDeviceViaInterface
HANDLE GetDeviceViaInterface(GUID* pGuid, DWORD instance) {
// Get handle to relevant device information set
HDEVINFO info = SetupDiGetClassDevs(pGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
if (info==INVALID_HANDLE_VALUE) {
printf("No HDEVINFO available for this GUID\n");
return NULL;
}
// Get interface data for the requested instance
SP_INTERFACE_DEVICE_DATA ifdata;
ifdata.cbSize = sizeof(ifdata);
if (!SetupDiEnumDeviceInterfaces(info, NULL, pGuid, instance, &ifdata)) {
printf("No SP_INTERFACE_DEVICE_DATA available for this GUID instance \n");
SetupDiDestroyDeviceInfoList(info);
return NULL;
}
// Get size of symbolic link name
DWORD ReqLen;
SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &ReqLen, NULL);
PSP_INTERFACE_DEVICE_DETAIL_DATA ifDetail = (PSP_INTERFACE_DEVICE_DETAIL_DATA)(new char[ReqLen]);
if (ifDetail==NULL) {
SetupDiDestroyDeviceInfoList(info);
return NULL;
}
// Get symbolic link name
ifDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
if (!SetupDiGetDeviceInterfaceDetail(info, &ifdata, ifDetail, ReqLen, NULL, NULL)) {
SetupDiDestroyDeviceInfoList(info);
delete ifDetail;
return NULL;
}
printf("Symbolic link is %s\n",ifDetail->DevicePath):
// Open file
HANDLE rv = CreateFile(ifDetail->DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
delete ifDetail;
SetupDiDestroyDevicelnfoList(info);
return rv;
}
The Win32 call to SetupDiGetClassDevs opens a "device information set" about devices with the specified GUID. The DIGCF_PRESENT and DIGCF_INTERFACEDEVICE flags ensure that only devices that are present are found.
SetupDiEnumDeviceInterfaces is then called to retrieve a SP_INTERFACE_DEVICE_DATA context structure of information about the device instance in which you are interested. GetDeviceViaInterface tries to retrieve information about only one instance, but you might want to find all instances by incrementing the MemberIndex parameter from 0 until SetupDiEnumDeviceInterfaces fails and GetLastError returns ERROR_NO_MORE_ITEMS.
The next task is to obtain the symbolic link name for the instance that has been found. This string is in the PSP_INTERFACE_DEVICE_DETAIL_DATA ifDetail structure returned by a call to SetupDiGetDeviceInterfaceDetail. SetupDiGetDeviceInterfaceDetail must be called twice. The first call retrieves the required size for ifDetail, while the second actually gets the structure.
Eventually ifDetail->DevicePath contains the filename that is used to open a handle to the relevant Wdm1 device. This filename is passed to CreateFile and then ifDetail is deleted. Do not forget to call SetupDiDestroyDeviceInfoList on all paths to close the device information set.
The call to CreateFile in GetDeviceViaInterface uses a pretty standard set of parameters. As noted before, you can change the share and access modes, if necessary. Further, the device can be opened for overlapped nonblocking access. Overlapped access works for devices in Windows 98, NT, and Windows 2000 and lets an application issue an I/O request and get on with other work while the request is being processed. The detailed explanation of the DebugPrint Monitor program in Chapter 14 gives an example of this technique.
Simply run Wdm1Test.exe in the Wdm1\exe\Release directory. Make sure that the Wdm1 driver is installed. Wdm1Test is a console application running in a DOS box. Press the Enter key to exit the program.
If you run Wdm1Test in W2000, the output should look like that given in Listing 5.6. The first time round, Test 2 is designed to fail; subsequent tests should succeed. In Windows 98, the output is different, as it does not seem to support the SetFilePointer function on devices, such as Wdm1.
The rest of the Wdm1Test main function performs each of these tests in turn.
1. Open the first Wdm1 device.
2. Read the first DWORD stored in the shared memory buffer. This will fail first time round, as there will be nothing in the buffer.
3. Write 0x12345678 to the start of the buffer. 0x78 is written at file pointer zero, 0x56 at one, 0x34 at two, and 0x12 at three.
4. Set the file pointer to position 3.
5. Read one byte from the file. This should be 0x12.
6. Write 0x12345678 to the buffer starting at file position 3.
7. Use an IOCTL to get the buffer size, which should be 7 (i.e., 3+4).
8. Use an IOCTL to get the entire buffer. The first word is printed and should be 0x78345678.
9. Check that issuing an IOCTL with an invalid parameter fails. The IOCTL asks for the entire buffer with a request size that is too big.
10. Use an IOCTL to zero all the bytes in the buffer. Get the entire buffer again. The first word is printed which should be 0x00000000.
11. Use an IOCTL to remove the buffer. Check that the buffer size is now zero.
12. Try to issue an invalid IOCTL code. Confirm that this fails.
13. Write 0xabcdef01 to the start of the buffer. When Wdm1Test is run next, Test 2 should find this value.
Listing 5.6 Wdm1Test output on W2000
Test 1
Symbolic link is \\?\root#unknown#0003#{c0cf0640-5f6e-11d2-b677-00c0dfe4c1f3}
Opened OK
Test 2
Read successfully read stored value of 0xABC0EF01
Test 3
Write 0x12345678 succeeded
Test 4
SetFilePointer worked
Test 5
Read successfully read stored value of 0x12
Test 6
Write at new file pointer succeeded
Test 7
Buffer size is 7 (4 bytes returned)
Test 8
First DWORD of buffer is 78345678 (7 bytes returned)
Test 9
Too big get buffer failed correctly 87
Test 10
Zero buffer succeeded
First DWORD of buffer is 00000000 (7 bytes returned)
Test 11
Remove buffer succeeded
Buffer size is 0 (4 bytes returned)
Test 12
Unrecognised IOCTL correctly failed 1
Test 13
SetFilePointer worked Write 0xabcdef01 succeeded
Test 14
CloseHandle worked
Press enter please
Wdm1Test exercises all the functions of the Wdm1 driver. It checks that things that should work do work, and things that should fail do fail.
Writing this test program showed that the Wdm1 device does behave differently in Windows 98 and Windows 2000. However, the problem does not lie in the driver itself, but is due to Windows 98 not supporting SetFilePointer for device files.
This chapter shows how to open a connection to a Wdm1 device using device interfaces. The Wdm1Test user mode program puts the Wdm1 driver through its paces.
Chapter 7 shows how Wdm1 implements the read, write, and IOCTL calls. However, before I continue looking at the driver, the next chapter describes how to test and debug drivers. It explains how this book uses the DebugPrint software to see what is going on in a driver.
Listing 5.7 Wdm1Test.cpp
///////////////////////////////////////////////////////////////////////
//Copyright © 1998 Chris Cant, PHD Computer Consultants Ltd
//WDM Book for R&D Books, Miller Freeman Inc
//
//Wdm1Test example
///////////////////////////////////////////////////////////////////////
//WdmlTest.cpp:Win32 console application to exercise Wdm1 devices
///////////////////////////////////////////////////////////////////////
//mainProgram main line
//GetDeviceVialnterfaceOpen a handle via a device interface
///////////////////////////////////////////////////////////////////////
//Version history
//27-Apr-991.0.0CCcreation
///////////////////////////////////////////////////////////////////////
#include "windows.h"
#include "C:\98ddk\inc\win98\setupapi.h"// VC++ 5 one is out of date
#include "stdio.h"
#include "initguid.h"
#include "..\sys\GUIDs.h"
#include "winioctl.h"
#include "..\sys\Ioctl.h"
HANDLE GetDeviceViaInterface(GUID* pGuid, DWORD instance);
int main(int argc, char* argv[]) {
int TestNo = 1;
//////////////////////////////////////////////////////////////////////
// Open device
printf("\nTest %d\n",TestNo++);
HANDLE hWdm1 = GetDeviceViaInterface((LPGUID)&WDM1_GUID,0);
if (hWdml==NULL) {
printf("XXX Could not find open Wdm1 device\n");
return 1;
}
printf(" Opened OK\n");
//////////////////////////////////////////////////////////////////////
// Read first ULONG that's left in buffer
printf("\nTest %d\n",TestNo++);
DWORD TxdBytes;
ULONG Rvalue =0;
if (!ReadFile(hWdm1, &Rvalue, 4, &TxdBytes, NULL)) printf("XXX Could not read value %d\n", GetLastError());
else if(TxdBytes==4) printf(" Read successfully read stored value of 0x%X\n",Rvalue);
else printf("XXX Wrong number of bytes read: %d\n",TxdBytes);
//////////////////////////////////////////////////////////////////////
// Write 0x12345678
printf("\nTest M\n" ,TestNo++);
ULONG Wvalue = 0x12345678;
if (!WriteFile(hWdm1, &Wvalue, 4, &TxdBytes, NULL)) printf("XXX Could not write %X\n",Wvalue);
else if (TxdBytes==4) printf(" Write 0x12345678 succeeded\n");
else printf("XXX Wrong number of bytes written: %d\n",TxdBytes);
//////////////////////////////////////////////////////////////////////
// Set file pointer
printf("\nTest %d\n",TestNo++);
DWORD dwNewPtr = SetFilePointer(hWdrn1, 3, NULL, FILE_BEGIN);
if (dwNewPtr==0xFFFFFFFF) printf("XXX SetFilePointer failed %d\n", GetLastError());
else printf(" SetFilePointer worked\n");
//////////////////////////////////////////////////////////////////////
// Read
printf("\nTest %d\n",TestNo++);
Rvalue = 0;
if (!ReadFile(hWdm1, &Rvalue, 1, &TxdBytes, NULL)) printf("XXX Could not read value\n");
else if( TxdBytes==1) printf(" Read successfully read stored value of 0x%X\n",Rvalue);
else printf("XXX Wrong number of bytes read: %d\n",TxdBytes);
//////////////////////////////////////////////////////////////////////
// Write
printf("\nTest %d\n",TestNo++);
if (!WriteFile(hWdm1, &Wvalue, 4, &TxdBytes, NULL)) printf("XXX Could not write %X\n" ,Wvalue);
else if (TxdBytes==4) printf(" Write at new file pointer succeeded\n");
else printf("XXX Wrong number of bytes written: %d\n",TxdBytes);
//////////////////////////////////////////////////////////////////////
// Get buffer size
printf("\nTest %d\n",TestNo++);
ULONG BufferSize;
DWORD BytesReturned;
if (!DeviceIoControl(hWdm1, IOCTL_WDM1_GET_BUFFER_SIZE,
NULL, 0,// Input
&BufferSize, sizeof(ULONG),// Output
&BytesReturned, NULL)) printf("XXX Could not get buffer size\n");
else printf(" Buffer size is %i (%d bytes returned)\n",BufferSize,BytesReturned);
//////////////////////////////////////////////////////////////////////
// Get buffer size
printf("\nTest %d\n",TestNo++);
char* Buffer = new char[BufferSize+1];
if (!DeviceIoControl(hWdm1, IOCTL_WDM1_GET_BUFFER,
NULL, 0,// Input
Buffer, BufferSize,// Output
&BytesReturned, NULL)) printf("XXX Could not get buffer\n");
else printf(" First DWORD of buffer is %08X (%d bytes returned)\n",*((DWORD*)Buffer),Bytes Returned);
///////////////////////////////////////////////////////////////////////
// Get too big a buffer size
printf("\nTest %d\n",TestNo++);
if (!DeviceIoControl(hWdm1, IOCTL_WDM1_GET_BUFFER,
NULL, 0,// Input
Buffer, BufferSize+1,// Output
&BytesReturned, NULL)) printf(" Too big get buffer failed correctly %d\n",GetLastError());
else printf("XXX Too big get buffer unexpectedly succeeded\n");
///////////////////////////////////////////////////////////////////////
// Zero all buffer bytes
printf("\nTest %d\n",TestNo++);
if (!DeviceIoControl(hWdm1, IOCTL_WDM1_ZERO_BUFFER,
NULL, 0,// Input
NULL, 0,// Output
&BytesReturned, NULL)) printf("XXX Zero buffer failed %d\n" ,GetLastError());
else printf(" Zero buffer succeeded\n");
if (!DeviceIoControl(hWdm1, IOCTL_WDM1_GET_BUFFER,
NULL, 0,// Input
Buffer, BufferSize,// Output
&BytesReturned, NULL)) printf("XXX Could not get buffer\n");
else printf(" First DWORD of buffer is %08X (%d bytes returned)\ n",*((DWORD*)Buffer),BytesReturned);
///////////////////////////////////////////////////////////////////////
// Remove buffer
printf("\nTest %d\n",TestNo++);
if (!DeviceIoControl(hWdm1, IOCTL_WDM1_REMOVE_BUFFER,
NULL, 0,// Input
NULL, 0,// Output
&BytesReturned, NULL)) printf("XXX Remove buffer failed %d\n",GetLastError());
else printf(" Remove buffer succeeded\n");
if (!DeviceloControl(hWdm1, IOCTL_WDM1_GET_BUFFER_SIZE,
NULL, 0,// Input
&BufferSize, sizeof(ULONG),// Output
&BytesReturned, NULL)) printf("XXX Could not get buffer size\n");
else printf(" Buffer size is %i (%d bytes returned)\n",BufferSize,BytesReturned);
///////////////////////////////////////////////////////////////////////
// Unrecognised IOCTL
printf("\nTest %d\n",TestNo++);
if (!DeviceIoControl(hWdm1, IOCTL_WDM1_UNRECOGNISED,
NULL, 0,// Input
NULL, 0,1/ Output
&BytesReturned, NULL)) printf(" Unrecognised IOCTL correctly failed %d\n",GetLastError());
else printf("XXX Unrecognised IOCTL unexpectedly succeeded\n");
///////////////////////////////////////////////////////////////////////
// Write 0xabcdef01 to start of buffer
printf("\nTest %d\n",TestNo++);
dwNewPtr = SetFilePointer(hWdm1, 0, NULL, FILE_BEGIN);
if (dwNewPtr==0xFFFFFFFF) printf("XXX SetFilePointer failed %d\n",GetLastError());
else printf(" SetFilePointer worked\n");
Wvalue = 0xabcdef01;
if (!WriteFile(hWdm1, &Wvalue, 4, &TxdBytes, NULL)) printf("XXX Could not write %X\n", Wvalue);
else if( TxdBytes==4) printf(" Write 0xabcdef01 succeeded\n");
else printf("XXX Wrong number of bytes written: %d\n",TxdBytes);
/////////////////////////////////////////////////////////////////////////
// Close device
printf("\nTest %d\n",TestNo++);
if (!CloseHandle(hWdm1)) printf("XXX CloseHandle failed %d\n", GetLastError());
else printf(" CloseHandle worked\n");
/////////////////////////////////////////////////////////////////////////
delete Buffer;
printf("\nPress enter please");
char line[80];
gets(line);
return 0;
}
//////////////////////////////////////////////////////////////////////////
//GetDeviceViaInterface:Open a handle via a device interface
HANDLE GetDeviceViaInterface(GUID* pGuid, DWORD instance) {
// Get handle to relevant device information set
HDEVINFO info = SetupDiGetClassDevs(pGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
if (info==INVALID_HANDLE_VALUE) {
printf("No HDEVINFO available for this GUID\n");
return NULL;
}
// Get interface data for the requested instance
SP_INTERFACE_DEVICE_DATA ifdata;
ifdata.cbSize = sizeof(ifdata);
if (!SetupDiEnumDeviceInterfaces(info, NULL, pGuid, instance, &ifdata)) {
printf("No SP_INTERFACE_DEVICE_DATA available for this GUID instance\n");
SetupDiDestroyDeviceInfoList(info);
return NULL;
}
// Get size of symbolic link name
DWORD ReqLen;
SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &ReqLen, NULL);
PSP_INTERFACE_DEVICE_DETAIL_DATA ifDetail = (PSP_INTERFACE_DEVICE_DETAIL_DATA)(new char[ReqLen]);
if (ifDetail==NULL) {
SetupDiDestroyDeviceInfoList(info);
return NULL;
}
// Get symbolic link name
ifDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
if (!SetupDiGetDeviceInterfaceDetail(info, &ifdata, ifDetail, ReqLen, NULL, NULL)) {
SetupDiDestroyDeviceInfoList(info);
delete ifDetail;
return NULL;
}
printf("Symbolic link is %s\n", ifDetail–>DevicePath);
// Open file
HANDLE rv = CreateFile(ifDetail->DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
delete ifDetail;
SetupDiDestroyDeviceInfoList(info);
return rv;
}
///////////////////////////////////////////////////////////////////////
If you call IoAttachDeviceToDeviceStack at a higher IRQL, your driver may crash.
If you are only compiling in W2000, you can include initguid.h instead.