In the previous article, we introduced how to “write” a Windows PCI device driver from scratch. The term “write” is in quotes because we didn’t actually write any code; the driver code was generated using the KMDF template in Visual Studio. In this article, we will introduce the generated driver code.

The first function to look at is DriverEntry in Driver.c, which is the entry point for Windows drivers, analogous to the main function in an application.

The function DriverEntry has two parameters. The first is DriverObject, which is a pointer to a DRIVER_OBJECT structure representing the Windows driver object; each driver corresponds to one DRIVER_OBJECT. The second parameter is a Unicode string that indicates the path of this driver in the Windows registry. If we want to add registry entries for this driver (to pass parameters and control driver behavior), we need to add them under this path.

The first function called in DriverEntry is WPP_INIT_TRACING, which is actually a macro responsible for initializing WPP tracing (Windows software trace preprocessor), a tracing method provided by Windows that is widely used in Windows drivers. The TraceEvents function calls we see in the driver code (which is also a macro) utilize the WPP trace mechanism to log. The important parts of the DriverEntry function are highlighted in red, with the core content being the call to the WDF API function WdfDriverCreate to create the driver object. When creating the driver object, two callback functions need to be specified: one is PciTestDriverEvtDriverContextCleanup, which is called by the Windows kernel when the driver is unloaded, responsible for cleaning up resources allocated in DriverEntry, such as cleaning up WPP (WPP_CLEANUP).

The other callback function passed when creating the driver object is PciTestDriverEvtDeviceAdd, which is also called by the Windows kernel to create a device object (DEVICE_OBJECT) for this driver. A driver can match multiple devices, so one DRIVER_OBJECT can correspond to multiple DEVICE_OBJECTs, with each DEVICE_OBJECT representing a device that matches this driver. Each time this driver matches a device, the function PciTestDriverEvtDeviceAdd is called to create a corresponding DEVICE_OBJECT. This function has two parameters: the first parameter, Driver, is of type WDFDRIVER, which is a handle provided by WDF pointing to this driver object; this parameter is currently unused. The second parameter is a structure allocated by WDF used to initialize the device object, which will be passed to the function PciTestDriverCreateDevice, which actually creates the device object.

Next, let’s look at the implementation of the function PciTestDriverCreateDevice, which is located in Device.c. The current logic seems straightforward. It mainly calls the WDF API function WdfDeviceCreate to create a device object represented by WDFDEVICE. Like WDFDRIVER, WDFDEVICE is also a WDF handle pointing to the created device object (DEVICE_OBJECT). When creating the device object, a DEVICE_CONTEXT must be specified, which is a custom structure used by the driver to maintain device context information. The definition of this structure varies among different drivers, but currently, it is very simple, containing only a placeholder member called PrivateDeviceData. We will later expand this structure to include the members we need to maintain.

In addition to creating the device object, the function PciTestDriverCreateDevice also creates a device interface, allowing applications to enumerate this device using the device interface (refer to Using Windows SetupAPI to Enumerate Devices and Using Windows Cfgmgr32 API to Enumerate Devices), and communicate with this device. GUID_DEVINTERFACE_PciTestDriver is a GUID that applications can use to enumerate this device. After creating the device interface, the next function called is PciTestDriverQueueInitialize, which is responsible for initializing the queue object for communication between this device and applications or other devices. This device receives all IO requests (IoRead, IoWrite, and IoControl) sent to it through this queue object.
The function PciTestDriverQueueInitialize is defined in Queue.c, and like the previous routines for creating driver and device objects, it calls the WDF API function WdfIoQueueCreate to create the queue object. When creating, a queue config structure must be specified, which contains a set of callbacks for the driver to implement the IoRead, IoWrite, IoDeviceControl, and IoStop operations. In the generated code, only the IoDeviceControl and IoStop functions are implemented.

The function PciTestDriverEvtIoDeviceControl is mainly used to handle various IoControl operations sent to this device by applications. The specific IoControl operations are defined by the driver developer based on the needs of different devices. The function PciTestDriverEvtIoStop is responsible for completing, canceling, or deferring the processing of any outstanding IO requests when the device is about to exit the D0 state. In the auto-generated code, these two functions are essentially empty, so we won’t go into detail here; we will introduce them in detail when we expand their functionality later.
This concludes the overview of the KMDF driver code generated by Visual Studio. These codes alone are far from sufficient, and we will gradually enhance this driver by adding more features.