Guest Article: Kernel Mode DLLs
By Charles Saperstein of Hauppauge Computer Works
Ó 1997 OSR Open Systems Resources, Inc.
Recently, I was working on an NT kernel driver project where it would have been advantageous to partition the work into parts like dynamic link libraries (DLLs). Fortunately, this can be done, but has not been documented until now. The sample console application in this article sends an IOCTL to a kernel driver called master.sys. The kernel driver calls a function in a kernel mode DLL called slave.sys. The function processes the IRP and puts a value into the I/O buffer. The application tests the returned value to see if it is correct.
Building the Library
The DLL has to be built before the driver because the driver is linked to the import library of the DLL. If you look in makefile.def, you will find a TARGETTYPE called EXPORT_DRIVER. This is the key to making a kernel mode DLL and should be included in your sources file (See Figure 1). The sources file is used by the build utility to make the DLL. The sources file will put the libraries in the DDK tree. For our purposes here, the DLL was built in a directory called slave and the driver in directory called master. (Note: Be sure to include the path to the driver in the sources file if using a common header file to define the IOCTL as I have below).
# SOURCES FILE FOR SLAVE.SYS
TARGETNAME=SLAVE
TARGETPATH=$(BASEDIR)\lib
TARGETTYPE=EXPORT_DRIVER
INCLUDES=$(BASEDIR)\inc;..\MASTER
SOURCES=SLAVE.C
C_DEFINES=-DUNICODE -DSTRICT
The next thing you have to do is to create a DEF file (See Figure 2). Do not put the names of your functions under the EXPORTS section.
;DEF FILE FOR SLAVE.SYS
NAME SLAVE.SYS
DESCRIPTION 'SLAVE.SYS'
EXPORTSFigure 2 slave.def
Finally, you must create a function. The DLL needs a DriverEntry( ) function which can be as simple as just returning STATUS_SUCCESS. (Note: During my testing, I used a DriverEntry( ) function that created a device object and an UnLoad( ) function so that I could load and unload the driver using the Devices applet in Control Panel. All functions that are to be called from the driver have to be declared _declspec( dllexport )).
#include "ntddk.h"
#include "MYIOCTL.H"
NTSTATUS
DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )
{
return STATUS_SUCCESS;
}
NTSTATUS
_declspec( dllexport )DoSomethingMeaningless(IN PIRP pIrp,
IN PIO_STACK_LOCATION
pIrpStack)
{
USHORT *pw;
/* check to see if this is our ioctl. */
if (pIrpStack->Parameters.DeviceIoControl.IoControlCode != IOCTL_CALL_KERNEL_MODE_DLL)
return STATUS_INVALID_PARAMETER;
/* check to see if output buffer size is big enough. */
if
(pIrpStack->Parameters.DeviceIoControl.OutputBufferLength < sizeof(USHORT)) {
pIrp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL;
pIrp->IoStatus.Information = 0;
return STATUS_BUFFER_TOO_SMALL;
}
/* return a value back to the user. */
pIrp->IoStatus.Information = sizeof( USHORT );
pw = (USHORT *)pIrp->AssociatedIrp.SystemBuffer;
*pw = 0XAA55;
pIrp->IoStatus.Status = STATUS_SUCCESS;
return STATUS_SUCCESS;
}
Figure 3 slave.c
Building the Driver
In the sources file, the driver has to linked with the DLL import library (See Figure 4).
# SOURCES FILE FOR MASTER.SYS
TARGETNAME=MASTER
TARGETPATH=$(BASEDIR)\lib
TARGETTYPE=DRIVER
TARGETLIBS=$(BASEDIR)\lib\*\$(DDKBUILDENV)\slave.libINCLUDES=$(BASEDIR)\inc;.
SOURCES=MASTER.C
C_DEFINES=-DUNICODE -DSTRICT
Figure 4 sources
I put my custom IOCTLs in a header file so that it could be shared between projects (See Figure 5).
//CTL_CODE macro defined in devioctl.h in DDK\inc.
//custom device types (non Microsoft) in range 32768 to 65535
//custom user defined function codes in range 2048 to 4095
#define FILE_DEVICE_FOO 65534
#define IOCTL_CALL_KERNEL_MODE_DLL CTL_CODE(FILE_DEVICE_FOO, 2049, METHOD_BUFFERED, FILE_ANY_ACCESS)
Figure 5 myioctl.h
Figure 6 contains the code for the kernel driver that processes only one custom IOCTL and calls a function in the DLL. For my sample, I passed the function a pointer to the IRP and to the current stack location of the IRP. The function in the DLL then processes the IRP. For testing purposes, I had an UnLoad( ) function and a Dispatch function to handle IRP_MJ_CLOSE and IRP_MJ_CREATE.
#include "ntddk.h"
#include "myioctl.h"
typedef struct _FOO_DEVICE_EXTENSION {
ULONG Information;
} FOO_DEVICE_EXTENSION, *PFOO_DEVICE_EXTENSION;
// Function declarations
NTSTATUS
_declspec( dllimport )DoSomethingMeaningless(IN PIRP pIrp, IN PIO_STACK_LOCATION pIrpStack
);
NTSTATUS
DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )
{
UNICODE_STRING nameString, linkString;
PDEVICE_OBJECT deviceObject;
NTSTATUS status;
//slave.sys needs
to start before master.sys
// Create the device object.
RtlInitUnicodeString( &nameString, L
"\\Device\\MASTER");
status = IoCreateDevice( DriverObject,
sizeof(FOO_DEVICE_EXTENSION),
&nameString,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&deviceObject );
if (!NT_SUCCESS( status ))
return status;
// Create the symbolic link.
RtlInitUnicodeString( &linkString, L"\\DosDevices\\MASTER") );
status = IoCreateSymbolicLink (&linkString,
&nameString);
if (!NT_SUCCESS( status )) {
IoDeleteDevice
(DriverObject->DeviceObject);
return status;
}
// Initialize the driver object with this device driver's entry points.
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MasterDispatchIoctl;
return STATUS_SUCCESS;
}
NTSTATUS
MasterDispatchIoctl( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
NTSTATUS
status;
PIO_STACK_LOCATION
irpSp;
// Init to default
settings- we only expect 1 type of
// IOCTL to roll through here, all others an
error.
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
// Get a pointer to
the current location in the Irp. This is where
// the function codes and parameters are
located.
irpSp = IoGetCurrentIrpStackLocation( Irp );
switch (irpSp->Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_CALL_KERNEL_MODE_DLL:
if ( NT_SUCCESS( DoSomethingMeaningless( Irp , irpSp) ) ) {
status = STATUS_SUCCESS;
}
else
status = STATUS_INVALID_PARAMETER;
break;
default:
Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
break;
}
// DON'T get cute and try to use the status
field of
// the irp in the return status. That IRP IS
GONE as
// soon as you call IoCompleteRequest.
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// We never have pending operation so always
return the status code.
return status;
}
Figure 6 master.c
Installing the Driver
Both the driver and DLL are copied into the \%SystemRoot%\system32\drivers directory. I created a file called test.ini (Figure 7) and used the regini.exe utility in the DDK to update the registry. I was advised that the DLL should be loaded before the driver. Therefore, I set the Tag value accordingly so that the DLL would start before the driver when the group started.
During my testing, I found that if I unloaded the DLL using the devices applet (or set the startup to Manual), my test application still worked. I then removed the entire registry entry for the DLL and was still able to load the driver and run my test application. However, if the slave.sys was not in the \%SystemRoot%\system32\drivers directory, then the driver failed to load.
\registry\machine\system\currentcontrolset\services\SLAVE
Type = REG_DWORD
0x00000001
Start = REG_DWORD
0x00000002
Group = Extended base
ErrorControl =
REG_DWORD 0x00000001
Tag = REG_DWORD
0x00000001
Testing the Driver and DLL
To test the driver and DLL, I used a simple console application (Figure 8) that is run from the command prompt. The application gets a handle to the driver, sends it the IOCTL, tests the returned value, and displays the result. The application was successfully tested on NT Workstation 4.0.
#include "windows.h"int main ( void )
{
HANDLE hDriver;
DWORD cbReturned = 0x0;
WORD iobuf = 0x0;
// Try to open the device
if ((hDriver =
CreateFile("\\\.\\MASTER",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL )) != INVALID_HANDLE_VALUE )
printf ("\nRetrieved valid handle for MASTER driver\n");
else
{
printf ("Can't get a handle to MASTER
driver\n");
return 0;
}
// iobuf initialized to zero, so we better get something different after call.
if ( DeviceIoControl (hDriver,
(DWORD) IOCTL_CALL_KERNEL_MODE_DLL,
&iobuf,
(DWORD)(2*(sizeof (WORD))),
&iobuf,
(DWORD)(2*(sizeof (WORD))),
&cbReturned,
(LPVOID)NULL) )
{
if (iobuf ==
0xAA55) {
printf("DeviceIoControl worked and returned the correct value.\n");
} else {
printf("DeviceIoControl worked but reurned the wrong value %x\n", iobuf);
}
} else {
printf("DeviceIoControl Failed\n");
}
CloseHandle(hDriver);
return 1;
}
Conclusion
Kernel mode DLLs can be a useful tool in developing and testing kernel drivers. I found that they are easy to build and simple to use, once you know how.