Home‎ > ‎Articles and Publications‎ > ‎Articles (ENG)‎ > ‎

Enabling direct I/O ports access in user space


by Antonino Calderone - published on Computer Programming - n. 153 - Gennaio 2006 
 

This article describes the direct I/O access techniques in Linux and Windows user space applications. A Windows kernel driver which uses undocumented internal API is also described.


Intel x86 processors in addition to memory mapped devices support also so called I/O ports mapped devices via a 'privileged' set of machine instructions. Such instructions are checked by system to guarantee processes and kernel space isolation where only (Linux and Windows) Kernel privileged code is allowed to address the I/O directly.

Only two of four privilege levels (rings) of x86 processors are typically used by Linux and Windows: ring 0 for kernel space and ring 3 for user space. While in kernel space any privileged instruction can be executed, in user space this can only be allowed through a specific interface provided by operating system.

Even if user space applications normally don't access directly the I/O a specific class of applications can violate such rule. We are talking about User Mode Drivers (UMD).

An UMD is a simple user space application since it can be built, executed and debugged as any other user space application while a Kernel mode driver (KMD) requires specific tools and a very specific knowledge to be designed, implemented, deployed and debugged.

Even taking into account that an UMD might represent a violation of modern operating systems architecture which splits user space and kernel space domains, an UMD might still represent a pragmatic temporary solution for experimenting low level stuff without dealing with KMD complexity.

Often a better way is combining UMD and KMD for providing a solution where the KMD provides access to I/O and the UMD implements stuff 'too' complex to be implemented easily in the KMD. Sometimes this is not possible because the performances are compromised by the Kernel / User context switch overhead.

And finally, although we cannot replace a KMD anytime we have to deal with interrupts and other bunch of low level stuff, knowing how to access I/O from user space can still be useful in many other cases.

I/O permission level and I/O permission bit map

I processori x86 usano un algoritmo per validare l'accesso a una porta di I/O basato su due distinti meccanismi di "permission check" (è possibile trovarne una trattazione dettagliata in [1]):

x86 processors use an algorithm to validate a port I/O access based on two permission checks ([1]):

  • Checking the I/O Privilege Level (IOPL) of EFLAGS register
  • Checking I/O permission bit map (IOPM) of a process task state segment (TSS)

For the memory mapped devices other MMU related specific mechanisms are provided (but they are out of scope, see [2] for more details on this).

EFLAGS register is the 32 bit status register of processor. EFLAGS is stored into TSS when a task is suspended and it is replaced with the one contained in the new executing task's TSS. EFLAGS IOPL field controls the I/O ports address space restricting machine instruction access to such ports. Instruction as IN, INS, OUT, OUTS can be executed if the Current Privilege Level (CPL) of executing process is less than or equal to IOPL. Such instructions (plus STL and CLI instructions), are defined I/O sensitive. A general protection exception will be raised any time a non privileged process try to use them.

Because each process has a specific copy of EFLAGS, different process might have different privileges. User space processes (CPL=3) cannot modify directly the IOPL, while have to ask operating system to do that. Linux for example provides a syscall named iopl. A root process with CAP_SYS_RAWIO capability can modify the IOPL field getting access to the whole I/O address space.

You can find more information about iopl syscall (sys_iopl) and ioperm syscall (which modifies the I/O permission bitmap which will be soon discussed) looking Linux kernel source code.

I/O permission bitmap is part of TSS. Base address and location are both part of TSS.

By modifying the permission bitmap is possible to enable the I/O port access for any process even less privileged or virtual-8086 processes where CPL>=IOPL. The bitmap can cover the entire I/O address space or a subset of it.

Each bit corresponds to an I/O address space byte. For example 1-byte size port at address 0x31 corresponds to bit 1 of byte 7 of the bitmap. A process is allowed to access a specific byte if the related bit on the bitmap is 0.

Linux ioperm syscall can modify the first 0x3FF ports while the only way to get access to the remaining ports is using iopl syscall.

Fig 1 - I/O permission bit map and its TSS base-address.

Ke386IoSetAccessProcess and Ke386SetIoAccessMap

Windows does not support syscalls like ioperm or iopl but we can try to implement a KMD which will provide similar features as shown in the source code at List-1 that will discuss better in the next paragraph.

