Chat
Search
Ithy Logo

PCIe Configuration Space Reader Using WDM

A Comprehensive Guide and Sample Code for Windows Driver Development

computer hardware pci connector

Essential Highlights

  • Accessing PCI Space: Utilize IRP_MN_READ_CONFIG and related interfaces for safe configuration access.
  • Driver Structure: Implement a filter driver using WDM practices with proper event synchronization and error handling.
  • Extended Support: Adapt code for devices with SR-IOV and extended configuration space in modern Windows.

Introduction to PCIe Configuration Spaces

PCI Express (PCIe) configuration space is a memory region associated with every PCIe device. It holds important information and configuration registers for the device, including vendor IDs, device IDs, revision numbers, and extended capability registers. Accessing this space is crucial for device initialization, configuration, and debugging purposes.

In Windows, particularly with the Windows Driver Model (WDM), safe and controlled access to the PCIe configuration space is achieved using specific IRPs (I/O Request Packets) and interfaces provided by the operating system. This guide provides an in-depth explanation of how to build a PCIe configuration space reader, including code samples and detailed steps to handle configuration space access effectively.


Understanding WDM and PCI Access

PCI Configuration Space and Its Structure

Each PCIe device has a standardized configuration space, typically 256 bytes for traditional devices and up to 4096 bytes for devices supporting extended capabilities. This region includes:

  • Basic configuration registers such as vendor ID, device ID, and command/status registers.
  • Base Address Registers (BARs) that determine memory and I/O address space allocations.
  • Extended capabilities for advanced features like SR-IOV, PCIe power management, and more.

Since the configuration space is memory-mapped, reading it without proper synchronization and using correct privilege levels may cause system instability. Windows provides several APIs and IRPs such as IRP_MN_READ_CONFIG and IRP_MN_WRITE_CONFIG to help drivers manage these operations safely.

Driver Model and APIs

Key IRP Requests

To read the PCIe configuration space, our driver needs to send an IRP with the minor function IRP_MN_READ_CONFIG. The underlying PCI bus driver processes this IRP and retrieves the appropriate configuration information.

In more modern Windows environments, the BUS_INTERFACE_STANDARD interface is used. This interface offers functions like GetBusData and SetBusData for accessing configuration registers from within a kernel-mode context.

Development Considerations

When developing a PCI config space reader, consider the following:

  • Always operate at PASSIVE_LEVEL to ensure safe access without causing deadlocks.
  • Ensure proper synchronization with events when issuing IRPs to the driver stack.
  • Handle errors gracefully, and always check for valid pointers and resource availability.
  • Adhere to security guidelines: Direct PCI access is restricted in user mode and advanced OS versions have additional security measures such as virtualization-based security.
  • For devices supporting extended configuration space, verify BIOS and hardware support.

Step-by-Step Guide: Building the PCIe Config Space Reader

1. Driver Setup and Initialization

Start by writing the DriverEntry function which serves as the entry point for all WDM drivers. Initialize necessary structures, assign the unload routine, and prepare communication with the PCI bus driver.


/* Include necessary headers */
#include <ntddk.h>
#include <wdm.h>
#include <pci.h>

/* Driver entry point function */
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    // Set the unload routine for cleanup during driver unload
    DriverObject->DriverUnload = UnloadDriver;

    // Additional initialization can be done here
    // For example, initializing global data structures or custom device extensions

    return STATUS_SUCCESS;
}

/* Unload routine for cleaning up driver resources */
VOID UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    // Perform cleanup necessary when the driver is unloaded.
}
  

2. Accessing PCI Configuration Space

Using IRP_MN_READ_CONFIG

The key to reading the PCI configuration space is the IRP with the IRP_MN_READ_CONFIG minor function. In this routine, the driver constructs an IRP, sets up the stack location, and then calls the device driver via IoCallDriver. The system waits for the IRP completion using synchronization objects.