Although such driver is quite simple it is peculiar because of two undocumented Kernel API Ke386IoSetAccessProcess e Ke386SetIoAccessMap. Non official documentation about such APIs is the following:

  • void Ke386IoSetAccessProcess (PEPROCESS, int);

This function ask the Kernel to enable access for the current process to the IOPM bitmap. Second argument enables (1) or disables (0) such access.

  • void Ke386SetIoAccessMap (int, IOPM *);

Replaces the current process IOPM bitmap (first parameter must be 1).

  • void Ke386QueryIoAccessMap (int, IOPM *);

Returns the current process IOPM. First argument must be 1.

Described functions can be combined to update the I/O permission bitmap of calling process, allowing it to access to the whole I/O address space, as showing in the following fragment of code:

char * pIOPM = NULL;
//...
pIOPM = MmAllocateNonCachedMemory(65536 / 8);
RtlZeroMemory(pIOPM, 65536 / 8);
Ke386IoSetAccessProcess(IoGetCurrentProcess(), 1);
Ke386SetIoAccessMap(1, pIOPM);

In the above example, a new 64 Kbit bitmap is allocated and its content is set to 0. Such bitmap is used to replace the existing one. More in detail Ke386IoSetAccessMap replaces the bitmap, working in the context selected by the API Ke386IoSetAccessProces.

The Kernel Mode Driver

KMD role is to provide the O/S with the access to a specific device.

From a user space process point of view a KMD can be handled as special file and handled by syscalls like CreateFile, ReadFile or DeviceIoControl.

From an implementation point of view it is a set of functions registered into and called by I/O Manager during the I/O operations on the controlled device.


A generic Windows KMD implementation includes:

  • DriverEntry function called by I/O Manager as soon as the driver is loaded.
  • Dispatch entry points: functions called on I/O requests which process the I/O Request Packet (IRPs).
  • Interrupt Service Routines (ISRs): which handle the device Interrupt requests (IRQs) 
  • Deferred Procedure Calls (DPCs): special routines typically called from ISR to complete a service routine task out of ISR execution context.

Driver implementation shown in the List-1 does not use any ISR or DPC, while it just implements I/O control command used to get access the I/O permission bitmap.

Our driver in fact exports just two specific features: enabling and disabling the direct I/O ports access, implemented via a IOCTL request. Same result can be obtained in Linux calling the iopl syscall (ioperm can be used just for the first 0x3ff ports for historical reasons).


Driver entry point is implemented the is the following function:

NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath);

Such function accepts a DRIVER_OBJECT structure pointer which is a unique object is built by Windows upon the driver activation. RegistryPath parameter is a unicode string which represent the registry path name used for configuring the driver.

The returned value is processed by I/O Manager. In case it should be different than STATUS_SUCCESS, the driver will be terminated and removed from memory.

This function creates a device object and the related name and then registers the dispatch entry points: in our driver they are implemented in the function ioperm_create, ioperm_close and ioperm_devcntrl.

Such functions process the IRPs built by I/O Manager as result of a I/O system request.

To build the driver binary we have used Microsoft Driver Development Kit (DDK).

DDK provides a tools and libraries to create the driver binary which typically has .sys file extension which is installed in a specific system directory (system32\drivers)

Installation process includes the Windows Register registration which can be done via using a specific .REG as shown in the following example:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ioperm]
"Type"=dword:00000001
"ErrorControl"=dword:00000001
"Start"=dword:00000002
"DisplayName"="ioperm"
"ImagePath"="System32\\DRIVERS\\ioperm.sys"

Once installed the driver configuration will be visible via the Device Manager (as shown in Fig.2).

We have implemented a simple user space program for Windows and Linux (List-2) which probes the parallel port. Such device is typically mapped at port addresses 0x278, 0x378, 0x3BC. Data register (offset 0) is a 8 bit data latch.

Knowing that a program can detect the device writing and reading back a byte using a specific pattern (skipping pull-down or pull-up values like 0 or 0xFF).

The program can be compiled in Visual Studio or using GNU C++.

Fig.2 - Windows Device Manager

Conclusion

Looking back the Linux versions we discovered that ioperm and iopl syscalls have been added since early versions.