/* Function to read PCIe configuration space using IRP */
NTSTATUS ReadPciConfigSpace(
    HANDLE DeviceHandle,   // Handle representing the PCI device
    ULONG Offset,          // Offset into the configuration space
    PVOID Buffer,          // Buffer to store the configuration data
    ULONG Length           // Number of bytes to read
)
{
    IO_STATUS_BLOCK iosb;
    NTSTATUS status;
    KEVENT event;

    // Initialize an event for synchronization purposes.
    KeInitializeEvent(&event, NotificationEvent, FALSE);

    // Allocate the IRP based on the device's stack size
    PIRP irp = IoAllocateIrp(DeviceHandle->StackSize, FALSE);
    if (!irp)
    {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    // Mark the IRP as non-cached and set a default status
    irp->Flags = IRP_NOCACHE;
    irp->IoStatus.Status = STATUS_NOT_IMPLEMENTED;
    irp->UserEvent = &event;

    // Prepare the IRP stack location for configuration read
    PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(irp);
    stack->MajorFunction = IRP_MJ_PNP;
    stack->MinorFunction = IRP_MN_READ_CONFIG;
    stack->Parameters.ReadWriteConfig.Address = Offset;
    stack->Parameters.ReadWriteConfig.Length = Length;
    stack->Parameters.ReadWriteConfig.Data = Buffer;

    // Send the configured IRP down to the PCI bus driver
    status = IoCallDriver(DeviceHandle, irp);

    // Wait until the IRP is completed
    KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);

    // Return status of the read operation
    return status;
}
  

3. Basic Driver Code Structure and Sample Implementation

Below is an integrated sample code that incorporates both the DriverEntry and the read function. This template should serve as a foundation for further customization depending on the PCI device specifics and additional functionalities required.


/* Complete Sample Code for a Basic PCIe Config Space Reader */
#include <ntddk.h>
#include <wdm.h>
#include <pci.h>

/* Function prototypes */
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath);
VOID UnloadDriver(PDRIVER_OBJECT DriverObject);
NTSTATUS ReadPciConfigSpace(
    HANDLE DeviceHandle,
    ULONG Offset,
    PVOID Buffer,
    ULONG Length
);

/* Main driver entry point */
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    // Set the unload routine
    DriverObject->DriverUnload = UnloadDriver;

    // Additional initialization code here.
    // This may involve setting up device objects, symbolic links etc.
    // This minimal implementation returns success.
    return STATUS_SUCCESS;
}

/* Driver unload function for cleanup */
VOID UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    // Release any allocated resources if necessary.
}

/* 
 * ReadPciConfigSpace: Reads PCIe configuration space.
 * Parameters:
 *  DeviceHandle - Handle to the PCI device.
 *  Offset - Offset into the configuration space from which to read.
 *  Buffer - Pointer to the output buffer for configuration data.
 *  Length - Number of bytes to read.
 */
NTSTATUS ReadPciConfigSpace(
    HANDLE DeviceHandle,
    ULONG Offset,
    PVOID Buffer,
    ULONG Length
)
{
    IO_STATUS_BLOCK iosb;
    NTSTATUS status;
    KEVENT event;

    // Initialize event for synchronization.
    KeInitializeEvent(&event, NotificationEvent, FALSE);

    // Allocate IRP based on device’s stack size.
    PIRP irp = IoAllocateIrp(DeviceHandle->StackSize, FALSE);
    if (irp == NULL)
    {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    // Set up IRP parameters: mark as non-cached and use event for IRP completion.
    irp->Flags = IRP_NOCACHE;
    irp->IoStatus.Status = STATUS_NOT_IMPLEMENTED;
    irp->UserEvent = &event;

    // Get the next IRP stack location.
    PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(irp);
    stack->MajorFunction = IRP_MJ_PNP;
    stack->MinorFunction = IRP_MN_READ_CONFIG;
    stack->Parameters.ReadWriteConfig.Address = Offset;
    stack->Parameters.ReadWriteConfig.Length = Length;
    stack->Parameters.ReadWriteConfig.Data = Buffer;

    // Send the IRP to the appropriate device driver.
    status = IoCallDriver(DeviceHandle, irp);

    // Wait until the IRP is finished.
    KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);

    // Return the result of the operation.
    return status;
}
  