We are not surprised of such thing, and we are not surprised that Microsoft has decided not to do that.

References

[1] Intel - "Intel Architecture Software Developer’s Manual - Volume 1:Basic Architecture", Intel, 1999

[2] Intel - "Intel Architecture Software Developer’s Manual, Volume 3" Intel, 1999

[3] P.G. Viscarola, W.A. Mason - "Windows NT - Device Driver Development", MTP, 1999

[5] http://www.ddj.com/articles/1996/9605/

[6] http://www.beyondlogic.org/porttalk/porttalk.htm

[7] http://www.microsoft.com/whdc/devtools/ddk/default.mspx

[8] http://lxr.linux.no/linux-bk+v2.6.11.5/arch/i386/kernel/ioport.c

List 1

#include <ntddk.h>
#include "ioperm_devcntl.h"

#define IO_BITMAP_BITS  65536
#define IO_BITMAP_BYTES (IO_BITMAP_BITS/8)
#define IOPM_BITMAP_SIZE IO_BITMAP_BYTES
#define PRIVATE static

PRIVATE WCHAR DEVICE_NAME[] = L"\\Device\\ioperm";
PRIVATE WCHAR DEVICE_DOS_NAME[] = L"\\DosDevices\\ioperm";
PRIVATE UNICODE_STRING device_dos_name, device_name;

typedef unsigned char* iopm_bitmap_t;
PRIVATE iopm_bitmap_t iopm_bitmap_copy_ptr = 0;

void Ke386IoSetAccessProcess(PEPROCESS, int);
void Ke386SetIoAccessMap(int, iopm_bitmap_t);

NTSTATUS ioperm_devcntrl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp );

NTSTATUS ioperm_create(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
  Irp->IoStatus.Status = STATUS_SUCCESS;
  Irp->IoStatus.Information=0;

  IoCompleteRequest(Irp, IO_NO_INCREMENT);

  return(STATUS_SUCCESS);
}

NTSTATUS ioperm_close(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
  Irp->IoStatus.Status = STATUS_SUCCESS;
  Irp->IoStatus.Information=0;

  IoCompleteRequest(Irp, IO_NO_INCREMENT);

  return(STATUS_SUCCESS);
}

VOID ioperm_unload(IN PDRIVER_OBJECT DriverObject) {
  if (iopm_bitmap_copy_ptr) {
    MmFreeNonCachedMemory(iopm_bitmap_copy_ptr, IOPM_BITMAP_SIZE);
  }

  IoDeleteSymbolicLink (&device_dos_name);
  IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) {
  PDEVICE_OBJECT deviceObject = 0;
  NTSTATUS status = STATUS_SUCCESS;

  iopm_bitmap_copy_ptr = MmAllocateNonCachedMemory(IOPM_BITMAP_SIZE);

  if (! iopm_bitmap_copy_ptr)  {
    status = STATUS_INSUFFICIENT_RESOURCES;
    goto error_handler;
  }

  RtlInitUnicodeString(&device_name, DEVICE_NAME );
  RtlInitUnicodeString(&device_dos_name, DEVICE_DOS_NAME );

  status = IoCreateDevice(DriverObject, 0,
                          &device_name,
                          FILE_DEVICE_UNKNOWN, 0, FALSE,
                          &deviceObject);

  if (! NT_SUCCESS(status) ) {
    goto error_handler;
  }

  if (! NT_SUCCESS(status = IoCreateSymbolicLink (&device_dos_name, &device_name))) {
    goto error_handler;
  }

  DriverObject->MajorFunction[IRP_MJ_CREATE] = ioperm_create;
  DriverObject->MajorFunction[IRP_MJ_CLOSE] = ioperm_close;
  DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ioperm_devcntrl;
  DriverObject->DriverUnload = ioperm_unload;

error_handler:
  return status;
}

NTSTATUS
ioperm_devcntrl( IN PDEVICE_OBJECT DeviceObject, IN PIRP pIrp ) {
  NTSTATUS status = STATUS_SUCCESS;
  pIrp->IoStatus.Information = 0;

  switch (IoGetCurrentIrpStackLocation(pIrp)->Parameters.DeviceIoControl.IoControlCode) {
    case IOCTL_IOPM_ENABLE_IO:
      RtlZeroMemory(iopm_bitmap_copy_ptr, IOPM_BITMAP_SIZE);
      break;

    case IOCTL_IOPM_DISABLE_IO:
      RtlFillMemory(iopm_bitmap_copy_ptr, IOPM_BITMAP_SIZE, -1);
      break;

    default:
      status = STATUS_UNSUCCESSFUL;
      break;
  }

  if ( NT_SUCCESS( status ) ) {
    PEPROCESS current_process = IoGetCurrentProcess();

    if (current_process) {
      Ke386SetIoAccessMap(1, iopm_bitmap_copy_ptr);
      Ke386IoSetAccessProcess(current_process, 1);
    }
  }

  pIrp->IoStatus.Status = status;
  IoCompleteRequest( pIrp, IO_NO_INCREMENT );

  return status;
}


List 2

#if defined (_MSC_VER)
#define PER_WIN32
# include <windows.h>
# include <winioctl.h>
# include "ioperm_devcntl.h"
# include <conio.h>
# include <stdio.h>
#elif defined (__GNUC__) 
# include <sys/io.h>
# define PER_LINUX
# include <stdio.h>
# include <stdlib.h>
#define _inp inb_p 
#define _outp(_PORT_,_VALUE_) outb_p(_VALUE_,_PORT_)
#else
# error Please use GNU C++ for Linux or MS Visual C++ compiler for Windows
#endif

// Usual parallel port addresses
unsigned short port_addr[] = {0x378, 0x278, 0x3BC};

#define MAGIC_NUMBER 0xAC

int main()
{
#ifdef PER_WIN32
  DWORD dwBytesReturned = 0;
  #define       MAX_ERR_STR 256
  char szError[MAX_ERR_STR] = {0};

  //CreateFile opens the device driver and returns a handle that will be used 
  //to access the device driver
  HANDLE h = CreateFile("\\\\.\\ioperm",                      // file name
                        GENERIC_READ | GENERIC_WRITE,         // access mode
                        FILE_SHARE_READ | FILE_SHARE_WRITE,   // share mode
                        0,                                    // Security Attributes
                        OPEN_EXISTING,                        // how to open
                        0,                                    // file attributes: normal
                        0);                                   // handle to template file: NONE


  //Check for open errors
  if (h == INVALID_HANDLE_VALUE) {
    sprintf(szError, "Error %ld", GetLastError());
    MessageBox(0, "IoPerm Error", szError, MB_OK|MB_ICONERROR);
    return -1;
  }

  // The DeviceIoControl function sends the IOCTL_IOPM_ENABLE_IO
  // control code directly to a ioperm device driver, 
  // causing the device to perform the ioperm operation enabling or
  // disabling direct I/O access for calling process
  if (! DeviceIoControl(h,                    // handle to device
                        IOCTL_IOPM_ENABLE_IO, // operation
                        NULL,                 // no input data buffer
                        0,                    // size of input data buffer is 0
                        NULL,                 // no output data buffer
                        0,                    // size of output data buffer is 0
                        &dwBytesReturned,     // byte count
                        NULL))                // overlapped information
  {
    sprintf(szError, "Error %ld", GetLastError());
    MessageBox(0, "IoPerm Error", szError, MB_OK|MB_ICONERROR);
    CloseHandle(h);
    return -1;
  }

  //We need to re-schedule 
  Sleep(1);

#elif defined(PER_LINUX)
  if (iopl(3)) {
    perror("iopl");
    exit(-1);
  }
#endif


  // Ok, device opened 
  for (unsigned int i=0;
       i<sizeof(port_addr)/sizeof(port_addr[0]);
       ++i)
  {
    _outp(port_addr[i], MAGIC_NUMBER);

    if ( MAGIC_NUMBER == _inp(port_addr[i]) ) {
      printf("Parallel port found at address 0x%X ", port_addr[i]);
      printf("(Status Info 0x%02X - Control Info 0x%02X)\n",
             _inp(port_addr[i] + 1),
             _inp(port_addr[i] + 2));
    }
  }

#ifdef PER_WIN32
  CloseHandle(h);
#endif

  return 0;
}
Comments