Advanced Considerations and Enhancements

Handling Extended PCIe Configuration Space

Some PCIe devices support configuration spaces larger than 256 bytes. To accommodate this:

  • Check for device support using ACPI BIOS information (for example, PNP IDs such as PNP0A08 or PNP0A03).
  • For devices supporting Single Root I/O Virtualization (SR-IOV), explore functions like GetVirtualFunctionData to manage configuration settings for virtual functions (VFs).
  • Ensure that the code is updated for extended memory access patterns, especially when larger configuration spaces are involved.

Adjust the IRP parameters accordingly if you intend to read beyond the first 256 bytes.

Synchronization and Error Handling

Using Events for Synchronization

Kernel-mode driver operations must ensure that asynchronous calls, like sending IRPs, do not lead to race conditions. In our implementation, a kernel event is used to wait for IRP completion. By using the KeInitializeEvent and KeWaitForSingleObject APIs, we ensure that the read operation is complete before proceeding.

Robust Error Checking

Every call to an API that accesses hardware must validate success or failure codes. For example, check the return status from IoAllocateIrp and IoCallDriver. Also, when dealing with hardware registers, make sure to validate that the offset and length fall within valid ranges to prevent buffer overflows or corrupt data.

Integrating with the PCI Subsystem

Modern Windows environments use a layered driver model. The PCI bus filter driver intercepts calls between the device driver and the PCI bus driver. By developing your code as a miniport or filter, you ensure that you have proper access to the configuration space while still observing the system’s security protocols.

Sample Table of Configuration Parameters

The table below outlines common configuration registers along with their respective offsets and descriptions:

Register Name Offset (Hex) Description
Vendor ID 0x00 Identifies the vendor of the device
Device ID 0x02 Specific device identification
Command Register 0x04 Controls device settings and command execution
Status Register 0x06 Reports device status including error information
Base Address Registers (BARs) 0x10-0x24 Define memory or I/O space requirements

Best Practices and Security

Adhering to System Security

Modern versions of Windows implement strict security protocols for device access. Direct PCI configuration access from user-mode applications is restricted to prevent unauthorized hardware manipulation. Therefore, such operations must be performed in kernel mode, adhering to the Windows Driver Model (WDM) guidelines.

Virtualization-based security (VBS) further restricts modifications to device configuration registers, hence ensure that your driver properly checks for privilege levels and system compatibility.

Testing and Debugging

Before deploying your PCI config space reader in production environments, thoroughly test your code. Use debugging tools such as Kernel Debugger (KD) and WinDbg, as well as utilities that can simulate PCI environments. Often, hardware emulators or dedicated diagnostic tools are used to verify that your driver functions correctly.

Establish reliable logging within your driver to capture IRP completion statuses and any errors encountered during the configuration space access.


Additional Code Enhancements

Extending to Write Operations

Although our primary focus is reading the PCIe configuration space, it is possible to extend the solution to write operations using IRP_MN_WRITE_CONFIG. The procedures are similar, though the write operation requires great caution because erroneous writes can disrupt system stability.

When implementing write support, follow similar steps as in the read function but substitute the minor function with IRP_MN_WRITE_CONFIG and ensure that the contents of the output buffer are correct before writing.

Future Enhancements

As hardware evolves, you might need to integrate support for more advanced features such as SR-IOV. In such cases, consider:

  • Using specialized APIs like GetVirtualFunctionData for reading configuration space of virtual functions.
  • Implementing robust error recovery and rollback mechanisms in writing configuration registers.
  • Adapting to changes in the Windows PCI subsystem, especially with the release of new Windows versions.

References

Recommended Further Exploration


Last updated March 3, 2025
Ask Ithy AI
Export Article
Delete Article