About KmdTut

21 August 2004

KmdTut - A set of tutorials about kernel mode drivers development in assembly language.

You can download KmdTut from http://www.freewebs.com/four-f/. You also will need KmdKit.

More articles (in Russian) available on http://www.wasm.ru/.


About KmdKit

KmdKit - Kernel Mode Driver development Kit for assembly language programmers.

The package contains all you need to code kernel mode drivers in assembly language using masm compiler.

You can download KmdKit from http://www.wasm.ru/ or http://www.freewebs.com/four-f/


The Basics

1.1 Architecture Overview
1.1.1 Main system components
1.1.2 Kernel Mode vs. User Mode
1.2 Windows NT Device Drivers
1.2.1 Types of Device Drivers
1.2.2 Layered and Monolithic (single-layered) Device Drivers
1.3 Thread context
1.4 Interrupt request levels
1.5 System Crashes
1.6 Driver Development Kit
1.7 Kernel-Mode Driver Kit for MASM programmers
1.8 Driver Debugging
1.9 Read also

These tuts explain how to write kernel-mode device drivers for Windows NT based operating system line including currently NT4.0, 2000, XP and 2003 in assembly language (using masm compiler). Writing VxD drivers for Windows 95/98/ME is beyond the scope of these tuts. Also, no doubt these tuts are far from the perfection. Probably they contain some inaccuracies. If you found anything incorrect, please feel free to contact me. I'm not native English person. So, translating all that stuff into English was a really hard work for me. I thank masquer and Volodya for proofreading. The English is probably still not perfect, sorry.

1.1 Architecture Overview

1.1.1 Main system components

Windows NT internals are divided by two distinct part concerning both address space and code permissions and responsibilities.

Address space sharing is amazingly simple. Whole four gigabytes of memory available in 32-bit architecture divided by two equal parts (4GT RAM Tuning and Physical Address Extension omitted as an exotic case). The address space for a user-mode processes is mapped into the lower 2GB of linear memory at addresses 00000000 - 7FFFFFFFh. The upper 2GB of linear memory address range 80000000h - 0FFFFFFFFh maps system components such as device drivers, system memory pools, system data structures etc. Sharing permissions and responsibilities is slightly complicated.

Basic types of user-mode processes are described as follows:

  • System Support Processes - for example, the Logon process (contained in \%SystemRoot%\System32\Winlogon.exe);

  • Service Processes - for example, the Spooler service (contained in \%SystemRoot%\System32\spoolsv.exe);

  • User Applications - can be one of five types: Win32, Windows 3.1, MS-DOS, POSIX and OS/2;

  • Environment Subsystems - Windows ships with three environment subsystems: Win32 (contained in \%SystemRoot%\System32\Csrss.exe); POSIX (contained in \%SystemRoot%\System32\Psxss.exe); OS/2 (contained in \%SystemRoot%\System32\Os2ss.exe).

In Windows XP and later POSIX and OS/2 subsystems are removed.

The kernel-mode components include the following:

  • Executive - memory management, process and thread management, security, etc.;

  • Kernel - thread scheduling, interrupt and exception dispatching, etc. (Executive and Kernel contained in \%SystemRoot%\System32\Ntoskrnl.exe);

  • Device Drivers - hardware device drivers, file system and network drivers;

  • Hardware Abstraction Layer, HAL - isolates the kernel, device drivers, and executive from platform-specific hardware differences (contained in \%SystemRoot%\System32\Hal.dll);

  • Windowing And Graphics System - implements the graphical user interface (GUI) functions such as dealing with windows, user interface controls, and drawing (contained in \%SystemRoot%\System32\Win32k.sys).

Fig. 1-1 Simplified Windows NT architecture

1.1.2 Kernel Mode vs. User Mode

The architecture of the Intel x86 processor defines four privilege levels (known as rings). Windows uses privilege level 0 (or ring 0) for kernel-mode and privilege level 3 (or ring 3) for user-mode. The reason Windows uses only two levels is that some of the hardware architectures that were supported in the past (such as Compaq Alpha and Silicon Graphics MIPS) implemented only two privilege levels.

Each user-mode process has its own private memory space. These processes threads run at the least privileged level (called user-mode or ring 3) and can not execute privileged CPU instructions, have limited and indirect access to the system data and system address space and have no direct access to the hardware equipment. For example, if a user-mode process touches any address in the upper half of the 4GB address space, the system will terminate it immediately. Note that thread can switch to the kernel-mode calling system services, but in this case it completely lose its execution control until gets back to the user-mode.

The user-mode processes are considered as potentially dangerous in terms of system stability. Their rights are strictly limited. All attempts to violate these restrictions are terminated.

The kernel-mode components sharing common protected kernel-mode memory space, run at a privileged or supervisor level (called kernel-mode or ring 0), able to execute any CPU instructions, including privileged ones, have unlimited and unrestricted access to the system data, code and hardware resources.

The code running in the system address space is considered to be completely trusted. Being loaded, the driver becomes a part of the system itself. Therefore the driver is running as a trusted component of the kernel itself, with seemingly unrestricted power to do everything it wants.

Properly speaking the user-mode applications are completely separated from the operating system. It's good for the integrity of the operating system but it could be a headache for some kind of utility application such as debugging tools. Fortunately, unrestricted access provided by the kernel-mode drivers could be used to perform practically impossible tasks on behalf of user-mode applications. So, if you plan to access internal operating system functions or data structures that are not accessible in user-mode, the only way is to load a kernel-mode driver into the system address space. It's rather simple, yet reliable and completely supported by the operating system itself.

1.2 Windows NT Device Drivers

1.2.1 Types of Device Drivers

Windows NT supports a wide range of different device driver types, which can be divided into the following broad categories:

  • User-Mode Drivers:

    • Virtual Device Drivers (VDD) - a user-mode component that are used to emulate 16-bit MS-DOS applications. Although this kind of driver shares a name of VxD used in Windows 95/98, they are completely different.

    • Printer Drivers - translate device-independent graphics requests to printer-specific commands.

  • Kernel-Mode Drivers:

    • File System Drivers - implement the standard file system model;

    • Legacy Drivers - control a hardware devices without help from other drivers and are written for earlier versions of Windows NT but that run unchanged on Windows 2000/XP/2003;

    • Video Drivers - visual data rendering;

    • Streaming Drivers - support multimedia devices such as sound cards;

    • WDM-Drivers - adhere to the Windows Driver Model (WDM). WDM includes support for Windows NT power management and Plug and Play. WDM is implemented on Windows 2000, Windows 98, and Windows Millennium Edition, so WDM drivers are source-compatible between these operating systems and in many cases are also binary compatible.

In different papers you can meet a slightly different classification, but it really doesn't matter.

It is clear from the name, the device driver is a piece of code intended to control a device. It's not necessary for device to be the physical one. It could be a virtual one as well.

Structurally the device driver is nothing more than the file of the PE-format (Portable Executable, PE). Exactly the same as any other EXE or DLL. Device drivers are loadable kernel-mode modules (generally with "sys" extension). The only difference is that the device drivers are loaded and managed completely other way. Actually, we can consider them as kernel-mode DLL intended to perform the tasks insoluble from the user-mode. The fundamental difference here (not considering the privilege level) is that we can't directly access neither the device driver nor its code, nor its data. The only possible way to do it is through the I/O Manager. It provides a primitive driver management environment.

Proceeding to the studying of the kernel-mode device driver's development, you will feel yourself like a newbie, since all your previous experience of programming with Win API will not help you. Even if you have written a considerable amount of the user-mode application development - the kernel offers you a completely different set of functions and structures. You will also have to use poorly documented (defined only in the header files) or totally undocumented functions and data structures.

1.2.2 Layered and Monolithic (single-layered) Device Drivers

The most drivers that control physical devices are layered drivers. Layered driver is the one that handles user-mode requests, processing and passing each request from the highest-level driver to the lowest-level driver. An I/O requests handling is distributed between several drivers. For example, if an application issues a read request for a file stored on hard disk, I/O request is passed to the file system driver that would simply rout it to the disk driver, asking it to read data from certain location on the hard disk. It's possible to add any amount of filter-drivers in between (e.g. for the encryption/decryption purposes).

The monolithic driver is a simplest type of a device driver. This device driver type generally has no dependencies on other loaded device drivers. They just provide an interface to the user-mode applications without any support from anyone else. Developing and debugging this type of drivers is a quite simple task. This is the type of the device drivers we are about to discuss. The other device driver categories are beyond the scope of these articles.

1.3 Thread context

Since, in most cases, we have only the one CPU, the system creates an illusion of simultaneous applications execution. For all those running applications, the operating system schedules some CPU time for each process's thread. This creates the illusion of all the threads run concurrently by offering time slices to the threads in a round-robin fashion. If the machine has multiple CPUs, the operating system's algorithm is much more complex to keep the balance of the threads over the CPUs. When Windows determine it should select a new thread to run, it performs a context switch. A context switch is the procedure of saving the machine state associated with a running thread, loading another thread's state, and starting its execution. If the selected thread belongs to other process, it is necessary to load the process page directory pointer into CR3 register.

Each user process has private address space, so both different directories of pages and sets of the page tables (CPU uses them to translate the virtual addresses into the physical ones). All that has nothing to do with the driver programming directly. But since context switches waste CPU time, the drivers basically don't create their own threads. Therefore, for saving CPU time on context switching, drivers run in kernel-mode in one of three contexts:

  • In the context of the user thread that initiated an I/O request;

  • In the context of a kernel-mode system thread;

  • As a result of an interrupt (and therefore not in the context of any particular process or thread - whichever process or thread was current when the interrupt occurred).

By processing I/O request packets (IRPs) we are always running in the same process context as the user-mode caller and thus we can directly address its memory space. When the driver is loaded/unloaded we are in the system process context and can only deal with the system memory space.

1.4 Interrupt request levels

The interrupts are integral part of the any operating system. Every interrupt requires processing therefore it diverts the processor to execute outside the normal control flow. There are both software and hardware interrupts available. Interrupts are served in the priority order, and a higher-priority interrupt preempts the serving of a lower-priority interrupt.

Windows imposes interrupt priority scheme known as interrupt request levels (IRQLs). The kernel represents IRQLs internally as a number from 0 (passive) through 31 (high), with higher numbers representing higher-priority interrupts. IRQL priority levels have a completely different meaning than thread-scheduling priorities, by the way.

Strictly speaking the interruption with IRQL=0 is not an interrupt, since it can't interrupt flow of any code (this requires lower IRQL, but there is no such level). Every user-mode thread runs at the passive level. The code of our drivers will flow at this IRQL too. It doesn't mean that the code of any driver should always run at the passive level.

Hence two important conclusions follow:

First: The driver's code is always preemptable by any activity that executes at an elevated IRQL, as along with any user-mode thread's code. There are functions allowing to find out the current IRQL, and also to raise or to lower it.

Second: At the passive IRQL it's possible to call any kernel function. (DDK specifies required IRQL for each function to call). Also we can address both nonpaged and paged memory. An attempt to reference paged memory at an elevated IRQL (equal or more than DISPATCH_LEVEL) results to the system crash, since Memory Manager becomes incapable of serving page faults.

1.5 System Crashes

I guess everyone saw at least once the exciting picture known by the name "Blue Screen Of Death", (BSOD). Probably, there is no need to explain, what it is and why it appears sometimes. Prepare yourself to meet BSOD often while developing kernel-mode drivers.

Windows doesn't provide any protection to the private system memory being used by drivers running in the kernel-mode. Once in the kernel-mode the driver has complete access to the system memory space and to all operating system data. So, design and test your drivers carefully to ensure that they don't violate system stability.

You can consider all above-mentioned as the very basic essential principles to understand. It is impossible to start developing the kernel-mode drivers without concepts of the thread context, interrupt request levels, kernel-mode and user-mode are, etc.

1.6 Driver Development Kit

The Windows DDK is a part of the MSDN Professional (and Universal) subscription, but it is also available at http://www.microsoft.com/ddk/). The DDK is an abundant source of Windows NT internals information including the internal system routines and data structures used by device drivers. Unfortunately, Microsoft has stopped free distribution of the DDK. Now you have to order a CD.

Besides the documentation itself, DDK includes a set of Library Files (*.lib), necessary at the linking stage. There are two packages of such files: for free version of Windows (called the free build); and for special debug version (called the checked build). These files are in the directories %ddk%\libfre\i386 and %ddk%\libchk\i386 accordingly. The checked build is a recompilation of the Windows source code with the compile-time flag DEBUG set to TRUE. It performs more strict error checking on kernel-mode functions called by device drivers or other system code. It's necessary to use files according to your Windows version.

1.7 Kernel-Mode Driver Kit for MASM programmers

KmdKit contains all you need to start programming kernel-mode drivers using asm: include files, libraries, macros, examples, tools, these articles, etc. Please, explore the package for details. In the next articles we'll analyze some examples from this package.

1.8 Driver Debugging

As we should debug a kernel-mode code an appropriate debugger is required. SoftICE by Compuware ( http://www.compuware.com/products/numega/index.htm ) is the best choice. Also you can use Microsoft Kernel Debugger. It requires two computers - a target and a host. The target is the system being debugged, and the host is the system running the debugger. Mark Russinovich ( http://www.sysinternals.com/ ) has written the utility LiveKd that allows the use of the standard Microsoft Kernel Debugger on a live system, without needing two computers.

1.9 Read also

  • David Solomon, Mark Russinovich, "Inside Microsoft Windows 2000. Third Edition", Microsoft Press, 2000

    • Though there is no source code in this book at all, it's the number one book for the device driver programmers.

  • Sven B. Schreiber, "Undocumented Windows 2000 Secrets. A Programming Cookbook", Addison-Wesley

    • The especially practical book, it has many Windows 2000 secrets revealed.

  • Walter Oney, "Programming the Microsoft Driver Model", Microsoft Press, 1999

  • Walter Oney, "Programming the Microsoft Windows Driver Model. 2nd edition", Microsoft Press, 2003

    • Excellent book. Its emphasis is Plug'n'Play drivers, but it does not minimize its importance, since the base principles of driver development are universal.

  • Art Baker č Jerry Lozano, "The Windows 2000 Device Driver Book, A Guide for Programmers, Second Edition", Prentice Hall, 2000

    • Also good book and conforms to our subject.

  • Rajeev Nagar, "Windows NT File System Internals. A Developer's Guide", O'Reilly

    • The title of this book says all.

  • Prasad Dabak, Sandeep Phadke, and Milind Borate, "Undocumented Windows NT", M&T Books, 1999

    • You will discover from this book a couple of undocumented things about Windows NT internals.

  • Gary Nebbett, " Windows NT-2000 Native API Reference", MacMillan Technical Publishing, 2000

    • If you plan to use some undocumented functions and structures in your device drivers this book is for you.

  • Jeffrey Richter, "Programming Applications for Microsoft Windows. Fourth Edition", Microsoft Press, 1999

    • This book has nothing to do with the device drivers programming, but is very interesting too ;-)

This list doesn't apply for entirety at all. Concerning the books, I still want to say that all of them are from bit "must have".

Copyright © 2002-2004 Four-F, four-f@mail.ru


Services

2.1 Services
2.2 The Service Control Manager
2.3 The Service Control Program
2.3.1 Establishing a connection to the SCM
2.3.2 Installing new driver
2.3.3 Starting the driver
2.3.4 Uninstalling the driver
2.4 String macros
Source code: KmdKit\examples\simple\Beeper

You may wonder how user-mode services related to the kernel-mode drivers. Actually they are completely different animals altogether. But before we can communicate with the device driver we have to install and start it at first. So, let's conform the interface rules regarding interaction with the services.

2.1 Services

Windows NT has a mechanism to start processes that provide services not tied to an interactive user. Such processes are called services. A good example of a service might be a Web server. Most of the services don't have any user interface. It's a sole category of applications working that way. Services can be started at the system startup time or they can be also started manually. In that sense the device drivers are very similar to the services.

Windows NT also supports a driver service, which conforms to the device driver protocols for Windows NT. It's similar to the user-mode service. So, service can be referred either to a user-mode server process or to a kernel-mode device driver. Microsoft had for unknown reasons mixed up user-mode services and kernel-mode drivers. Therefore further narration can seem a little bit confusing, since I will use at times term "driver", at the other times - "service". But this article deals with kernel-mode device drivers only. And you should always consider it like a "driver". I will explicitly note when necessary to separate "service" from "driver". Also keep in mind that the documentation describing the functions to manipulate with the services is rather ambiguous at times. Many functions discussed in this section apply to both services and device drivers, but I will emphasize on device drivers and omit discussing services.

There are three types of components involved in making Windows NT services work:

  • Service Control Manager (SCM). The SCM is responsible for starting the service, communicating with it and so on.

  • Service Control Program (SCP). The SCP communicates with the SCM telling it when to start or stop service and so on.

  • A service program that contains executable code. And as I noted earlier the service is considered as the kernel-mode device driver.

As I have already said, we'll study the driver itself in the next part, and now we'll concentrate on the first two components.

2.2 The Service Control Manager

The SCM lives in \%SystemRoot%\System32\Services.exe. Winlogon process starts the SCM early during the system boot. It then scans the contents of the registry under the key HKLM\SYSTEM\CurrentControlSet\Services, creating an entry in the service database for each key it encounters. A database entry includes all the service-related parameters defined for a service. If service or a driver is marked for auto-start the SCM starts it and detects startup failures.

To gain some insight about it, start the Registry Editor (\%SystemRoot%\regedit.exe), then open HKLM\SYSTEM\CurrentControlSet\Services\ and explore its content.

To view the list of installed services (not drivers), select Administrative Tools from Control Panel, and then select Services.

Running Computer Management you can list the installed drivers. (From the Start menu, select Programs, Administrative Tools, and then Computer Management; or from Control Panel, open Administrative Tools and select Computer Management.) From within Computer Management expand System Information and then Software Environment, and open Drivers (Unfortunately, this feature is unavailable since Windows XP).

Having analyzed the content of these three windows, you will notice that they coincide in many respects.

The HKLM\SYSTEM\CurrentControlSet\Services\ contains a subkeys, denoted by an internal name of the driver or service. Each subkey includes all the service-related parameters.

Let's consider a minimum possible set of parameters necessary to install device driver. As an example, we'll take the beeper.sys driver (we'll talk about the driver itself next time).

Figure 2-1. Registry key for beeper.sys driver

Parameter Description

DisplayName

- Name of service to be used by user interface programs. If no name is specified, the name of the service's registry key becomes its name.

ErrorControl

- If a driver reports an error in response to the SCM's startup command, this value specifies the level of error control and determines SCM's reactions.

Two values can be of interest for us:

SERVICE_ERROR_IGNORE (0)

- The I/O Manager ignores errors the driver returns but continues the startup operation. Nothing is logged;

SERVICE_ERROR_NORMAL (1)

- If the driver fails to load or initialize, startup should proceed with a warning display to the user. And an event to the System Event Log is written.

You can view an event description by selecting Administrative Tools > Event Viewer and double-clicking on an Event Log entry.

For example, the beeper driver does all useful job at initialization stage (in the DriverEntry routine), then it returns an error code to be removed from the memory since it can't do anything more. The ErrorControl parameter for beeper driver is equal to SERVICE_ERROR_IGNORE, so no logging occured.

ImagePath

- Specifies the fully qualified path of the driver's image file.

If ImagePath is not specified, the I/O Manager looks for drivers in \%SystemRoot%\Drivers directory.

Start

- Specifies when to start the driver.

There can be useful only two values to us:

SERVICE_AUTO_START (2)

- A driver is started during system startup.

SERVICE_DEMAND_START (3)

- A driver is started by the Service Control Manager in response to an explicit user demand.

If driver has Start specified as SERVICE_AUTO_START (2) it will be started by the SCM during system startup. Such drivers are called auto-start services. If the driver depends on any other drivers SCM will starts those drivers too (To control the order of loading device drivers use the Group, Tag and DependOnGroup values and services use Group and DependOnService). There are also other flags indicating auto-start, for example, SERVICE_BOOT_START (0). Only device drivers can specify it. The I/O Manager loads such drivers before any user-mode processes execute, and therefore before the SCM starts.

Type

- Specifies the type of service.

Since we are going to deal with device driver the only value we can use is SERVICE_KERNEL_DRIVER (1).

Having looked on figure 2-1 what can we tell about beeper.sys driver? Well, Kernel-mode driver beeper is resides in C:\masm32\Ring0\Kmd\Article2\beeper directory. It has display name "Nice Melody Beeper", started on demand, possible errors are ignored and not logged.

What prefix "\??" in the path to the driver's image file means you will know later.

If we want to start the driver not presented in the SCM database, it can be done dynamically, at any moment, with the help of the service control program (device control program to be more precise, but there in no such concept in Microsoft terminology).

2.3 The Service Control Program

As follows from its name, the service control program is intended to control the service or device driver. It does this under the SCM supervision, calling the appropriate functions. All of them are exported by the module \%SystemRoot%\System32\advapi.dll (Advanced API).

Here is the code of SCP, which will control the beeper.sys driver.

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  Service Control Program for beeper driver
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                             I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

start proc

local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("beeper.sys"), sizeof acDriverPath, addr acDriverPath, esp
        pop eax

        invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
        .if eax != NULL
            mov hService, eax
            invoke StartService, hService, 0, NULL
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
                            NULL, MB_ICONSTOP
    .endif

    invoke ExitProcess, 0

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

2.3.1 Establishing a connection to the SCM

The first thing we have to do is to call OpenSCManager function to establish a connection to the SCM on the specified computer and to open the specified database.

OpenSCManager proto lpMachineName:LPSTR, lpDatabaseName:LPSTR, dwDesiredAccess:DWORD
Parameter Îďčńŕíčĺ

lpMachineName

- Points to a null-terminated string that names the target computer. If the pointer is NULL or if it points to an empty string, the function connects to the SCM on the local computer.

lpDatabaseName

- Points to a null-terminated string that names the SCM database to open. This string should specify ServicesActive. If it is or NULL, the ServicesActive database is opened by default.

.const
szActiveDatabase db "ServicesActive", 0
SERVICES_ACTIVE_DATABASE equ offset szActiveDatabase

Since we are not going to open any other SCM database, except for the active one, we simply specify NULL.

dwDesiredAccess

- Specifies the access right to the SCM.

This parameter tells the SCM what we intend to do with its database.

Three values can be useful to us:

SC_MANAGER_CONNECT

- Enables connecting to the SCM.

This access type is implicitly specified by default (if you simply pass 0). Very strange, but the documentation tells nothing about what particularly we can do having this access type. But many actions can be done. We can start and stop the driver, and even delete its entry from the SCM database;

SC_MANAGER_CREATE_SERVICE

- Enables calling of the CreateService function to create a service object and add it to the database.

Actually having this access type creating a service is not a sole thing we can do. Since the SC_MANAGER_CONNECT flag is set by default, we can do all possible with this access type. Though it's not obvious too;

SC_MANAGER_ALL_ACCESS

- Gives full access to the SCM database.

We establish a connection to the SCM in this way:

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

If the OpenSCManager succeeds, the return value is a handle to the specified SCM database. We'll pass it to other functions to manipulate the SCM database.

By the way, I've forgotten to say that the installation of kernel-mode device driver requires an account with administrator privileges. It provides the necessary security. So normal users cannot add and execute privileged code without the proper authority. Therefore, it's assumed here that you have appropriate privilege level.

2.3.2 Installing new driver

Once the SCM has been opened, we add our driver to its database by the call to CreateService. Here is its prototype. CreateService has thirteen parameters. But don't panic. Actually everything is rather simple.

CreateService proto hSCManager:HANDLE, lpServiceName:LPSTR, lpDisplayName:LPSTR, \
                    dwDesiredAccess:DWORD, dwServiceType:DWORD, dwStartType:DWORD, \
                    dwErrorControl:DWORD, lpBinaryPathName:LPSTR, lpLoadOrderGroup:LPSTR, \
                    lpdwTagId:LPDWORD, lpDependencies:LPSTR, lpServiceStartName:LPSTR, \
                    lpPassword:LPSTR

Parameter | Îďčńŕíčĺ
hSCManager | - Handle to SCM database.
lpServiceName | - Points to a null-terminated string that names the service to install. The maximum string length is 256 characters. Forward-slash (/) and back-slash (\) are invalid service name characters. This string corresponds to a name of a service registry subkey.
lpDisplayName | - Points to a null-terminated string that is to be used by user interface programs to identify the service. This string has a maximum length of 256 characters. Corresponds to the DisplayName value under service registry subkey.
dwDesiredAccess | - Specifies the access to the service. There can be useful following values for us:
 | SERVICE_ALL_ACCESS | - Full access to the service;
 | SERVICE_START | - Enables calling of the StartService function to start the service;
 | SERVICE_STOP | - Enables calling of the ControlService function to stop the service;
 | DELETE | - Enables calling of the DeleteService function to delete the service;
 | We need to do only two things: to start the driver and to remove it from the SCM database. So, we pass SERVICE_START and DELETE in this parameter. We don't have to stop the started driver since its initialization will fail.
dwServiceType | - Specifies the type of service. We use only SERVICE_KERNEL_DRIVER. Corresponds to the Type value under service registry subkey.
dwStartType | - Specifies when to start the service. If we want to start the driver by ourselves we pass SERVICE_DEMAND_START. If the driver should be started right after system boots, just before logon prompt appears, pass SERVICE_AUTO_START. Corresponds to the Start value under service registry subkey.
dwErrorControl | - Specifies the severity of the error if the driver fails to start during startup. We use SERVICE_ERROR_IGNORE to ignore errors or SERVICE_ERROR_NORMAL to log possible errors. Corresponds to the ErrorControl value under service registry subkey.
lpBinaryPathName | - Points to a null-terminated string that contains the fully qualified path to the driver binary file. Corresponds to the ImagePath value under service registry subkey.
lpLoadOrderGroup | - Points to a null-terminated string that names the load ordering group of which this service is a member. Our driver does not belong to any group, so we simply pass NULL.
lpdwTagId | - Points to a 32-bit variable that receives a unique tag value for this service in the group specified in the lpLoadOrderGroup parameter. No tag is required for us and this parameter will be NULL.
lpDependencies | - This parameter has no meaning for the driver services. It will be always NULL.
lpServiceStartName | - Pointer to a null-terminated string with account name the service should run under. If the service type is SERVICE_KERNEL_DRIVER the name is the driver object name that the system uses to load the device driver. We specify NULL as our driver is to use a default object name created by the I/O subsystem.
lpPassword | - Passwords are ignored for driver services. Should always be NULL.

Let me draw a bottom line here. In the last five parameters we always specify NULL, and you can completely forget about it. The first parameter is the handle to the SCM database. What is dwDesiredAccess for, I hope is clear too. And I think you already have guessed what are the other parameters are for. Well, they correspond to the registry keys we have analyzed above. The table below is the visual aid for you.

CreateService | Registry
lpServiceName | registry subkey name
lpDisplayName | DisplayName
dwServiceType | Type
dwStartType | Start
dwErrorControl | ErrorControl
lpBinaryPathName | ImagePath

Table 2-1. Correspondence between some parameters passing to the CreateService and the registry keys.

As you can see, not all is so black as it's painted. Let's get back to the source code.

        push eax
        invoke GetFullPathName, $CTA0("beeper.sys"), sizeof acDriverPath, addr acDriverPath, esp
        pop eax

        invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
        .if eax != NULL
            mov hService, eax

Calling GetFullPathName function we form the complete path to the device driver file and pass it to the CreateService.

CreateService adds our driver to the SCM database, and creates an appropriate registry subkey. Look at Figure 2-1 once again. All this info was added into the registry by CreateService. If you comment the call to DeleteService out, recompile csp.asm and run it you can see exactly the same on your computer.

Don't think that using general RegXxx functions to manipulate with the registry it is possible to achieve the same result. You can add the data into the registry, but it will not appear in the SCM database.

If the specified device driver already exists in the SCM database the call to CreateService will fail. Calling GetLastError returns ERROR_SERVICE_EXISTS. If CreateService is able to successfully add the driver to the SCM database, the handle to driver is returned. This handle is required by other functions in order to manipulate the driver.

2.3.3 Starting the driver

The next function we have to call is StartService. And here is its prototype:

StartService proto hService:HANDLE, dwNumServiceArgs:DWORD, lpServiceArgVectors:LPSTR

Parameter | Description
hService | - Identifies the opened service.
dwNumServiceArgs | - This parameter is always zero for device drivers.
lpServiceArgVectors | - Driver services do not receive any arguments. So, it should be NULL.

Now we start the driver like this:

            invoke StartService, hService, 0, NULL

The StartService function forces the system to make some actions that reminds loading common user-mode DLL. An image of the driver's file is mapped into the system address space. The driver is always mapped at arbitrary address. Then the system performs relocations within the driver image using reloc section of PE file. All references to imported symbols are fixed up. When the driver's image is prepared, the system calls an entry point of the driver, which resides in the DriverEntry routine. The main difference here is that the code of the DriverEntry routine always runs in context of the system process.

The call to StartService function is synchronous. It means it will not return until the driver's DriverEntry routine finished. If the driver initialization succeeds, DriverEntry should return STATUS_SUCCESS, and the StartService will return nonzero value. And we are back in the context of thread called StartService again, i.e. the context of our SCP.

We don't care about the value, returned by the StartService, since beeper driver has already played its nice melody and returned an error code. So, we know beforehand that the StartService will return an error.

2.3.4 Uninstalling the driver

            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager

Now all we have to do is bring the system to initial state. Calling DeleteService we remove the driver from the SCM database. Strange, but it is not necessary to pass the handle of the SCM database to DeleteService. The DeleteService prototype is simple:

DeleteService proto hService:HANDLE

Parameter | Description
hService | - Identifies the service to be removed. It is necessary to have appropriate access right. We have it.

This function does not actually delete the service right away; it simply marks the service for deletion. The SCM will delete the service only when the service stops running and after all handles to the service have been closed. As we still hold the handle to the driver it's not removed from the SCM database. If you try to call DeleteService again, it will fail. Calling GetLastError returns ERROR_SERVICE_MARKED_FOR_DELETE.

Since we don't need to communicate with the driver anymore, we must close the handle to it by calling CloseServiceHandle:

CloseServiceHandle proto hSCObject:HANDLE

Parameter | Description
hSCObject | - Handle to the driver or SCM database to be closed.

As there are no open handles to the driver now, its entry is removed from the SCM database. The second call to CloseServiceHandle closes the handle to the SCM itself.

2.4 String macros

Finally you should know what $CTA0 is. It's a macro function. It let you define ASCII string terminating with zero in read-only data section. You can use it right in the invoke macro. This macro is not sole. The file \Macros\Strings.mac contains many other useful macros to define strings with detailed explanation how to use it. Since it has nothing related with the kernel-mode driver programming I will not pay your attention to this subject anymore, but I will use such macros everywhere.


The Simplest Device Drivers

3.1 How to compile and link the kernel-mode device driver
3.2 The simplest possible kernel-mode device driver
3.2.1 Simplest driver source code
3.2.2 DriverEntry Routine
3.3 Beeper device driver
3.3.1 Beeper driver source code
3.3.2 Controlling the system timer
3.3.3 Starting the driver automatically
3.4 Service Control Program for giveio driver
3.4.1 Giveio driver's SCP source code
3.4.2 Using the registry for passing some info to the driver
3.4.3 Accessing the CMOS
3.5 Giveio device driver
3.5.1 Giveio driver source code
3.5.2 I/O permission bit map
3.5.3 Reading info from the registry
3.5.4 Give user-mode process access to the I/O ports
3.6 A couple of words about driver debugging
Source code: KmdKit\examples\simple\Beeper
Source code: KmdKit\examples\simple\DateTime

3.1 How to compile and link the kernel-mode device driver

I always place driver's source code into a batch file. Such file is a mixture of *.bat and *.asm files, but has "bat" extension.

;@echo off
;goto make

.386                      ; driver's code start

  ;::::::::::::::::::::::::::::::::
  ; the rest of the driver's code ;
  ;::::::::::::::::::::::::::::::::

end DriverEntry            ; driver's code end

:make

set drv=drvname

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

If you run such "self-compiling" file the following will occur. First two commands are commented out, thus they ignored by masm compiler, but accepted by command processor, that in turn ignores semicolon symbol. The control jumps to :make label where some options for the compiler and linker are specified. All instructions following the end directive is ignored by the compiler. Thus all lines between goto make command and :make label are ignored by the command processor but accepted by the compiler. And all that is outside (including goto make command and :make label) is ignored by the compiler but accepted by the command processor. This method is extremely convenient, since the source code itself keeps all the info about how it should be compiled and linked. Also you can simply add some extra processing if you need. I use this method for all my drivers. Since the control programs usually don't require anything special you can compile it as you like.

set drv=drvname

We define an environment variable, which will be the substitution for the driver's file name.

The used linker options means:

Command Key | Description
/driver | - tells the linker it should create a Windows NT kernel-mode driver; The most important effect of this option is the addition of a new section called "INIT". Here goes the part of ".idata" section, which contains a list of IMAGE_IMPORT_DESCRIPTOR structures and the names of imported functions and modules. This "INIT" section is marked as discardable in order to NT image loader discard it after locating the imported function addresses.
/base:0x10000 | - sets the driver base address equal to 10000h. We have already discussed about it in the preceding part;
/align:32 | - the system memory is a precious resource. Therefore, the device driver's files have more "fine" section alignment;
/out:%dvr%.sys | - the linker creates *.exe files by default. Or builds a *.dll if the /DLL option is specified. We should force the linker to create *.sys file.
/subsystem:native | - In the PE header of every executable file there is a field that tells the image loader which subsystem this module requires: Win32, POSIX or OS/2. It's necessary to place a driver's image into the appropriate environment. When we compile *.exe or *.dll file, we usually indicate under this option that the executable file requires a Win32 subsystem. The kernel-mode drivers don't require any subsystem at all, since they run in the native environment.

3.2 The simplest possible kernel-mode device driver

3.2.1 Simplest driver source code

Here is the source code of the simplest possible kernel-mode device driver.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; simplest - Simplest possible kernel-mode driver
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              B U I L D I N G   D R I V E R
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:make

set drv=simplest

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

3.2.2 DriverEntry Routine

Like any other executable module each driver must have the entry point, which is called when the driver is loaded into the memory. The driver's entry point is the DriverEntry routine. This name is conventionally given to the main entry point of a kernel-mode device driver. You may rename it anything you like. The DriverEntry routine initializes driver-wide data structures. The prototype for DriverEntry routine is defined as follows:

DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING

Unfortunately well-known "hungarian notation" by Charles Simonyi is not used in DDK. I will use it everywhere if possible. Therefore, I have added the prefixes to the DriverObject and the RegistryPath.

The data types of PDRIVER_OBJECT and PUNICODE_STRING are defined in \include\w2k\ntddk.inc and \include\w2k\ntdef.inc respectively.

PDRIVER_OBJECT  typedef PTR DRIVER_OBJECT
PUNICODE_STRING typedef PTR UNICODE_STRING

When the I/O Manager calls the DriverEntry routine it passes two pointers to it:

Parameter Description

pDriverObject

- a pointer to a barely initialized driver object that represents the driver.

Windows NT is an object-oriented operating system. So, the drivers are represented as objects. By the loading of the driver into the memory the system creates driver object which represents the given driver. The driver object is nothing more then DRIVER_OBJECT structure (defined in \include\w2k\ntddk.inc). The pDriverObject pointer gives the driver an access to that structure. But we don't need to touch it this time.

pusRegistryPath

- a pointer to a counted Unicode string that specifies a path to the driver's registry subkey. We have discussed about the driver's registry subkey in the previous part.

The driver can use this pointer to store or retrieve some driver-specific information. If a driver will need to use the path after its DriverEntry routine has completed, the driver should save a copy of the unicode string, not the pointer itself since it has no meaning outside the DriverEntry routine.

Counted Unicode String is also the structure of type UNICODE_STRING. Unlike the user-mode code, the kernel-mode code operates with the strings in UNICODE_STRING format. It's defined in \include\w2k\ntdef.inc like this:

UNICODE_STRING STRUCT
    _Length         WORD    ?
    MaximumLength   WORD    ?
    Buffer          PWSTR   ?
UNICODE_STRING ENDS

Parameter | Description
_Length | - The length of the string in bytes (not characters), not counting the terminating null character (I had to change an original Length name, since it is a masm reserved word);
MaximumLength | - The length in bytes (not characters) of the buffer pointed by Buffer member;
Buffer | - Pointer to the Unicode-string itself. Don't expect it as always zero-terminated. It does not sometimes!

The main advantage of this format is its clear determination of both the current string length, and its maximum possible length. It allows avoid some additional calculations.

The above-described driver (\src\Article2-3\simplest\simplest.sys) is the simplest one. The only thing it does allows to load itself. Since it can't do anything more it returns an error code STATUS_DEVICE_CONFIGURATION_ERROR (see \include\w2k\ntstatus.inc for complete list of possible error codes). If you return STATUS_SUCCESS the driver will remain in the memory, and you can't unload it, since we have not defined the DriverUnload routine responsible for this.

You can register and load any driver with the KmdManager utility.

3.3 Beeper device driver

3.3.1 Beeper driver source code

Now let's examine the beeper driver. Last time we have written its control program.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  beeper - Kernel Mode Drive
;  Makes beep thorough computer speaker
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\hal.inc

includelib \masm32\lib\w2k\hal.lib

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     E Q U A T E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

TIMER_FREQUENCY        equ 1193167                   ; 1,193,167 Hz
OCTAVE                 equ 2                         ; octave multiplier

PITCH_C                equ 523                       ; C        -  523,25 Hz
PITCH_Cs               equ 554                       ; C#       -  554,37 Hz
PITCH_D                equ 587                       ; D        -  587,33 Hz
PITCH_Ds               equ 622                       ; D#       -  622,25 Hz
PITCH_E                equ 659                       ; E        -  659,25 Hz
PITCH_F                equ 698                       ; F        -  698,46 Hz
PITCH_Fs               equ 740                       ; F#       -  739,99 Hz
PITCH_G                equ 784                       ; G        -  783,99 Hz
PITCH_Gs               equ 831                       ; G#       -  830,61 Hz
PITCH_A                equ 880                       ; A        -  880,00 Hz
PITCH_As               equ 988                       ; B        -  987,77 Hz
PITCH_H                equ 1047                      ; H        - 1046,50 Hz

; We are going to play c-major chord

TONE_1                 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)
TONE_2                 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)
TONE_3                 equ (PITCH_G*OCTAVE)           ; for HalMakeBeep

DELAY                  equ 1800000h                   ; for my ~800mHz box

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         M A C R O S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DO_DELAY MACRO
    mov eax, DELAY
    .while eax
        dec eax
    .endw
ENDM

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            MakeBeep1
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

MakeBeep1 proc dwPitch:DWORD

    ; Direct hardware access

    cli

    mov al, 10110110y
    out 43h, al

    mov eax, dwPitch
    out 42h, al

    mov al, ah
    out 42h, al

    ; Turn speaker ON

    in al, 61h
    or  al, 11y
    out 61h, al

    sti

    DO_DELAY

    cli

    ; Turn speaker OFF

    in al, 61h
    and al, 11111100y
    out 61h, al

    sti

    ret

MakeBeep1 endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            MakeBeep2
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

MakeBeep2 proc dwPitch:DWORD

    ; Hardware access using WRITE_PORT_UCHAR and READ_PORT_UCHAR
    ; functions from hal.dll

    cli

    invoke WRITE_PORT_UCHAR, 43h, 10110110y

    mov eax, dwPitch
    invoke WRITE_PORT_UCHAR, 42h, al
    mov eax, dwPitch
    invoke WRITE_PORT_UCHAR, 42h, ah

    ; Turn speaker ON

    invoke READ_PORT_UCHAR, 61h
    or  al, 11y
    invoke WRITE_PORT_UCHAR, 61h, al

    sti

    DO_DELAY

    cli

    ; Turn speaker OFF

    invoke READ_PORT_UCHAR, 61h
    and al, 11111100y
    invoke WRITE_PORT_UCHAR, 61h, al

    sti

    ret

MakeBeep2 endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    invoke MakeBeep1, TONE_1
    invoke MakeBeep2, TONE_2

    ; Hardware access using hal.dll HalMakeBeep function

    invoke HalMakeBeep, TONE_3
    DO_DELAY
    invoke HalMakeBeep, 0

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:make

set drv=beeper

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

This driver is intended to beep c-major arpeggio using the motherboard speaker. For this purpose the driver uses IN and OUT CPU instructions, accessing the appropriate I/O ports. It is well-known that the access to the I/O ports is guarded by Windows NT as an important system resource. An attempt to execute IN or OUT instruction from user-mode results in termination of the process. But actually there is a way to bypass this limitation, i.e. to allow the user-mode to access the I/O ports directly. We'll talk about it a bit later.

3.3.2 Controlling the system timer

There are three timers inside the computer. These are known as timers 0, 1 and 2 and they reside in the Programmable Interval Timer (PIT). Timer 2 is used to control sound generation. The frequency at which timer oscillates is determined by an initial count value. The timer counts down from this value to zero, and when it reaches zero, the timer oscillates. The counter is then re-set to the predetermined initial count value and the process starts again. The counting down process is controlled by the main system oscillator, which runs at a frequency of 1,193,180 Hz. This value is fixed across the entire range of PC families. Every time it oscillates, the system timer counts down once. To vary the frequency at which the timer oscillates, we just need to give it a new initial count value. To calculate the frequency at which the speaker will sound we have to use this formula: 1193180/. You can find more detailed information searching the Web.

There is one subtlety here, which I misunderstand a bit. The QueryPerformanceFrequency from kernel32.dll really returns the value of 1193180. But in hal.dll's HalMakeBeep function I have found a little bit different value equal to 1193167. I'll use this value. Probably it's some time compensation. I don't know. Anyway it doesn't prevent us from beeping the speaker.

We play the first sound (do) of the c-major chord using MakeBeep1 routine.

    mov al, 10110110y
    out 43h, al

First we have to set the timer's control register. We should load a binary value 10110110 into port 43h to achieve this goal.

    mov eax, dwPitch
    out 42h, al

    mov al, ah
    out 42h, al

Then, in two consecutive statements, we load the low byte and high byte of the new initial count value into port 42h.

    in al, 61h
    or  al, 11y
    out 61h, al

Now we turn the speaker on by setting bits 0 and 1 of the value on port 61h. Now the speaker is producing the sound.

DO_DELAY MACRO
    mov eax, DELAY
    .while eax
        dec eax
    .endw
ENDM

We let the speaker sound for some time, using DO_DELAY macro. Yes - it is primitive, but is rather effective.

    in al, 61h
    and al, 11111100y
    out 61h, al

To turn the speaker off we need to clear bits 1 and 2 of the value on port 61h. Don't forget that the timer is a global system resource. Therefore we disable the maskable hardware interrupts by clearing the interrupt flag. It will be much more difficult on the multi-processor machine.

We play the second (mi) sound of the c-major chord using MakeBeep2 routine. It differs only by using WRITE_PORT_UCHAR and READ_PORT_UCHAR functions from hal.dll instead of in/out. HAL hides hardware-dependent details such as I/O interfaces (as in our case) and other, making it machine-independent.

The third (sol) sound of the c-major chord we play with the HalMakeBeep from hal.dll. As a parameter it is necessary not to use the initial count value, but the frequency value itself instead.

In the beginning of the beeper.bat file you will find all twelve key notes. I used only three of them. Others are left for your future synthesizer ;-). To turn the speaker off it is necessary to call alMakeBeep once again, passing 0 as an argument.

The beeper driver returns an error code to the system and is removed from the memory. I repeat. It is necessary to return an error code only to cause the system removes the driver from the memory. When we'll reach full-function drivers, we'll have to return STATUS_SUCCESS.

3.3.3 Starting the driver automatically

The scp.exe installs driver beeper.sys to be started on demand. Last time we have discussed different start types of the drivers. Now we try to force the system to start our driver automatically. It can be done in many ways. The simplest one is to comment the call to DeleteService out, change SERVICE_DEMAND_START to SERVICE_AUTO_START and SERVICE_ERROR_IGNORE to SERVICE_ERROR_NORMAL, then recompile scp.asm and execute it. After scp.exe exits the registry will contain the brand new service entry. Now you can completely forget about it. During the next system boot the driver beeper.sys will remind you about itself. In the Event Log you will find the entry about driver's startup failure. Select from the Start menu Programs/Administrative Tools/Event Viewer, select System Log, and double-click on an Event Log entry to see it. You will see something like this:

Figure 3-1. Failure Event Log entry

Don't forget to remove the registry entry otherwise you will hear that nice melody every time your system boots up.

3.4 Service Control Program for giveio driver

3.4.1 Giveio driver's SCP source code

Now let's take a look at another SCP to control giveio.sys driver.

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  DateTime - Service Control Program for giveio driver
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option  casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         M A C R O S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

CMOS MACRO by:REQ
    mov al, by
    out 70h, al
    in al, 71h

    mov ah, al
    shr al, 4
    add al, '0'

    and ah, 0Fh
    add ah, '0'
    stosw
ENDM

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                           C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                          DateTime
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DateTime proc uses edi

local acDate[16]:CHAR
local acTime[16]:CHAR
local acOut[64]:CHAR

    ; See Ralf Brown's Interrupt List for details

    ;:::::::::::::::::: Set data format  ::::::::::::::::::

    mov al, 0Bh               ; status register B
    out 70h, al
    in al, 71h

    push eax                  ; save old data format
    and al, 11111011y         ; Bit 2: Data Mode - 0: BCD, 1: Binary
    or al, 010y               ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode
    out 71h, al

    ;:::::::::::::::::::: Lets' fetch current date  ::::::::::::::::::::

    lea edi, acDate

    CMOS 07h                  ; date of month
    mov al, '.'
    stosb

    CMOS 08h                  ; month
    mov al, '.'
    stosb

    CMOS 32h                  ; two most significant digit od year
    CMOS 09h                  ; two least significant digit od year

    xor eax, eax              ; terminate string with zero
    stosb

    ;:::::::::::::::::::: Lets' fetch current time :::::::::::::::::::

    lea edi, acTime

    CMOS 04h                  ; hours
    mov al, ':'
    stosb

    CMOS 02h                  ; minutes
    mov al, ':'
    stosb

    CMOS 0h                   ; seconds

    xor eax, eax              ; terminate string with zero
    stosb

    ;:::::::::::::: restore old data format :::::::::::::

    mov al, 0Bh
    out 70h, al
    pop eax
    out 71h, al

    ;::::::::::::::::: Show current date and time :::::::::::::::

    invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
    invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK

    ret

DateTime endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

start proc

local fOK:BOOL
local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR

local hKey:HANDLE
local dwProcessId:DWORD

    and fOK, 0        ; assume an error

    ; Open the SCM database

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
    .if eax != NULL
        mov hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("giveio.sys"), sizeof acDriverPath, addr acDriverPath, esp
        pop eax

        ; Register driver in SCM active database

        invoke CreateService, hSCManager, $CTA0("giveio"), $CTA0("Current Date and Time fetcher."), \
                SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
                SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL

        .if eax != NULL
            mov hService, eax

            invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
                                    $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
                                    0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey

            .if eax == ERROR_SUCCESS

                ; Add current process ID into the registry

                invoke GetCurrentProcessId
                mov dwProcessId, eax
                invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
                                        addr dwProcessId, sizeof DWORD

                .if eax == ERROR_SUCCESS

                    ; Start driver

                    invoke StartService, hService, 0, NULL
                    inc fOK                ; Set OK flag
                    invoke RegDeleteValue, hKey, addr szProcessId
                .else
                    invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
                                        NULL, MB_ICONSTOP
                .endif

                invoke RegCloseKey, hKey

            .else
                invoke MessageBox, NULL, $CTA0("Can't open registry."), NULL, MB_ICONSTOP
            .endif

            ; Remove driver from SCM database

            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
                           NULL, MB_ICONSTOP
    .endif

    ; If OK display current date and time to the user

    .if fOK
        invoke DateTime
    .endif

    invoke ExitProcess, 0

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

3.4.2 Using the registry for passing some info to the driver

There is nothing new here except a few points.

            invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
                                    $CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
                                    0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey

            .if eax == ERROR_SUCCESS
                invoke GetCurrentProcessId
                mov dwProcessId, eax
                invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
                                        addr dwProcessId, sizeof DWORD

                .if eax == ERROR_SUCCESS
                    invoke StartService, hService, 0, NULL

Before starting the driver we create additional ProcessId value under driver registry subkey. It contains current process identifier, which is the identifier of SCP itself. Please notice how I use $CTA0 macro here. I did specify a label szProcessId, which the text "ProcessId" will be marked with. This lets us to reference this text later on. My text macros are flexible enough, by the way.

If the new registry value was added successfully, we can start the driver. What this additional registry value is for you'll find out a bit later.

                    inc fOK
                    invoke RegDeleteValue, hKey, addr szProcessId
                .else
                    invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
                                        NULL, MB_ICONSTOP
                .endif

                invoke RegCloseKey, hKey

Having returned from the StartService we consider that the driver has done its work and set fOK flag. The call to the RegDeleteValue is not necessary here, because all driver registry subkeys will be removed by the subsequent call to the DeleteService. But it's a good programming practice to clean up explicitly.

    .if fOK
        invoke DateTime
    .endif

Having removed the driver entry from the SCM database we close all opened handles and, if fOK flag is set, call DateTime function.

3.4.3 Accessing the CMOS

In a computer motherboard there is a microchip that is used to store some system configuration information, such as disk drive parameters, memory configuration, and the date-time. This microchip is often referred to as "the CMOS" (CMOS is an acronym stands for Complementary Metal Oxide Semiconductor). The microchip is battery-powered and has the real-time clock (RTC). We can obtain its data accessing 70h and 71h I/O ports. See "Ralf Brown's Interrupt List" for details ( http://www-2.cs.cmu.edu/afs/cs/user/ralf/pub/WWW/files.html ).

    mov al, 0Bh               ; status register B
    out 70h, al
    in al, 71h

    push eax                  ; save old data format
    and al, 11111011y         ; Bit 2: Data Mode - 0: BCD, 1: Binary
    or al, 010y               ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode
    out 71h, al

Firstly we set a convenient data format using the status register B.

Using CMOS macro we can obtain all the information we need from CMOS and format it at the same time.

    invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
    invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK

Then we expose all retrieved data. And you should see something like this:

Figure 3-2. The output of the DateTime.exe

Most strange thing here is that we have accessed the SMOS memory without the system stops us. As I have already mentioned above, the access to I/O ports is protected under Windows NT. Executing IN or OUT instruction in user-mode will cause process termination. But we have touched them. How it can be? Well, it becomes possible due to the giveio driver.

3.5 Giveio device driver

3.5.1 Giveio driver source code

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  giveio - Kernel Mode Driver
;
;  Demonstrate direct port I/O access from a user mode
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     E Q U A T E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

IOPM_SIZE equ 2000h     ; sizeof I/O permission map

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local status:NTSTATUS
local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE
local kvpi:KEY_VALUE_PARTIAL_INFORMATION
local pIopm:PVOID
local pProcess:LPVOID

    invoke DbgPrint, $CTA0("giveio: Entering DriverEntry")

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

    lea ecx, oa
    InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

    invoke ZwOpenKey, addr hKey, KEY_READ, ecx
    .if eax == STATUS_SUCCESS

        push eax
        invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
                               KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
        pop ecx

        .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )

            invoke DbgPrint, $CTA0("giveio: Process ID: %X"), \
                                dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data

            ; Allocate a buffer for the I/O permission map

            invoke MmAllocateNonCachedMemory, IOPM_SIZE
            .if eax != NULL
                mov pIopm, eax

                lea ecx, kvpi
                invoke PsLookupProcessByProcessId, \
                        dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
                .if eax == STATUS_SUCCESS

                    invoke DbgPrint, $CTA0("giveio: PTR KPROCESS: %08X"), pProcess

                    invoke Ke386QueryIoAccessMap, 0, pIopm
                    .if al != 0

                        ; I/O access for 70h port

                        mov ecx, pIopm
                        add ecx, 70h / 8
                        mov eax, [ecx]
                        btr eax, 70h MOD 8
                        mov [ecx], eax

                       ; I/O access for 71h port

                        mov ecx, pIopm
                        add ecx, 71h / 8
                        mov eax, [ecx]
                        btr eax, 71h MOD 8
                        mov [ecx], eax

                        invoke Ke386SetIoAccessMap, 1, pIopm
                        .if al != 0
                            invoke Ke386IoSetAccessProcess, pProcess, 1
                            .if al != 0
                                invoke DbgPrint, $CTA0("giveio: I/O permission is successfully given")
                            .else
                                invoke DbgPrint, $CTA0("giveio: I/O permission is failed")
                                mov status, STATUS_IO_PRIVILEGE_FAILED
                            .endif
                        .else
                            mov status, STATUS_IO_PRIVILEGE_FAILED
                        .endif
                    .else
                        mov status, STATUS_IO_PRIVILEGE_FAILED
                    .endif
                    invoke ObDereferenceObject, pProcess
                .else
                    mov status, STATUS_OBJECT_TYPE_MISMATCH
                .endif
                invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
            .else
                invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
                mov status, STATUS_INSUFFICIENT_RESOURCES
            .endif
        .endif
        invoke ZwClose, hKey
    .endif

    invoke DbgPrint, $CTA0("giveio: Leaving DriverEntry")

    mov eax, status
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:make

set drv=giveio

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

The driver's code is based on well-known example (giveio) by Dale Roberts. I have decided it will be appropriate to mention here.

3.5.2 I/O permission bit map

Our driver changes the I/O permission bit map (IOPM) that allows the process free access to the I/O ports. Refer to this doc for details http://www.intel.com/design/intarch/techinfo/pentium/PDF/inout.pdf .

Each process has its own I/O permission bit map, thus access to the individual I/O ports can be granted to the individual process. Each bit in the I/O permission bit map corresponds to the byte I/O port. If this bit is set, the access to the corresponding port is forbidden, if it is clear the process may access this I/O port. Since the I/O address space consists of 64K individually addressable 8-bit I/O ports, the maximum IOPM size is 2000h bytes.

The purpose of the TSS is to save the state of the processor during task or context switches. For performance reasons, Windows NT does not use this architectural feature and maintains one base TSS that all processes share. This means that IOPM is also shared. So any changes to it are not private for particular process but are system-wide.

There are some undocumented functions in the ntoskrnl.exe to manipulate with the IOPM: Ke386QueryIoAccessMap and Ke386SetIoAccessMap.

Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

Ke386QueryIoAccessMap copies current IOPM by the size of 2000h bytes from TSS to the memory buffer pointed to by pIopm parameter.

Parameter | Description
dwFlag | 0 - Fill memory buffer with 0FFh values (all bits are set - access denied); 1 - Copy current IOPM from TSS to the memory buffer.
pIopm | Points to the memory buffer to receive current IOPM. The buffer size must be not less than 2000h bytes.

If the function succeeds it returns nonzero value in al (not eax) register. If the function fails, the al (not eax) register is clear.

Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID

Ke386SetIoAccessMap copies specified IOPM by the size of 2000h from the memory buffer pointed to by pIopm parameter to TSS.

Parameter | Description
dwFlag | It can be only 1 - permits copying. Any other value causes the function to return an error.
pIopm | Points to the memory buffer containing IOPM. The buffer size must not be less than 2000h bytes.

If the function succeeds it returns nonzero value in al (not eax) register. If the function fails, the al (not eax) register is clear.

After the IOPM has been copied to the TSS, the IOPM offset pointer must be adjusted to point to new IOPM. This is done by using Ke386IoSetAccessProcess - one more very useful and also completely undocumented function from the ntoskrnl.exe.

Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD

Ke386IoSetAccessProcess permits/forbids using IOPM for the process.

Parameter | Description
pProcess | Points to the KPROCESS structure (I'll tell you later about it).
dwFlag | 0 - Denies access to the I/O ports, setting offset to the IOPM abroad of TSS segment; 1 - Allows access to the I/O ports, setting offset to the IOPM in limits TSS segment ( 88h).

If the function succeeds it returns nonzero value in al (not eax) register. If the function fails, the al (not eax) register is clear.

By the way, almost all functions from ntoskrnl are prefixed. By this prefix you can determine to which system major executive component the function belongs to.

To denote internal functions used a variation of the prefix - either the first letter of the prefix followed by an i (for internal) or the full prefix followed by a p (for private) or f (fastcall). For example, Ke represents kernel functions, Psp refers to internal process support functions, Mm represents the Memory Manager functions and so on.

The first parameter to Ke386IoSetAccessProcess function is a pointer to the process object, which is KPROCESS structure (defined in \include\w2k\w2kundoc.inc. I have specially prefixed file name with "w2k", since undocumented structures differ across Windows NT versions. So, to use this include file in the driver intended for XP is not the good idea). Ke386IoSetAccessProcess sets the IopmOffset member of KPROCESS structure to the appropriate value.

3.5.3 Reading info from the registry

Since we have to call Ke386IoSetAccessProcess, we need the pointer to the process object. It can be obtained by many different ways. I have chosen the most simple - using the process identifier. For this reason in the DateTime.exe we get the current process identifier and we put it into the registry. In this case we use the registry for passing parameter between the user-mode code and the kernel-mode device driver. Since the DriverEntry routine runs in the System process context, there is no way to find out, what exactly process actually has started the driver.

The second parameter - pusRegistryPath - to the DriverEntry routine is a pointer to the driver registry subkey. And we use it to get process identifier from the registry.

Now let's see how all that works.

    lea ecx, oa
    InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL

We have to initialize OBJECT_ATTRIBUTES structure (\include\w2k\ntdef.inc) before we can call ZwOpenKey. I've used InitializeObjectAttributes macro for this, but you'd better do it manually since InitializeObjectAttributes macro is not always behaves as expected. And you can do it like this:

lea ecx, oa
xor eax, eax
assume ecx:ptr OBJECT_ATTRIBUTES
mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
mov [ecx].RootDirectory, eax                       ; NULL
push pusRegistryPath
pop [ecx].ObjectName
mov [ecx].Attributes, eax                          ; 0
mov [ecx].SecurityDescriptor, eax                  ; NULL
mov [ecx].SecurityQualityOfService, eax            ; NULL
assume ecx:nothing

ZwOpenKey returns registry key handle in hKey. Second parameter specifies the access rights required to the key. And you should remember that ecx register contains the pointer to the initialized object attributes of the key being opened.

    invoke ZwOpenKey, addr hKey, KEY_READ, ecx
    .if eax == STATUS_SUCCESS

        push eax
        invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
                               KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
        pop ecx

ZwQueryValueKey returns the entry value for an open registry key. And we use it to get process identifier from the registry. Second parameter points to the name of the value entry for which the data is requested. I've used $CCOUNTED_UNICODE_STRING macro to define UNICODE_STRING structure (4 bites aligned) and the unicode-string itself. If you don't like macros, you can use the common way:

 usz dw 'U', 'n', 'i', 'c', 'o', 'd', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 0
 us UNICODE_STRING {sizeof usz - 2, sizeof usz, offset usz}

But I never liked this way, so I wrote the following macros: COUNTED_UNICODE_STRING, $COUNTED_UNICODE_STRING, CCOUNTED_UNICODE_STRING, $CCOUNTED_UNICODE_STRING (\Macros\Strings.mac).

Third parameter specifies the type of information requested. KeyValuePartialInformation is a symbolic constant (defined in \include\w2k\ntddk.inc). The fourth and fifth parameters are the pointer to KEY_VALUE_PARTIAL_INFORMATION structure and its size respectively. In the Data member of this structure we'll get our process identifier. The last parameter is the pointer to the number of bytes returned. We should also reserve the place for it on the stack before calling ZwQueryValueKey.

3.5.4 Give user-mode process access to the I/O ports

        .if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )
            invoke MmAllocateNonCachedMemory, IOPM_SIZE
            .if eax != NULL
                mov pIopm, eax

If ZwQueryValueKey successfully returns, we allocate a virtual address range of noncached and cpu cache-aligned memory for IOPM by calling the MmAllocateNonCachedMemory.

                lea ecx, kvpi
                invoke PsLookupProcessByProcessId, \
                        dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
                .if eax == STATUS_SUCCESS
                    invoke Ke386QueryIoAccessMap, 0, pIopm

By passing the process identifier to the PsLookupProcessByProcessId we get in pProcess the pointer to our process object. Ke386QueryIoAccessMap copies IOPM in the memory buffer.

                    .if al != 0

                        mov ecx, pIopm
                        add ecx, 70h / 8
                        mov eax, [ecx]
                        btr eax, 70h MOD 8
                        mov [ecx], eax

                        mov ecx, pIopm
                        add ecx, 71h / 8
                        mov eax, [ecx]
                        btr eax, 71h MOD 8
                        mov [ecx], eax

                        invoke Ke386SetIoAccessMap, 1, pIopm
                        .if al != 0
                            invoke Ke386IoSetAccessProcess, pProcess, 1
                            .if al != 0
                            .else
                                mov status, STATUS_IO_PRIVILEGE_FAILED
                            .endif
                        .else
                            mov status, STATUS_IO_PRIVILEGE_FAILED
                        .endif
                    .else
                        mov status, STATUS_IO_PRIVILEGE_FAILED
                    .endif

Now we'll clear the bits corresponding to 70h and 71h I/O ports, write modified IOPM back and call Ke386IoSetAccessProcess to allow I/O access.

                    invoke ObDereferenceObject, pProcess
                .else
                    mov status, STATUS_OBJECT_TYPE_MISMATCH
                .endif

The previous call to PsLookupProcessByProcessId had incremented a reference count for the process object. The Object Manager increments a reference count for an object each time it gives out a pointer to it; when kernel-mode components have finished using the pointer, they call the Object Manager to decrement the object's reference count. The system also increments the reference count when it increments the handle count (gives someone a handle to the object), and likewise decrements the reference count when the handle count decrements (someone closes some handle), because a handle is also a reference to the object that must be tracked. So even after an object's open handle counter reaches 0, the object's reference count might remain positive, indicating that the operating system is still using the object. Sooner or later the reference count also drops to 0. When this happens the Object Manager deletes the object from the memory.

We decrement the process object's reference count by calling ObDereferenceObject. And it returns to its previous state.

                invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
            .else
                invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
                mov status, STATUS_INSUFFICIENT_RESOURCES
            .endif
        .endif
        invoke ZwClose, hKey
    .endif

Calling MmFreeNonCachedMemory we release memory buffer, and close registry handle by calling ZwClose.

The work is done - the driver is not needed any more. Since it returns an error code, the system removes it from the memory. But now, the user-mode process has direct access to two I/O ports.

In this example we have accessed the CMOS memory just for instance. We could beep with the system speaker, as in the previous driver beeper.sys. I leave it to you. But remember you must not use the privileged instructions like cli and sti. And you must not call functions from the hal.dll also, since they are in the kernel-mode address space. The only thing you can do is to give yourselves an access to all 65535 I/O ports using this code-snippet:

invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
    mov pIopm, eax
    invoke RtlZeroMemory, pIopm, IOPM_SIZE
    lea ecx, kvpi
    invoke PsLookupProcessByProcessId, \
                dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
    .if eax == STATUS_SUCCESS
        invoke Ke386SetIoAccessMap, 1, pIopm
        .if al != 0
            invoke Ke386IoSetAccessProcess, pProcess, 1
        .endif
        invoke ObDereferenceObject, pProcess
    .endif
    invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
    mov status, STATUS_INSUFFICIENT_RESOURCES
.endif

Always keep in mind that playing the system speaker and reading the CMOS memory is harmless enough. But accessing some other I/O ports can be potentially dangerous, basically since you can't synchronize it in the user-mode.

3.6 A couple words about driver debugging

Now we can talk about the driver debugging in more details. As I've already mentioned, you should better use SoftICE as a debugger.

To force a breakpoint in the driver's code we have to execute a CPU breakpoint instruction. You achieve it by placing an "int 3" somewhere in your driver's code. The "int 3" raises an exception that is handled by the kernel debugger like SoftICE. But before you use it make sure you have enabled INT 3 handling. Use the I3HERE command to specify that any interrupt 3 instruction pops up SoftICE. Check SoftICE Command Reference for details. And always bear in mind that not handled breakpoint exception in kernel will cause a bug check resulting in the BSOD! So, don't forget to type "i3here on" before starting the driver. In latter SoftICE versions the int 3 handling is set by default for the kernel-mode addresses.

I repeatedly called the DbgPrint function in the giveio driver's code. This function causes a string to be printed onto the debugger command window. SoftICE is perfectly understands it. You can also use DebugView by Mark Russinovich ( www.sysinternals.com ) to monitor debug output.


The I/O Subsystem

4.1 I/O Manager
4.2 The control program for the VirtToPhys driver
4.2.1 Control program source code
4.2.2 Device object
4.2.3 Driver object
4.2.4 Symbolic link object
4.2.5 File object
4.2.6 Communicating with the device
4.2.7 I/O Control Codes
4.2.8 Data exchange
4.2.9 Cleanup
Source code: KmdKit\examples\simple\VirtToPhys

I/O Manager

Unlike the user-mode, where we can call functions from some dll directly, simply using its address, in the kernel-mode such scenario would be extremely dangerous in the view of the system stability. Therefore, the system provides the intermediary to communicate with the kernel-mode. Such intermediary is an I/O Manager, which is one of the I/O subsystem's component. The I/O Manager connects applications and system components with devices, and defines the infrastructure that supports device drivers.

Very simplified scheme of how the I/O Manager interacts with the user-mode applications and the device drivers is given on figure 4-1.

Figure 4-1. Simplified I/O subsystem architecture

From the above figure follows that absolutely all calls from the user-mode applications to the devices and, therefore, to the device drivers are under control of the I/O Manager.

The user-mode code is forced to order the I/O operation to the device. Only and exactly the device. The driver must create some device (or devices) to control. In our case this device is virtual one. Of course, creating the device doesn't mean some new real device will be created. It just means some new object will be created in the memory (namely device object) representing a physical or logical device on the system and describing its characteristics.

Creating the device the driver tells the I/O Manager: "Here is the device for me to control. If you will receive some I/O request to this device, send it to me, and I'll take care about the rest." The driver only knows how to handle I/O requests to its device(s). The only responsibility of the I/O Manager is to create and direct the I/O request to the appropriate device driver. And the user-mode code does not (and should not) know at all, which driver services a particular device(s).

4.2 The control program for the VirtToPhys driver

4.2.1 Control program source code

Strictly speaking this code combines the service control program responsible for registration and starting the driver, and the client program to communicate with the device.

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; VirtToPhys.asm - Driver Control Program for VirtToPhys driver
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib

include \masm32\include\winioctl.inc

include \masm32\Macros\Strings.mac

include common.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                        C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      BigNumToString
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

BigNumToString proc uNum:UINT, pszBuf:LPSTR

; This function accepts a number and converts it to a
; string, inserting commas where appropriate.

local acNum[32]:CHAR
local nf:NUMBERFMT

    invoke wsprintf, addr acNum, $CTA0("%u"), uNum

    and nf.NumDigits, 0
    and nf.LeadingZero, FALSE
    mov nf.Grouping, 3
    mov nf.lpDecimalSep, $CTA0(".")
    mov nf.lpThousandSep, $CTA0(" ")
    and nf.NegativeOrder, 0
    invoke GetNumberFormat, LOCALE_USER_DEFAULT, 0, addr acNum, addr nf, pszBuf, 32

    ret

BigNumToString endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

start proc uses esi edi

local hSCManager:HANDLE
local hService:HANDLE
local acModulePath[MAX_PATH]:CHAR
local _ss:SERVICE_STATUS
local hDevice:HANDLE

local adwInBuffer[NUM_DATA_ENTRY]:DWORD
local adwOutBuffer[NUM_DATA_ENTRY]:DWORD
local dwBytesReturned:DWORD

local acBuffer[256+64]:CHAR
local acThis[64]:CHAR
local acKernel[64]:CHAR
local acUser[64]:CHAR
local acAdvapi[64]:CHAR

local acNumber[32]:CHAR

    invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
    .if eax != NULL
        mov hSCManager, eax

        push eax
        invoke GetFullPathName, $CTA0("VirtToPhys.sys"), \
                                sizeof acModulePath, addr acModulePath, esp
        pop eax

        invoke CreateService, hSCManager, $CTA0("VirtToPhys"), \
                              $CTA0("Virtual To Physical Address Converter"), \
                              SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER, \
                              SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath, \
                              NULL, NULL, NULL, NULL, NULL

        .if eax != NULL
            mov hService, eax

            ; Driver's DriverEntry procedure will be called
            invoke StartService, hService, 0, NULL
            .if eax != 0

                ; Driver will receive I/O request packet (IRP) of type IRP_MJ_CREATE
                invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
                                0, NULL, OPEN_EXISTING, 0, NULL

                .if eax != INVALID_HANDLE_VALUE
                    mov hDevice, eax

                    lea esi, adwInBuffer
                    assume esi:ptr DWORD
                    invoke GetModuleHandle, NULL
                    mov [esi][0*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
                    mov [esi][1*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
                    mov [esi][2*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
                    mov [esi][3*(sizeof DWORD)], eax

                    lea edi, adwOutBuffer
                    assume edi:ptr DWORD
                    ; Driver will receive IRP of type IRP_MJ_DEVICE_CONTROL
                    invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
                                            esi, sizeof adwInBuffer, \
                                            edi, sizeof adwOutBuffer, \
                                            addr dwBytesReturned, NULL

                    .if ( eax != 0 ) && ( dwBytesReturned != 0 )

                        invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
                                                  addr acModulePath, sizeof acModulePath

                        lea ecx, acModulePath[eax-5]
                        .repeat
                            dec ecx
                            mov al, [ecx]
                        .until al == '\'
                        inc ecx
                        push ecx

                        CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod

                        invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
                        pop ecx
                        invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
                                         [esi][0*(sizeof DWORD)], \
                                         [edi][0*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
                                         [esi][1*(sizeof DWORD)], \
                                         [edi][1*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
                                         [esi][2*(sizeof DWORD)], \
                                         [edi][2*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
                                         [esi][3*(sizeof DWORD)], \
                                         [edi][3*(sizeof DWORD)], addr acNumber

                        invoke wsprintf, addr acBuffer, \
                                         $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
                                         addr acThis, addr acKernel, addr acUser, addr acAdvapi

                        assume esi:nothing
                        assume edi:nothing
                        invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \
                                           MB_OK + MB_ICONINFORMATION
                    .else
                        invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \
                                           MB_OK + MB_ICONSTOP
                    .endif
                    ; Driver will receive IRP of type IRP_MJ_CLOSE
                    invoke CloseHandle, hDevice
                .else
                    invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
                .endif
                ; DriverUnload proc in our driver will be called
                invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss
            .else
                invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
            .endif
            invoke DeleteService, hService
            invoke CloseServiceHandle, hService
        .else
            invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
        .endif
        invoke CloseServiceHandle, hSCManager
    .else
        invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, \
                           MB_OK + MB_ICONSTOP
    .endif

    invoke ExitProcess, 0

start endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end start

Not considering the code that prepares the input data sending to the device and the code responsible for formatting and displaying the output from the device, there is a few new stuff here - only three calls: CreateFile, DeviceIoControl and CloseHandle. All these functions accept the device (I repeat, not the driver) handle as an argument.

4.2.2 Device object

After loading, VirtToPhys driver creates the named device "devVirtToPhys" (The "dev" prefix is not necessary, but I have added it with purpose - I'll tell you why below).

The device name is placed in the Object Manager namespace. Object Manager is the system component responsible for creating, deleting, protecting, and tracking objects. By convention, device objects are placed in the \Device directory, inaccessible by the applications using the Win32 API.

To traverse the namespace maintained by the Object Manager use my Windows Object Explorer (WinObjEx) ( http://www.wasm.ru/ ) or Object Viewer by Mark Russinovich ( http://www.sysinternals.com/ ).

To view objects created by VirtToPhys on your computer, simply run VirtToPhys.exe, but do not close a dialog window.

Figure 4-2. devVirtToPhys device object in the object manager namespace

Figure 4-3. devVirtToPhys device object properties

4.2.3 Driver object

VirtToPhys driver object (I did not use any prefixes in the name) is placed in the \Driver directory.

Figure 4-4. VirtToPhys driver object in the object manager namespace

4.2.4 Symbolic link object

Internal device names can't be used in Win32 applications (all directories, except for "\BaseNamedObjects" and "\??", are invisible to user programs) - instead, the device name must appear in a special directory in the Object Manager's namespace, "\??". This directory contains a symbolic links to the real, internal device names. Device drivers are responsible for creating links in this directory so their devices will be accessible to Win32 applications.

So, our driver needs to make it possible for user-mode code to open the device object, thus it have to create a symbolic link in the "\??" directory which points to the device object in the "\Device" directory. Thereafter, when the caller wants to have the device handle, the I/O Manager can find the device object directly.

By the way, you can examine or even change these links from user-mode with the Win32 QueryDosDevice and DefineDosDevice functions.

Having opened "\??" directory you will see, that it teems with symbolic links. Prior to Windows NT 4, this directory was named \DosDevices; it was renamed to "\??" for performance reasons - that name places first in the alphabetical order.

For backward compatibility in the Object Manager's namespace root directory there is the "\DosDevices" link to the "\??" directory.

The driver VirtToPhys creates the symbolic link "slVirtToPhys" to the device "devVirtToPhys" in the "\??" directory, which value is the string "\Device\devVirtToPhys". Here I've used the prefix "dev".

Figure 4-5. slVirtToPhys symbolic link object in the object manager namespace

Figure 4-6. slVirtToPhys symbolic link object properties

I've added the prefixes only to distinguish the different kinds of objects. The point is to show it's not necessary for the device name and the symbolic link name to be (though, usually it's the case) coincided with the name of the driver. The important thing here is that the symbolic link name should specify the valid device name. And one more important point - there can not be two objects with the same name in a single object directory, just as there are no two files with identical name in the same file system directory.

Thus, upon exit from the StartService function we have three new objects: the driver "\Driver\VirtToPhys", the device "\Device\devVirtToPhys" and the symbolic link to the device " \??\slVirtToPhys".

If you still remember, in the second part of this doc, I have promised to tell what "\??", preceding the driver's file path like "\??\C:\masm32\..." is. So, "\??\C:" is a symbolic link to the internal device named "\Device\HarddiskVolume1", or the first volume on the first hard drive in the system.

4.2.5 File object

Let's get back to our source code. After the driver is started, we want to call it somehow. To accomplish this, we only need to open a file handle to the driver calling CreateFile:

The description of the CreateFile takes a a lot of space in the documentation. But only the small piece of that info is concerned about the device drivers.

CreateFile proto stdcall     lpFileName:LPCSTR,            dwDesiredAccess:DWORD, \
                             dwShareMode:DWORD,            lpSecurityAttributes:LPVOID, \
                             dwCreationDistribution:DWORD, dwFlagsAndAttributes:DWORD, \
                             hTemplateFile:HANDLE

Despite its name, this function creates or opens existing (many Create* functions work this way) object, but not just a file. Microsoft definitely should name it CreateObject. The device can appear as an object.

Parameter | Description
lpFileName | Points to a null-terminated string that specifies the name of the device to open. The symbolic link name pointing to the device object, to be exact.
dwDesiredAccess | Specifies the type of access to the device. We will need two values:
 | GENERIC_READ | Specifies read access. Data can be read from the device;
 | GENERIC_WRITE | Specifies write access. Data can be written to the device.
 | These flags can be combined together.
dwShareMode | Set of bit flags that specifies how the device can be shared. Three values can be useful to us:
 | 0 | The device cannot be shared. Subsequent open operations on the device will fail, until the handle is closed. Though the documentation stands this, I could not achieve it using 0.
 | If you need to share the device, use the following values:
 | FILE_SHARE_READ | Subsequent open operations on the device will succeed only if read access is requested;
 | FILE_SHARE_WRITE | Subsequent open operations on the device will succeed only if write access is requested.
lpSecurityAttributes | Pointer to a SECURITY_ATTRIBUTES. Since any special protection is not necessary for us and we don't need returned handle to be inherited by child processes we simply specify NULL here.
dwCreationDistribution | Specifies the action to take on files that exist, and which action to take when files do not exist. For devices, this parameter must be always OPEN_EXISTING.
dwFlagsAndAttributes | Specifies the attributes and flags. This parameter will be always equal 0.
hTemplateFile | Specifies a handle to a template file. For devices, this parameter must be always NULL.

If CreateFile successfully creates or opens the specified device, a handle to a device is returned; otherwise, INVALID_HANDLE_VALUE is returned.

Most Windows functions that return a handle return NULL when they are unsuccessful. CreateFile, however, returns INVALID_HANDLE_ VALUE defined as -1.

We call CreateFile as follows.

                invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
                                0, NULL, OPEN_EXISTING, 0, NULL

I hope everything is clear with the last five parameters. The second parameter is a combination of flags GENERIC_READ + GENERIC_WRITE, since we intend both to send the data to the device and to receive the results of its work.

Let's examine the first parameter. It is a pointer to the symbolic link name, in the form "\\.\slVirtToPhys". The "\\.\" is a Win32-defined alias for the local computer. The CreateFile is the wrap around the other function NtCreateFile (realized in \%SystemRoot%\System32\ntdll.dll), which in turn accesses the corresponding system service (don't confuse to Win32 service processes).

System service is an entry point into the kernel from environment subsystems. A system service dispatch is triggered as a result of executing an int 2Eh (Windows NT/W2K) or sysenter (Windows XP/2003) instruction on x86 processors. Executing these instruction results in a trap that causes the executing thread to transition into kernel-mode and enter the system service dispatcher.

NtCreateFile substitutes an alias for the local computer "\\.\" with the "\??" (thus "\\.\slVirtToPhys" turns to "\?? \slVirtToPhys") and calls the kernel's ObOpenObjectByName function. Through the symbolic link ObOpenObjectByName finds the "\Device\devVirtToPhys" object and returns the pointer to it (thus the symbolic link visible from the user-mode code is used by the Object Manager for compilation in the internal device name). Using this pointer NtCreateFile creates the new file object representing the device and returns its handle.

The operating system abstracts all I/O requests as operations on a virtual file, hiding the fact that the target of an I/O operation might not be a file-structured device. The driver converts the requests from requests made to a virtual file to hardware-specific requests. This abstraction generalizes an application's interface to devices. All data that is read or written is regarded as a simple stream of bytes directed to these virtual files.

Before CreateFile returns, the I/O Manager creates IRP the type of IRP_MJ_CREATE and sends it to the driver for processing. The driver-defined routine that is responsible for processing this type of IRP will execute in the same thread context as the initiator of the I/O requests (the caller of the CreateFile) at IRQL = PASSIVE_LEVEL. If that driver-defined routine successfully returns, the Object Manager creates a handle for the file object in the process's handle table and the handle propagates back through the calling chain, finally reaching the application as a return parameter from CreateFile.

The newly created file object is the executive object and does not get into the Object Manager namespace. You can use Process Explorer utility by Mark Russinovich ( http://www.sysinternals.com ) to explore such objects.

Figure 4-7. File object

Figure 4-8. File object properties

Let me summarize. So, "\\.\slVirtToPhys" turns to the symbolic link "\??\slVirtToPhys" and finally used to find the appropriate device "\Device\devVirtToPhys". From the device object DEVICE_OBJECT is fetched out which driver is responsible for managing this device. Then I/O Manager sends IRP_MJ_CREATE request directly to this driver. This way the driver knows that some code tries to get the access to its device. If the driver wants to grant the access it returns success. Now the Object Manager creates a handle for the virtual file object representing the device and returns it to the user-mode code.

Handles and symbolic links serve as indirect pointers to system resources; this indirection keeps application programs from fiddling directly with system data structures.

4.2.6 Communicating with the device

                .if eax != INVALID_HANDLE_VALUE
                    mov hDevice, eax

If CreateFile returned valid device handle we save it in hDevice variable. Now we are able to communicate with the device calling ReadFile, WriteFile, and DeviceIoControl. The DeviceIoControl is the universal function to communicate with the devices. Here is its prototype:

DeviceIoControl proto stdcall hDevice:HANDLE,         dwIoControlCode:DWORD, \
                              lpInBuffer:LPVOID,      nInBufferSize:DWORD, \
                              lpOutBuffer:LPVOID,     nOutBufferSize:DWORD, \
                              lpBytesReturned:LPVOID, lpOverlapped:LPVOID

The DeviceIoControl accepts even more parameters than CreateFile, but it's simple enough.

Parameter | Description
hDevice | Handle to the device;
dwIoControlCode | -Control code that indicates what control operation to perform; We'll discuss how to define these codes a bit further on.
lpInBuffer | Pointer to a buffer that contains the data required to perform the operation. This parameter can be NULL if the dwIoControlCode parameter specifies an operation that does not require input data;
nInBufferSize | Specifies the size, in bytes, of the buffer pointed to by lpInBuffer;
lpOutBuffer | Pointer to a buffer that receives the operation's output data. This parameter can be NULL if the dwIoControlCode parameter specifies an operation that does not produce output data;
nOutBufferSize | Specifies the size, in bytes, of the buffer pointed to by lpOutBuffer;
lpBytesReturned | Pointer to a variable that receives the size, in bytes, of the data stored into the buffer pointed to by lpOutBuffer;
lpOverlapped | Pointer to an OVERLAPPED structure. This structure is required to help control an asynchronous operation. As we only want to call our driver synchronously (the DeviceIoControl will not return until the appropriate driver's routine will complete), we pass NULL.

4.2.7 I/O Control Codes

The device driver can be considered as a package of kernel-mode functions. I/O Control Code defines which function will be called. The dwIoControlCode argument to DeviceIoControl is used for this purpose. It indicates the control operation we want to perform and how it should be performed.

The control code is a 32-bit numeric constant that can be defined using the CTL_CODE macro that's part of both the winioctl.inc and the ntddk.inc include files.

Figure 4-9. I/O Control Code Layout

Bit field Description

DeviceType

The device type (16 bits) indicates the type of the device that implements this control operation.

Values in the range 0 - 7FFFh are reserved by Microsoft. Values in the range 8000h - 0FFFFh are available for developers of new kinds of kernel-mode drivers.

In \include\w2k\ntddk.inc you can find a set of FILE_DEVICE_XXX symbolic constants which values are from the range reserved by Microsoft. We will use FILE_DEVICE_UNKNOWN. However you can define another FILE_DEVICE_XXX.

Access

The access code (2 bits) indicates the access rights an application needs to its device handle to issue this control operation.

As this field is only two bits long, we have four possibilities here:

FILE_ANY_ACCESS (0)

Maximum access rights. The driver will carry out the requested operation for any caller that has a handle for its device.

FILE_READ_ACCESS (1)

Read access rights. With this required access, the device driver transfers data from the device to memory buffer.

FILE_WRITE_ACCESS (2)

Write access rights. With this required access, the device driver transfers data from memory buffer to its device.

FILE_READ_ACCESS or FILE_WRITE_ACCESS (3)

Both read and write access rights. With this required access, the device driver transfers data between memory buffer and the device.

Function

The function code (12 bits) indicates precisely which control operation this code describes.

It can take any value in the range 800h - 0FFFh for private I/O control codes. Values in the range 0 - 7FFh are reserved by Microsoft for public I/O control codes.

Method

The buffering method (2 bits) indicates how the I/O Manager will handle the input and output buffers supplied by the application.

This field is two bits long, so four values can be used as one of the following system-defined constants:

METHOD_BUFFERED (0)

buffered I/O;

METHOD_IN_DIRECT (1)

direct I/O;

METHOD_OUT_DIRECT (2)

METHOD_NEITHER (3)

neither I/O.

We'll talk about buffer management in more details later. Now the important thing is the buffered method is most safe, since the system takes care about buffers handling, resulting in an overhead of the memory copy operation. But drivers commonly use buffered I/O when callers transfer requests smaller than one page (4 KB), because the copy operation of small buffers matches the overhead of the memory lock performed by direct I/O. And we use buffered method in VirtToPhys driver.

You can form I/O control code manually, but it's much more convenient to use a macro CTL_CODE, that offers a mechanism to generate IOCTL values. Here it is:

CTL_CODE MACRO DeviceType:=<0>, Function:=<0>, Method:=<0>, Access:=<0>
    EXITM %(((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Function) SHL 2) OR (Method))
ENDM

As I already have said the CTL_CODE macro is defined both in the winioctl.inc, which included in the source code of the service control program, and in the ntddk.inc, included in the driver's source code.

Since we use NUM_DATA_ENTRY, DATA_SIZE constants and IOCTL_GET_PHYS_ADDRESS I/O control code both in the service control program and in the driver, they placed in separate include file common.inc. Thus all changes in this file will be mirrored in the both source codes.

NUM_DATA_ENTRY         equ 4
DATA_SIZE              equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)

4.2.8 Data exchange

Now let's return to the driver's source code.

                    lea esi, adwInBuffer
                    assume esi:ptr DWORD
                    invoke GetModuleHandle, NULL
                    mov [esi][0*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
                    mov [esi][1*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
                    mov [esi][2*(sizeof DWORD)], eax
                    invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
                    mov [esi][3*(sizeof DWORD)], eax

Here we fill adwInBuffer buffer with the virtual addresses to be converted.

                    lea edi, adwOutBuffer
                    assume edi:ptr DWORD
                    invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
                                            esi, sizeof adwInBuffer, \
                                            edi, sizeof adwOutBuffer, \
                                            addr dwBytesReturned, NULL

And by calling DeviceIoControl we pass the buffer to the driver, that has to convert each virtual address to physical one.

                    .if ( eax != 0 ) && ( dwBytesReturned != 0 )

                        invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
                                                  addr acModulePath, sizeof acModulePath

                        lea ecx, acModulePath[eax-5]
                        .repeat
                            dec ecx
                            mov al, [ecx]
                        .until al == '\'
                        inc ecx
                        push ecx

                        CTA0 "%s \t%08Xh\t%08Xh   ( %s )\n", szFmtMod

                        invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
                        pop ecx
                        invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
                                         [esi][0*(sizeof DWORD)], \
                                         [edi][0*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
                                         [esi][1*(sizeof DWORD)], \
                                         [edi][1*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
                                         [esi][2*(sizeof DWORD)], \
                                         [edi][2*(sizeof DWORD)], addr acNumber

                        invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
                        invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
                                         [esi][3*(sizeof DWORD)], \
                                         [edi][3*(sizeof DWORD)], addr acNumber

                        invoke wsprintf, addr acBuffer, \
                                         $CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
                                         addr acThis, addr acKernel, addr acUser, addr acAdvapi

                        assume esi:nothing
                        assume edi:nothing
                        invoke MessageBox, NULL, addr acBuffer, $CTA0("Modules Base Address"), \
                                           MB_OK + MB_ICONINFORMATION
                    .else
                        invoke MessageBox, NULL, $CTA0("Can't send control code to device."), NULL, \
                                           MB_OK + MB_ICONSTOP
                    .endif

If DeviceIoControl successfully returns, dwBytesReturned is equal to number of bytes we have in adwOutBuffer buffer filled by the driver. Now our task is simple. We have to format derived info and show it to our user. I'm sure you smart enough to understand it by yourself what is going on here. The escape sequences used in $CTA0 are the common one (see \Macros\Strings.mac for details).

Figure 4-10. The output of VirtToPhys.exe

4.2.9 Cleanup

                    invoke CloseHandle, hDevice

Now we only have to close opened device handle. At this point I/O Manager sends two IRPs to device driver. Firstly it is IRP_MJ_CLEANUP, telling the driver that its device handle is about to close. And then IRP_MJ_CLOSE, telling the driver that its device handle have been closed. By the way, you can prevent closing your device handle by returning error code from the routine responsible for handling IRP_MJ_CLEANUP request. The driver-defined routines responsible for processing these types of IRP will execute in the same thread context as the initiator of the I/O requests (the caller of the CloseHandle) at IRQL = PASSIVE_LEVEL.

We'll talk in the next part about how the driver handles IRP.

To make the drivers work under previous builds of Windows NT you need to change "\??" to "\DosDevices" and recompile the driver, since, as I have already mentioned, prior to Windows NT 4 "\??" directory was named "\DosDevices".


The full-function driver

5.1 The driver's source code
5.2 Driver name and symbolic link name
5.3 Writing DriverEntry Routine
5.3.1 Creating Virtual Device
5.3.2 Creating Symbolic Link
5.3.3 Announcing the Dispatch Routines
5.3.4 Cleanup
5.3.5 New objects are here
5.4 I/O Dispatch Routines
5.5 Dispatch Routine for IRP_MJ_CREATE and IRP_MJ_CLOSE
5.6 Calling conventions
5.7 Memory buffer management
5.7.1 Buffered I/O
5.7.2 Direct I/O
5.7.3 Neither I/O
5.8 Dispatch Routine for IRP_MJ_DEVICE_CONTROL
5.9 Memory Address Translation
5.10 DriverUnload Routine
5.11 How to compile
5.12 Adding resources
5.13 A little more words about debugging
Source code: KmdKit\examples\simple\VirtToPhys

5.1 The driver's source code

Now it's time to take a look at full-function driver's source code. Here it is:

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
; VirtToPhys - Kernel Mode Driver
;
;  Translates virtual addres to physical address
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
include \masm32\include\w2k\w2kundoc.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

include ..\common.inc

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                      C O N S T A N T S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.const
CCOUNTED_UNICODE_STRING    "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING    "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                            C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                    GetPhysicalAddress
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

GetPhysicalAddress proc dwAddress:DWORD

    mov eax, dwAddress
    mov ecx, eax

    shr eax, 22
    shl eax, 2

    mov eax, [0C0300000h][eax]

    .if ( eax & (mask pde4kValid) )
        .if !( eax & (mask pde4kLargePage) )
            mov eax, ecx
            shr eax, 10
            and eax, 1111111111111111111100y
            add eax, 0C0000000h
            mov eax, [eax]

            .if eax & (mask pteValid)
                and eax, mask ptePageFrameNumber

                and ecx, 00000000000000000000111111111111y
                add eax, ecx
            .else
                xor eax, eax
            .endif
        .else
            and eax, mask pde4mPageFrameNumber
            and ecx, 00000000001111111111111111111111y
            add eax, ecx
        .endif
    .else
        xor eax, eax
    .endif

    ret

GetPhysicalAddress endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                   DispatchCreateClose
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    assume eax:ptr _IRP
    mov [eax].IoStatus.Status, STATUS_SUCCESS
    and [eax].IoStatus.Information, 0
    assume eax:nothing

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                     DispatchControl
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DispatchControl proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

local status:NTSTATUS
local dwBytesReturned:DWORD

    and dwBytesReturned, 0

    mov esi, pIrp
    assume esi:ptr _IRP

    IoGetCurrentIrpStackLocation esi
    mov edi, eax
    assume edi:ptr IO_STACK_LOCATION

    .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
        .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )

            mov edi, [esi].AssociatedIrp.SystemBuffer
            assume edi:ptr DWORD

            xor ebx, ebx
            .while ebx < NUM_DATA_ENTRY

                invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]

                mov [edi][ebx*(sizeof DWORD)], eax
                inc ebx
            .endw

            mov dwBytesReturned, DATA_SIZE
            mov status, STATUS_SUCCESS
        .else
            mov status, STATUS_BUFFER_TOO_SMALL
        .endif
    .else
        mov status, STATUS_INVALID_DEVICE_REQUEST
    .endif

    assume edi:nothing

    push status
    pop [esi].IoStatus.Status

    push dwBytesReturned
    pop [esi].IoStatus.Information

    assume esi:nothing

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, status
    ret

DispatchControl endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverUnload
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverUnload proc pDriverObject:PDRIVER_OBJECT

        invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

        mov eax, pDriverObject
        invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

    ret

DriverUnload endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              D I S C A R D A B L E   C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code INIT

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local status:NTSTATUS
local pDeviceObject:PVOID

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
                                             0, FALSE, addr pDeviceObject

    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
        .if eax == STATUS_SUCCESS
            mov eax, pDriverObject
            assume eax:PTR DRIVER_OBJECT
            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],           offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],            offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],   offset DispatchControl
            mov [eax].DriverUnload,                                          offset DriverUnload
            assume eax:nothing
            mov status, STATUS_SUCCESS
        .else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

    mov eax, status
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=VirtToPhys

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj

del %drv%.obj
move %drv%.sys ..

echo.
pause

5.2 Driver name and symbolic link name

Let's start with the definition of UNICODE_STRING structures, describing the device and symbolic link names. As I've already mentioned, kernel likes to work with strings in this format.

Inside of absolutely all drivers sources, both assembly and C, I meet the same senseless sequence like this:

.const
uszDeviceName          dw "\", "D", "e", "v", "i", "c", "e", "\", "D", "e", "v", "N", "a", "m", "e", 0
uszSymbolicLinkName    dw "\", "?", "?", "\", "D", "e", "v", "N", "a", "m", "e", 0

.code
DriverEntry proc . . .
. . .
local usDeviceName:UNICODE_STRING
local usSymbolicLinkName:UNICODE_STRING
. . .
    invoke RtlInitUnicodeString, addr usDeviceName, offset uszDeviceName
    invoke RtlInitUnicodeString, addr usSymbolicLinkName, offset uszSymbolicLinkName

The purpose of the RtlInitUnicodeString is to measure unicode-string and to fill UNICODE_STRING structure in. Since the unicode-strings in this code are defined statically, i.e. never will be changed, it is possible to fill UNICODE_STRING structure in at a link time. It is easier, more visual and saves a few bytes (8 bytes for UNICODE_STRING structure + maximum 3 bytes for alignment against the minimum 14 bytes for RtlInitUnicodeString call). That's why I don't like this way. Thus I use CCOUNTED_UNICODE_STRING macro for this. And all above code will turn into two elegant lines.

CCOUNTED_UNICODE_STRING "\\Device\\DevName", usDeviceName,       4
CCOUNTED_UNICODE_STRING "\\??\\DevName",     usSymbolicLinkName, 4

Having agreed with me you can define your driver and symbolic link names like this:

.const
CCOUNTED_UNICODE_STRING    "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING    "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4

On the earlier releases of Windows NT there is no "\??" directory in the Object Manager namespace. In this case it is necessary to change "\??" to "\DosDevices". In the later Windows releases it will also work, since for backward compatibility there is a symbolic link "\DosDevices" in root directory of the Object Manager namespace, which points to "\??".

5.3 Writing DriverEntry Routine

Every kernel-mode driver has to expose a routine whose name is DriverEntry (you can give it any name you like, by the way), which initializes driver-wide data structures. The I/O Manager calls the DriverEntry routine when it loads the driver. DriverEntry routine runs at IRQL = PASSIVE_LEVEL, which means it can access paged system resources. DriverEntry runs in the System process context.

Before we go further, pay attention at this line:

.code INIT

All code marked like this is directed into the INIT section because it's not needed once the driver finishes initializing. The code inside the INIT section can be discarded as soon as the driver returns from its DriverEntry. It's up to the system to decide when to discard it.

It has no big sense for our tiny driver because its sections are 32-bytes aligned (we pass /align:32 key to the linker). Thus all its sections occupy only one page. And it will not be discarded at all even if we mark with "INIT" some lines. Previous Windows NT drivers had large DriverEntry routine that had to create device objects, locate resources, configure devices, and so on. In this case using of this feature is lead to significant memory savings. So if your DriverEntry is big enough it is meaningful to place it in a separate section marked as "INIT". I have already told about this section in third part.

    mov status, STATUS_DEVICE_CONFIGURATION_ERROR

Assume that driver initialization will fail. If it happens, this error code returns to the system, and user-mode call StartService will also fail.

5.3.1 Creating Virtual Device

    invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
                                             0, FALSE, addr pDeviceObject

Since the main driver purpose is to control some device, physical, virtual, or logical, we firstly have to create such device (virtual one in our case). We achieve this by calling IoCreateDevice that is used to create and initialize a device object (see DEVICE_OBJECT structure) for use by the driver. And here is its prototype:

IoCreateDevice proto stdcall DriverObject:PDRIVER_OBJECT, DeviceExtensionSize:DWORD, \
                             DeviceName:PUNICODE_STRING,  DeviceType:DEVICE_TYPE, \
                             DeviceCharacteristics:DWORD, Exclusive:BOOL, \
                             DeviceObject: PDEVICE_OBJECT
Parameter Description

DriverObject

Points to the driver object (DRIVER_OBJECT structure). Each driver receives a pointer to its driver object as a parameter to its DriverEntry routine;

DeviceExtensionSize

Specifies the driver-determined number of bytes to be allocated for the device extension of the device object. The internal structure of the device extension is driver-defined.

It has no sense to use it in our simple driver.

DeviceName

Optionally points to a buffer containing a zero-terminated Unicode string that names the device object. The string must be a full path name. Full path here means not a path to some file on HDD, but the path to object as it appears in Object Manager namespace.

And this parameter is mandatory for us. We should create the named device, otherwise we can't create the symbolic link, and the user-mode can't get access to our device;

The device name must be unique of course.

DeviceType

Specifies one of the system-defined FILE_DEVICE_XXX constants indicating the type of device or the driver-defined value for a new type of device.

We use FILE_DEVICE_UNKNOWN.

DeviceCharacteristics

Specifies additional information about the driver's device.

We have nothing like this. So it will be 0.

Exclusive

Indicates whether the device object represents an exclusive device. That is, only one handle at a time can send I/O requests to the corresponding device object.

Last time I said that I did not manage to get exclusive access to the device using dwShareMode parameter of the CreateFile. It can be done with Exclusive.

We don't need to own our device exclusively. So we pass FALSE.

DeviceObject

Points to the newly created device object (DEVICE_OBJECT structure) if the call succeeds.

If next call to IoCreateSymbolicLink will fail we must remove our device object from the system. So, we save pointer to device object returned by IoCreateDevice in local variable pDeviceObject for future use.

We need this pointer also by unloading the driver. But at that point we can obtain it from the driver object itself.

You can save this pointer in a global variable or somewhere else. But I'm just don't want to create ?data section for this purpose only.

5.3.2 Creating Symbolic Link

    .if eax == STATUS_SUCCESS
        invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName

If new device was created successfully we have to make it visible to the Win32 subsystem by creating a symbolic link (you have already know what this is). To create a symbolic link we call IoCreateSymbolicLink. This function takes an existing device name and a symbolic link name (both passed as UNICODE_STRING data types).

5.3.3 Announcing the Dispatch Routines

        .if eax == STATUS_SUCCESS
            mov eax, pDriverObject
            assume eax:PTR DRIVER_OBJECT
            mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],           offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],            offset DispatchCreateClose
            mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],   offset DispatchControl

If the symbolic link was successfully created we are at the next step.

Each driver object contains an array of function pointers to dispatch routines specific to the I/O request. Each driver must set at least one dispatch entry point in this array for the IRP_MJ_XXX requests which the driver handles. Any driver can set as many separate dispatch entry points as the IRP_MJ_XXX codes for the driver to handle. For example, if you want to receive the notice that the system is being shut down, you must "announce" the dispatch routine for such request. And you do that by placing the appropriate dispatch routine address into the IRP_MJ_SHUTDOWN slot of the MajorFunction table of the driver object. If you don't need to process such request you simply do nothing because the I/O Manager fills the entire MajorFunction table of the driver object with the pointers to the system internal routine IopInvalidDeviceRequest, which returns an error to the original caller before calling DriverEntry.

So, it's your responsibility to provide dispatch routines for each I/O function code you want to process.

We have to handle three types of I/O request packet in our driver. Every kernel-mode driver must support the function code IRP_MJ_CREATE since this code is generated in response to the Win32 CreateFile call. Without support for this code, Win32 applications would have no way to obtain a handle to the device. Similarly, the IRP_MJ_CLOSE must also be supported to handle the Win32 CloseHandle call. And IRP_MJ_DEVICE_CONTROL allows for extended requests from user-mode clients through the Win32 DeviceIoControl call.

Ňčď çŕďđîńŕ | Description
IRP_MJ_CREATE | I/O Manager sends it when a user-mode code has requested a handle for the file object that represents the target device object by calling CreateFile function.
IRP_MJ_DEVICE_CONTROL | I/O Manager sends it when a user-mode code has called the DeviceIoControl function.
IRP_MJ_CLOSE | I/O Manager sends it when the handle of the file object that represents the target device object has been released by calling CloseHandle function.

We handle IRP_MJ_CREATE and IRP_MJ_CLOSE in one DispatchCreateClose routine. I'll tell you a bit later about this.

In ntddk.inc, among others, you can find IRP_MJ_XXX codes that can be of interest for us:

IRP_MJ_CREATE               equ 0
. . .
IRP_MJ_CLOSE                equ 2
IRP_MJ_READ                 equ 3
IRP_MJ_WRITE                equ 4
. . .
IRP_MJ_DEVICE_CONTROL       equ 0Eh
. . .
IRP_MJ_CLEANUP              equ 12h

All IRP_MJ_XXX codes are listed in ntddk.inc. Each IRP_MJ_XXX code is the index in MajorFunction array. The preceding code snippet fills three slots of the MajorFunction array.

            mov [eax].DriverUnload,                                          offset DriverUnload

The purpose of DriverUnload function is to clean up after any global initialization DriverEntry might have done. If we want to dynamically unload the driver we have to supply the pointer to unload routine. This routine will be called by the system when the user-mode calls ControlService with SERVICE_CONTROL_STOP.

            assume eax:nothing
            mov status, STATUS_SUCCESS

If we have safely reached this point the driver was successfully initialized. So we report success to the system by returning STATUS_SUCCESS.

5.3.4 Cleanup

        .else
            invoke IoDeleteDevice, pDeviceObject
        .endif
    .endif

If the call to IoCreateSymbolicLink returns an error, we should release any allocated resources. So, we have to delete device object created by previous call to IoCreateDevice. And by calling IoDeleteDevice we remove device object from the system. If you have allocated some other resources you also have to return it back to the system of course.

Please always remember you must keep track of the memory you've allocated and any other allocated resources in order to release it when it's no longer needed. You are in the kernel-mode and must do all duty work by yourself. No one else will do that for you!

    mov eax, status
    ret

We return the current status code to the system. If it's STATUS_SUCCESS, the driver remains in the memory and the I/O Manager will route IRP to it. If it has any other value, the driver is removed.

5.3.5 New objects are here

Thus, after successful DriverEntry completion three new objects appear in the system: the driver "\Driver\VirtToPhys", the device "\Device\devVirtToPhys" and the symbolic link "\??\slVirtToPhys" to the device.

  • The driver object represents an individual driver in the system.

    • From this object the I/O Manager obtains the address of each driver's dispatch routine.

  • The device object represents a device on the system and describes its characteristics.

    • Via this object the I/O Manager obtains the pointer to the driver object that manages this device.

  • The file object, representing device object for user-mode.

    • Using this object the I/O Manager obtains the pointer to the representing device object.

  • The symbolic link that is visible to user-mode.

    • The symbolic link is used by the Object Manager.

Figure 5-1 shows the main interrelations between these objects. This scheme will help you to understand a further material more thoroughly.

Figure 5-1. The main interrelations between the driver, device and file object.

5.4 I/O Dispatch Routines

The I/O Manager invokes Dispatch routines in response to user-mode or kernel-mode requests. In the case of monolithic or highest-level driver you are guaranteed that Dispatch routines run in the same thread context as the initiator of the I/O requests. Like the driver's DriverEntry routine, Dispatch routines run at IRQL = PASSIVE_LEVEL, which means they can access paged system resources.

Each Dispatch routine is declared as follows:

DispatchRoutine proto stdcall pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

Parameter | Description
pDeviceObject | Pointer to the device object (DEVICE_OBJECT structure). If the driver serves some devices, it can determine, who is addressee of the IRP.
pIrp | Pointer to IRP (_IRP structure) describing I/O request. The I/O Manager creates an IRP describing the I/O request and sends its pointer to the device driver in pIrp parameter. It's up to the device driver how to handle this IRP.

Such uniform interface that Dispatch routines present allows the I/O Manager to call any driver without requiring any special knowledge of its structure or internal details.

5.5 Dispatch Routine for IRP_MJ_CREATE and IRP_MJ_CLOSE

Why such different types of IRP are processed by only one Dispatch routine? In our simple driver the only thing we have to do by processing IRP_MJ_CREATE and IRP_MJ_CLOSE requests is to mark IRP as completed.

If separate processing for create and close requests is required you must implement separate DispatchCreate and DispatchClose routines.

As I have already mentioned IRP_MJ_CREATE is generated in response to the CreateFile call. Without support of this code, Win32 applications would have no way to obtain a handle to the device. The mutual close IRP_MJ_CLOSE must also be supported to handle the CloseHandle call.

DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

    mov eax, pIrp
    assume eax:ptr _IRP
    mov [eax].IoStatus.Status, STATUS_SUCCESS
    and [eax].IoStatus.Information, 0
    assume eax:nothing

We fill the I/O status block determining a condition of the IRP.

The Information member of the I/O status block is set to zero indicating the handle to the device can be opened for the create request. Information field has no meaning for the close request. This member may have some other meaning for the other IRP types.

The Status member indicates whether the CreateFile or CloseHandle call returns without an error. So we fill it with STATUS_SUCCESS.

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, STATUS_SUCCESS
    ret

DispatchCreateClose endp

Now we should call IoCompleteRequest indicating the driver has completed IRP processing and returns it to the I/O Manager. And the returning by DispatchCreateClose STATUS_SUCCESS indicates that the device is ready to accept another I/O requests.

The first parameter of the oCompleteRequest tells the I/O Manager which IRP is to be completed. And the second determines a system-defined constant of runtime priority increase of the thread that requested the operation. The driver should compensate any thread that possibly waits for a device operation by giving a priority boost. For example, for sound devices DDK recommends to use IO_SOUND_INCREMENT which is equal to 8.

In our case we simply use IO_NO_INCREMENT equal to zero, which means the runtime priority of current thread remains the same.

IofCompleteRequest is a fastcall-function (notice the 'f' symbol in the prefix). There is also its stdcall counterpart IoCompleteRequest. But I use fastcall for the educational purposes.

5.6 Calling conventions

Three calling conventions are used in the Windows NT kernel APIs: __stdcall, __cdecl and __fastcall. Unfortunately, the last one is not supported by masm compiler.

The __fastcall calling convention specifies the first two DWORD arguments are passed in ECX and EDX registers; all other arguments are passed right to left. Called function pops the arguments from the stack.

Fastcall-function's names are mangled (decorated) as follows: @ sign is prefixed to the name, @ sign appended followed by a decimal number that indicates the count of bytes passed to the function as parameters. For example, IofCompleteRequest is decorated like this:

@IofCompleteRequest@8

The decorated name above means that it is a fastcall-function, its exported name is IofCompleteRequest and it takes two DWORD arguments.

This function is defined in \include\w2k\ntoskrnl.inc as follows (pay no attention on the SYSCALL):

EXTERNDEF SYSCALL @IofCompleteRequest@8:PROC
IofCompleteRequest TEXTEQU <@IofCompleteRequest@8>

To make it easier to call fastcall-functions I wrote this macro:

fastcall MACRO api:REQ, p1, p2, px:VARARG

local arg

    ifnb <px>
        % for arg, @ArgRev( <px> )
            push arg
        endm
    endif

    ifnb <p1>

        ifdifi <p1>, <ecx>
            mov ecx, p1
        endif

        ifnb <p2>
            ifdifi <p2>, <edx>
                mov edx, p2
            endif
        endif

    endif

    call api

ENDM

Here is the simplified version of macro. I've placed it in \include\w2k\ntddk.inc. There is no such macro in original ntddk.h, of course.

5.7 Memory buffer management

The I/O Manager performs three types of buffer management:

  • buffered I/O;

  • direct I/O;

  • neither I/O.

Here we'll examine only usage of DeviceIoControl function for I/O processing. The usage of ReadFile and WriteFile is a bit different. An example of using ReadFile to read the device data you will find in \src\NtBuild.

5.7.1 Buffered I/O

Starting I/O operation, the I/O Manager validates all virtual memory pages spanned by the user's buffer are valid. Then it allocates a nonpaged pool buffer of a size sufficient to hold the user's request.

While creating an IRP the I/O Manager copies the user's buffer data into the allocated buffer and passes its address to the driver in AssociatedIrp.SystemBuffer field of _IRP structure. The size of copied data is stored into Parameters.DeviceIoControl.InputBufferLength field of IO_STACK_LOCATION structure (Tail.Overlay.CurrentStackLocation of _IRP points to this structure and the pointer can be fetched with IoGetCurrentIrpStackLocation macro).

The driver handles the IRP and copies output data (if any) into the very same buffer.

When IRP is marked as completed by calling IofCompleteRequest the I/O Manager copies data from the allocated buffer to the user's buffer and then frees the allocated buffer. The amount of data to copy is placed by the driver in IoStatus.Information field of _IRP structure.

As you can see the I/O Manager copies data twice. Thus buffered I/O is used by slower devices that do not generally handle large data transfers as our VirtToPhys device does.

But this method has one big advantage - I/O Manager solves all possible problems with probable memory access errors by itself. We don't need to take care of it.

5.7.2 Direct I/O

This method is used for direct memory access (DMA).

I not examine in details this type of I/O handling, since it does not applicable in a scope of this doc.

When the I/O Manager creates the IRP, it locks the user's buffer into the memory (makes it nonpaged) which is made it accessible to driver code via an address above 80000000h. The I/O Manager stores a description of this memory in the form of a MemoryDescriptorLlist (MDL) and places pointer to it into the _IRP's MdlAddress field. An MDL specifies the physical memory occupied by the buffer. When the I/O Manager has finished IRP usage, it unlocks the buffer

5.7.3 Neither I/O

The I/O Manager doesn't perform any buffer management in any way. It's up to the discretion of the device driver.

The driver gets the user-mode virtual address of the input buffer in the Type3InputBuffer parameter of the stack location, and the user-mode virtual address of the output buffer in the UserBuffer field of the IRP. Neither address is of any use unless you know you're running in the same process context as the user-mode caller. And as the monolithic device driver's writers we know it for sure.

Also we know the monolithic device driver is always called from user-mode at IRQL = PASSIVE_LEVEL. So, no need to take care about the presence of the user buffer in memory. Memory Manager will do all necessary job if the user buffer is swapped out.

Only one more problem remains - the user-mode code can provide us with wrong buffer address or free it somehow (in the case of multithreaded application) while data transfer is in progress.

We have to foresee such situations and to handle it correctly. Thus the usage of Structured Exception Handling (SEH) is necessary (see example \src\Article4-5\NtBuild). But bear in mind, the SEH in the kernel-mode is in its entirety the same as in user-mode, though you can't handle all exceptions this way. For example, the attempt of divide by zero will result in BSOD even with installed SEH-handler!

5.8 Dispatch Routine for IRP_MJ_DEVICE_CONTROL

Once the driver has announced Dispatch routine for IRP_MJ_DEVICE_CONTROL, the I/O Manager starts passing IRPs directly to the driver code when user-mode client calls DeviceIoControl.

    and dwBytesReturned, 0

If any error will occur, the I/O Manager should not copy anything in the user buffer.

    mov esi, pIrp
    assume esi:ptr _IRP

    IoGetCurrentIrpStackLocation esi
    mov edi, eax
    assume edi:ptr IO_STACK_LOCATION

IoGetCurrentIrpStackLocation macro gives us a pointer to the IRP stack location - the pointer to IO_STACK_LOCATION structure containing some necessary data.

    .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS

We must not handle the receipt of an unrecognized I/O control code.

NUM_DATA_ENTRY         equ 4
DATA_SIZE              equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)

The IOCTL_GET_PHYS_ADDRESS control code we are waiting for is defined in common.inc as well as two constants. This file is included both in the driver and in its client source codes.

        .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )

We check the size of user's input and output buffers. If both are less than required we stop processing.

The OutputBufferLength and InputBufferLength fields of IO_STACK_LOCATION structure correspond to nOutBufferSize and nInBufferSize parameters of DeviceIoControl function.

            mov edi, [esi].AssociatedIrp.SystemBuffer

From the IRP stack location we obtain the pointer to the system buffer. This buffer contains now the data user-mode client have sent to us. In our case this data is four virtual addresses our driver have to translate to physical ones.

            assume edi:ptr DWORD

The compiler should know that edi points to DWORD value. Without this statement we have to use PTR DWORD each time we touch edi.

            xor ebx, ebx
            .while ebx < NUM_DATA_ENTRY

                invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]

                mov [edi][ebx*(sizeof DWORD)], eax
                inc ebx
            .endw

We are repeatedly cycling NUM_DATA_ENTRY times through buffer and for each dword (namely virtual address) we meet call GetPhysicalAddress whose output (namely physical address) is putted back into the buffer at the same place.

            mov dwBytesReturned, DATA_SIZE
            mov status, STATUS_SUCCESS

When we reached this point our job is done. So we put number of processed bytes into dwBytesReturned and indicate success in status.

        .else
            mov status, STATUS_BUFFER_TOO_SMALL
        .endif
    .else
        mov status, STATUS_INVALID_DEVICE_REQUEST
    .endif

If something went wrong status receives an appropriate error code.

    assume edi:nothing

    push status
    pop [esi].IoStatus.Status

Completing the IRP we place in Status field of the status block a current status value. This status code will be translated to the corresponding Win32 error code. Below is the correspondence for three status codes we used:

Nt Status | Win32 Error
STATUS_SUCCESS | NO_ERROR
STATUS_BUFFER_TOO_SMALL | ERROR_INSUFFICIENT_BUFFER
STATUS_INVALID_DEVICE_REQUEST | ERROR_INVALID_FUNCTION

You can call RtlNtStatusToDosError exported by ntdll.dll to translate kernel status to Win32 error. The user-mode applications call GetLastError to determine the cause of the failure.

    push dwBytesReturned
    pop [esi].IoStatus.Information

The Information field of the status block gets number of bytes I/O Manager has to copy into the user's buffer. The caller of DeviceIoControl will receive this value in a variable pointed to by lpBytesReturned.

    assume esi:nothing

    fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT

    mov eax, status
    ret

Call IofCompleteRequest to complete the IRP we have just handled.

And remember we must handle the receipt of even unrecognized I/O control code by setting the I/O status block with an appropriate NTSTATUS value, setting its Information field to zero, and completing the IRP with a PriorityBoost of IO_NO_INCREMENT.

5.9 Memory Address Translation

The kernel-mode code can translate virtual addresses to physical one. And MmGetPhysicalAddress kernel function is for this purpose. The GetPhysicalAddress function I use here does basically the same (not considering Physical Address Extension mode). Unfortunately I have no opportunity to describe in detail what is going on here. Please refer to "Inside Microsoft Windows 2000" by David Solomon and Mark Russinovich. There you will find the detailed description. Here is my commented code:

GetPhysicalAddress proc dwAddress:DWORD

    ; Converts virtual address in dwAddress to corresponding physical address

    mov eax, dwAddress
    mov ecx, eax

    shr eax, 22                               ; (Address >> 22) => Page Directory Index, PDI
    shl eax, 2                                ; * sizeof PDE = PDE offset

    mov eax, [0C0300000h][eax]                ; [Page Directory Base + PDE offset]

    .if ( eax & (mask pde4kValid) )           ; .if ( eax & 01y )
        ; PDE is valid
        .if !( eax & (mask pde4kLargePage) )  ; .if ( eax & 010000000y )
            ; small page (4kB)
            mov eax, ecx
            ; (Address >> 12) * sizeof PTE => PTE offset
            shr eax, 10
            and eax, 1111111111111111111100y
            add eax, 0C0000000h               ; add Page Table Array Base
            mov eax, [eax]                    ; fetch PTE

            .if eax & (mask pteValid)         ; .if ( eax & 01y )
                ; PTE is valid
                ; mask PFN   (and eax, 11111111111111111111000000000000y)
                and eax, mask ptePageFrameNumber

                ; We actually don't need these two lines
                ; because of module base is always page aligned
                and ecx, 00000000000000000000111111111111y  ; Byte Index
                add eax, ecx                  ; add byte offset to physical address
            .else
                xor eax, eax                  ; error
            .endif
        .else
            ; large page (4mB)
            ; mask PFN   (and eax, 11111111110000000000000000000000y)
            and eax, mask pde4mPageFrameNumber
            and ecx, 00000000001111111111111111111111y      ; Byte Index
            add eax, ecx                      ; add byte offset to physical address
        .endif
    .else
        xor eax, eax                          ; error
    .endif

    ret

GetPhysicalAddress endp

GetPhysicalAddress returns the physical address that corresponds to the given virtual address.

5.10 DriverUnload Routine

DriverUnload routine's work is straightforward, as it must delete each symbolic link and device object that has been created. This routine is called whenever the user-mode code calls ControlService with SERVICE_CONTROL_STOP, but only if there are no more opened handles of the device. If at least one open handle exists, device can receive IPR and thus it should remains in memory.

        invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName

        mov eax, pDriverObject
        invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject

The unload routine undoes the work of DriverEntry. Namely calling IoDeleteSymbolicLink we remove symbolic link from the Object Manager namespace and call to the IoDeleteDevice removes the device object itself.

As I have previously mentioned, you are in kernel-mode and must release all allocated resources.

The table 5-1 sums up all you have to know about process context and IRQL of main driver's routines. The info in this table is correct only in the case of monolithic or highest-level driver.

User-mode | Kernel-mode | Process context | IRQL
StartService | DriverEntry | System | PASSIVE_LEVEL
CreateFile | IRP_MJ_CREATE | User-mode caller | PASSIVE_LEVEL
DeviceIoControl | IRP_MJ_DEVICE_CONTROL | User-mode caller | PASSIVE_LEVEL
ReadFile | IRP_MJ_READ | User-mode caller | PASSIVE_LEVEL
WriteFile | IRP_MJ_WRITE | User-mode caller | PASSIVE_LEVEL
CloseHandle | IRP_MJ_CLEANUP, IRP_MJ_CLOSE | User-mode caller | PASSIVE_LEVEL
ControlService,,SERVICE_CONTROL_STOP | DriverUnload | System | PASSIVE_LEVEL

Table 5-1. Correspondence of user-mode functions to the driver's routines

5.11 How to compile

:make

set drv=skeleton

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj

del %drv%.obj
move %drv%.sys ..

echo.
pause

We have already analyzed all that in the third part. I have added the /ignore:4078 option, since we have two sections with the same name but with the different attributes. Otherwise the linker produces warning:

LINK : warning LNK4078: multiple "INIT" sections found with different attributes (E2000020)

5.12 Adding resources

We also put the version resource into the driver image providing the driver's version information. It can be done using a common resource script (see rsrc.rc).

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 1,0,0,0
 PRODUCTVERSION 1,0,0,0
 FILEFLAGSMASK 0x3fL
 FILEFLAGS 0x0L
 FILEOS 0x40004L
 FILETYPE 0x1L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904E4"
        BEGIN
            VALUE "Comments", "Written by Four-F\0"
            VALUE "CompanyName", "Four-F Software\0"
            VALUE "FileDescription", "Kernel-Mode Driver VirtToPhys v1.00\0"
            VALUE "FileVersion", "1, 0, 0, 0\0"
            VALUE "InternalName", "VirtualToPhysical\0"
            VALUE "LegalCopyright", "Copyright © 2003, Four-F\0"
            VALUE "OriginalFilename", "VirtToPhys.sys\0"
            VALUE "ProductName", "Kernel-Mode Driver Virtual To Physical Address Converter\0"
            VALUE "ProductVersion", "1, 0, 0, 0\0"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1200
    END
END

Nothing special here. It's compiled and linked as usual.

5.13 A little more words about debugging

You can obtain very useful information about the driver and its device with the help of SoftICE's driver and device commands. See SoftICE Command Reference for details. Here is how it looks on my machine:

Figure 5-2. The output of driver VirtToPhys

Figure 5-3. The output of device device devVirtToPhys

As you understand, the information SoftICE displays, is obtained from DRIVER_OBJECT and DEVICE_OBJECT structures accordingly. Using this info it is possible easily to find these objects in the memory and set breakpoints on its routines.


System Heaps

KmdTut Logo

System Memory Heaps

6.1 System Memory Heaps
6.2 Allocation from the system pool
Source code: KmdKit\examples\basic\MemoryWorks\SystemModules

Assuming you already grasp the basics, let's review some of the essential base techniques. I'd like to underscore the word "some" because drivers can do a lot of things. Since it is impossible to go further without the knowledge of memory management, let's start with it.

Memory Manager provides to the user processes a rich set of API for memory management, which can be divided into three groups of functions: virtual memory functions, memory-mapped file functions, and heap functions.

Kernel components, including drivers, have more advanced facilities. For example, the driver is able to allocate a contiguous memory range in the physical address space. The function names, Memory Manager provides, begin with the prefix "Mm". In addition, the executive support routines that begin with "Ex" are used to allocate and free memory from the system pools (paged and nonpaged) as well as to manipulate look-aside lists (look-aside lists provide more faster allocation but only predefined fixed block size).

6.1 System Memory Heaps

System memory heaps (have nothing common with user heaps, btw) are represented with two so called memory pools located in the system address space.

  • Nonpaged Pool. Nonpaged Pool pages cannot be paged out to the swap file and, of course, never paged in back. This pool always presents in the physical memory and thus can be accessed at any time (at any IRQL level) without incurring a page fault. One of the reasons the nonpaged pool is required is that any access to its pages never causes the Page Fault. Such errors leads to system crash at IRQL >= DISPATCH_LEVEL;

  • Paged Pool. These pages can be paged in and out. You can use this memory only at IRQL less than DISPATCH_LEVEL.

Both pools are located in the system address space and available from within any process context. There is ExAllocatePoolXXX function set to allocate memory from the system pools and ExFreePool to release.

Before we start using one of them, I'd like to note some fundamental points:

Access to paged out memory at IRQL >= DISPATCH_LEVEL, as I said, leads to system crash.

Keep in mind that if at the moment you are accessing the pageable memory it physically presents no crash happens, even at IRQL >= DISPATCH_LEVEL. But sooner or later the system pages this memory out and your access to it results in system crash.

It is not worth to use nonpaged memory anywhere you like. This resource is more expensive than paged one.

After you have allocated memory from any system pool, no matter what happens with your driver, this memory will not return to the system back until ExFreePool is called. I.e. until the driver explicitly releases allocated memory it will stay resident inside the system even after unloading the driver. As I already told many times, the drivers must explicitly release all allocated resources.

Memory allocated from system pools doesn't zeroed out and could contain garbage left by the last owner. So it is better to zero it before usage.

It is easy to define the memory type (paged or nonpaged) you need. If some code should access the memory at IRQL >= DISPATCH_LEVEL it is definitely should be nonpaged type only. Both the code itself and the data it touches should reside in nonpaged memory. By default the driver is loaded in the nonpaged memory except "INIT" section and sections, whose names starts with "PAGE". If you do not make any actions to change driver's memory attributes (do not call MmPageEntireDriver that makes the driver's image pageable, for example) you should not care about driver itself - it always presents in the memory.

In the previous articles we have discussed the IRQL levels common driver routines (DriverEntry, DriverUnload, DispatchXxx) called at.

DDK gives us information about IRQL each function called at. For example, we will use IoInitializeTimer function in one of the further articles. The description says that it executes by the system when timer event occurs at IRQL = DISPATCH_LEVEL. This means that this routine along any memory it accesses must be nonpaged.

At worst, if you not sure about current IRQL, you determine it calling KeGetCurrentIrql like this:

invoke KeGetCurrentIrql
.if eax < DISPATCH_LEVEL
    ; use any memory
.else
    ; use nonpaged memory only
.endif

6.2 Allocation from the system pool

Let's take a look at very simple driver SystemModules as an example. The main action concentrates inside DriverEntry routine. We'll allocate paged memory (I'm sure, you remember the DriverEntry runs at IRQL = PASSIVE_LEVEL, so it's OK to use paged memory), write something useful to it, release and force the system to unload the driver.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  SystemModules - Allocate memory from the system pool and use it
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.386
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              I N C L U D E  F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\native.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                              D I S C A R D A B L E   C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code INIT

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc uses esi edi ebx pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

local cb:DWORD
local p:PVOID
local dwNumModules:DWORD
local pMessage:LPSTR
local buffer[256+40]:CHAR

    invoke DbgPrint, $CTA0("\nSystemModules: Entering DriverEntry\n")

    and cb, 0
    invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb
    .if cb != 0

        invoke ExAllocatePool, PagedPool, cb
        .if eax != NULL
            mov p, eax

            invoke DbgPrint, \
                   $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"), cb, p

            invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb
            .if eax == STATUS_SUCCESS
                mov esi, p

                push dword ptr [esi]
                pop dwNumModules

                mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2

                invoke ExAllocatePool, PagedPool, cb
                .if eax != NULL
                    mov pMessage, eax

                    invoke DbgPrint, \
                           $CTA0("SystemModules: %u bytes of paged memory allocted at address %08X\n"), \
                           cb, pMessage

                    invoke memset, pMessage, 0, cb

                    add esi, sizeof DWORD
                    assume esi:ptr SYSTEM_MODULE_INFORMATION

                    xor ebx, ebx
                    .while ebx < dwNumModules

                        lea edi, [esi].ImageName
                        movzx ecx, [esi].ModuleNameOffset
                        add edi, ecx

                        invoke _strnicmp, edi, $CTA0("ntoskrnl.exe", szNtoskrnl, 4), sizeof szNtoskrnl - 1
                        push eax
                        invoke _strnicmp, edi, $CTA0("ntice.sys", szNtIce, 4), sizeof szNtIce - 1
                        pop ecx

                        and eax, ecx
                        .if ZERO?
                            invoke _snprintf, addr buffer, sizeof buffer, \
                                    $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \
                                    edi, [esi].Base, [esi]._Size
                            invoke strcat, pMessage, addr buffer
                        .endif

                        add esi, sizeof SYSTEM_MODULE_INFORMATION
                        inc ebx

                    .endw
                    assume esi:nothing

                    mov eax, pMessage
                    .if byte ptr [eax] != 0
                        invoke DbgPrint, pMessage
                    .else
                        invoke DbgPrint, \
                               $CTA0("SystemModules: Found neither ntoskrnl nor ntice.\n")
                    .endif

                    invoke ExFreePool, pMessage
                    invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), pMessage

                .endif
            .endif

            invoke ExFreePool, p
            invoke DbgPrint, $CTA0("SystemModules: Memory at address %08X released\n"), p

        .endif
    .endif

    invoke DbgPrint, $CTA0("SystemModules: Leaving DriverEntry\n")

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=SystemModules

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj

del %drv%.obj

echo.
pause

As a "something useful" let's take a modules list loaded into the system address space (this list includes system modules - ntoskrnl.exe, hal.dll, etc. and device drivers) and try to find ntoskrnl.exe and ntice.sys. We get system modules list calling ZwQuerySystemInformation with SystemModuleInformation information class. You can find description of this function in the Garry Nebbett's book "Windows NT-2000 Native API Reference". Btw, ZwQuerySystemInformation is a unique function. It returns a huge amount of system information of any kind.

I not provide driver control program here. Use KmdManager (included in KmdKit package) or similar utility and check driver debug messages using DebugView ( http://www.sysinternals.com ) or SoftICE console.

    and cb, 0
    invoke ZwQuerySystemInformation, SystemModuleInformation, addr p, 0, addr cb

First of all we should determine how much space information of our interest will take. Calling ZwQuerySystemInformation above showed the way we get STATUS_INFO_LENGTH_MISMATCH (this is quite normal since size of the buffer is zero), but cb variable receives desired buffer size. This way we get buffer size needed. Address of p is needed here for normal functioning of ZwQuerySystemInformation only.

    .if cb != 0
        invoke ExAllocatePool, PagedPool, cb

ExAllocatePool allocates required amount of memory from paged pool (first argument, for nonpaged memory specify NonPagedPool accordingly). ExAllocatePool is even simpler than its user-mode counterpart HeapAlloc. It takes only two arguments: the pool type (paged or nonpaged) and memory size required. Piece of cake!

        .if eax != NULL

If ExAllocatePool returns nonzero value, then it is a pointer to allocated buffer.

Examining debug information notice that buffer address ExAllocatePool returned is multiple of page size. If the size of requested memory is more or equal to page size (in our case this size is noticeably larger than the size of one page) then allocated memory starts from the page boundary.

            mov p, eax
            invoke ZwQuerySystemInformation, SystemModuleInformation, p, cb, addr cb

We call ZwQuerySystemInformation again using pointer to buffer and its size as an arguments.

            .if eax == STATUS_SUCCESS
                mov esi, p

If STATUS_SUCCESS returned our buffer contains the system modules list in the form of SYSTEM_MODULE_INFORMATION (defined in include\w2k\native.inc) structures array.

SYSTEM_MODULE_INFORMATION STRUCT        ;Information Class 11
    Reserved            DWORD   2 dup(?)
    Base                PVOID   ?
    _Size               DWORD   ?
    Flags               DWORD   ?
    Index               WORD    ?
    Unknown             WORD    ?
    LoadCount           WORD    ?
    ModuleNameOffset    WORD    ?
    ImageName           CHAR 256 dup(?)
SYSTEM_MODULE_INFORMATION ENDS

cb variable receives the number of bytes actually returned , but it is useless for us now.

I assume here no new modules appear between two ZwQuerySystemInformation calls. It is rarely happen so highly unlikely. This is acceptable for a learning purpose but you'd better use more safe approach - repeatedly call ZwQuerySystemInformation in a loop increasing the buffer size at every iteration until its size will satisfy the request.

                push dword ptr [esi]
                pop dwNumModules

The very first double word of the buffer contains the number of modules followed immediately by an array of SYSTEM_MODULE_INFORMATION. Keep modules number in the dwNumModules.

                mov cb, (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2
                invoke ExAllocatePool, PagedPool, cb
                .if eax != NULL
                    mov pMessage, eax

We need one buffer more to keep the names of two modules we are looking for and some additional information. We assume (sizeof SYSTEM_MODULE_INFORMATION.ImageName + 100)*2 is enough.

Notice that this time buffer address is not multiple of page size because it size is less than a page size.

                    invoke memset, pMessage, 0, cb

For safety precautions fill the buffer with zero to assure the strings we are going to copy is zero terminated.

                    add esi, sizeof DWORD
                    assume esi:ptr SYSTEM_MODULE_INFORMATION

Skip DWORD containing modules number so esi now points to the first SYSTEM_MODULE_INFORMATION structure.

                    xor ebx, ebx
                    .while ebx < dwNumModules

We walk dwNumModules times through the structures array searching for ntoskrnl.exe and ntice.sys.

On the multiprocessor system ntoskrnl.exe module has the name ntkrnlmp.exe, and on the system with PAE support ntkrnlpa.exe and ntkrpamp.exe respectively. I assume here you are not the lucky one who owns such a system.

                        lea edi, [esi].ImageName
                        movzx ecx, [esi].ModuleNameOffset
                        add edi, ecx

ImageName and ModuleNameOffset fields contain full path to the module and relative offset to the module's name inside the path respectively.

                        invoke _strnicmp, edi, $CTA0("ntoskrnl.exe", szNtoskrnl, 4), sizeof szNtoskrnl - 1
                        push eax
                        invoke _strnicmp, edi, $CTA0("ntice.sys", szNtIce, 4), sizeof szNtIce - 1
                        pop ecx

strnicmp do case-insensitive comparison of two ANSI strings. Third argument is the number of characters to compare. It is not necessary here since names of the modules in the SYSTEM_MODULE_INFORMATION are zero terminated and we can use _stricmp. But I use _strnicmp for a safety reasons.

Btw, ntoskrnl.exe exports a lot of basic string functions - strcmp, strcpy, strlen etc.

                        and eax, ecx
                        .if ZERO?
                            invoke _snprintf, addr buffer, sizeof buffer, \
                                    $CTA0("SystemModules: Found %s base: %08X size: %08X\n", 4), \
                                    edi, [esi].Base, [esi]._Size
                            invoke strcat, pMessage, addr buffer
                        .endif

                        add esi, sizeof SYSTEM_MODULE_INFORMATION
                        inc ebx

                    .endw
                    assume esi:nothing

If any of the above mentioned modules founded, we format the string containing modules names, base address and size using _snprintf (exported by the kernel too) and add it to the buffer. Labels szNtoskrnl and szNtIce used in the sizeof operator. You can switch label and alignment in my string macros - they are detected automatically. You can also use either label or alignment only (see macros\Strings.mac for details).

                    mov eax, pMessage
                    .if byte ptr [eax] != 0
                        invoke DbgPrint, pMessage
                    .else
                        invoke DbgPrint, \
                               $CTA0("SystemModules: Found neither ntoskrnl nor ntice.\n")
                    .endif

Since we zeroed buffer before use the nonzero value in first byte means we have found something.

                    invoke ExFreePool, pMessage
                .endif
            .endif
            invoke ExFreePool, p
        .endif
    .endif

Release the memory allocated from the system pools.

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

Force the system to unload the driver.

As you can see, using the system pools even simpler than using heaps in the user-mode. The only question is to correctly define the pool type.

A lot of ZwXxx functions exported by the user-mode ntdll.dll are just the gates to the kernel-mode equivalents. Notice that number of arguments and their meaning entirely the same. You can make profit of it. Since errors inside the kernel lead to the system crash you can debug your code in the user-mode and then copy it in your driver with very little changes if any. For example, ZwQuerySystemInformation called from the ntdll.dll returns the same information. Using this trick you can avoid a lot of reboots.

Translated by masquer


Lookaside Lists

7.1 Lookaside Lists
7.2 LookasideList driver source code
7.3 Working with Lookaside List
7.4 AddEntry routine
7.5 RemoveEntry routine
Source code: KmdKit\examples\basic\MemoryWorks\LookasideList

7.1 Lookaside Lists

Heap manager manages system and user heaps splitting heap space into blocks of equal size. When a heap allocation query arrives heap manager is trying to choose a free block with appropriate size. This can take some time, of course. If you need a memory blocks with fixed size but you don't know its amount and usage frequency beforehand you should use, for performance reasons, so called lookaside lists, which exists in the kernel mode only. The main difference of lookaside lists from the system pools is that you can allocate memory blocks with fixed and predefined size only. Allocation from the lookaside lists is faster because no need to search free region with suitable size.

When you first come to the lookaside lists the first problem you have to solve apart from the creation lookaside list itself will be management of the memory blocks you wish to allocate from it. Specifically where and how to store the block addresses you will refer and free to. This could be a serious problem since you don't know the quantity of these blocks. There are three structures for resolving such a problems:

  • Singly linked list;

  • S-list, sequenced singly-linked list (a singly linked list modification);

  • Doubly linked list.

We'll examine doubly linked list only as the most universal solution.

The following code may look complicated if you deal with lookaside and doubly linked lists concept for the first time, but anyway it is rather simple.

Both are called lists, but they are completely different things though. The lookaside list is a group of preallocated memory blocks of equal size. Some of the blocks may be in use and some of them are not. System will walk through the list searching for the nearest free block when allocation request arrives. If free block found, the allocation can be satisfied very quickly. Otherwise the system must allocate from paged or nonpaged pool. The system automatically tune the number of freed blocks that lookaside lists store according to how often the allocations from the list occur - the more frequent the allocations, the more blocks are stored on a list. Lookaside lists are automatically reduced in size if they aren't being allocated from.

Double linked list is just a form of data organization. It is convenient to link homogeneous structures in a list and traverse through it. Double linked lists are used by the system intensively for internal structure handling.

7.2 LookasideList driver source code

I was thinking really hard but I was failed to depict sensible and simple example for this article. That's why this driver will act probably senselessly. However this should not prevent you from understanding the concepts or associative and double linked lists.

There will be no driver control program, use KmdManager (included in KmdKit package) or something similar. Use DebugView ( http://www.sysinternals.com ) or SoftICE console to watch driver's debug messages.

;@echo off
;goto make

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;  LookasideList - Merely allocates and releases some fixed-size blocks of memory.
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.486
.model flat, stdcall
option casemap:none

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                 I N C L U D E   F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc

includelib \masm32\lib\w2k\ntoskrnl.lib

include \masm32\Macros\Strings.mac

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                  S T R U C T U R E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

SOME_STRUCTURE STRUCT
    SomeField1    DWORD        ?
    SomeField2    DWORD        ?
    ; . . .                          ; Any other fields come here

    ListEntry    LIST_ENTRY    <>    ; For tracking memory blocks.
                                     ; It can be the first member but
                                     ; to place it into is more common solution.

    ; . . .                          ; Any other fields come here
    SomeFieldX    DWORD        ?
SOME_STRUCTURE ENDS

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                          U N I N I T I A L I Z E D  D A T
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.data?

g_pPagedLookasideList    PPAGED_LOOKASIDE_LIST    ?
g_ListHead               LIST_ENTRY               <>
g_dwIndex                DWORD                    ?

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

.code

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                         AddEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

AddEntry proc uses esi

    invoke ExAllocateFromPagedLookasideList, g_pPagedLookasideList
    .if eax != NULL
        mov esi, eax

        invoke DbgPrint, \
               $CTA0("LookasideList: + Memory block allocated from lookaside list at address %08X\n"), esi

        invoke memset, esi, 0, sizeof SOME_STRUCTURE

        assume esi:ptr SOME_STRUCTURE

        lea eax, g_ListHead
        lea ecx, [esi].ListEntry
        InsertHeadList eax, ecx

        inc g_dwIndex
        mov eax, g_dwIndex
        mov [esi].SomeField1, eax

        invoke DbgPrint, $CTA0("LookasideList: + Entry #%d added\n"), [esi].SomeField1

        assume esi:nothing

    .else
        invoke DbgPrint, $CTA0("LookasideList: Very bad. Couldn't allocate from lookaside list\n")
    .endif

    ret

AddEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       RemoveEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

RemoveEntry proc uses esi

    IsListEmpty addr g_ListHead
    .if eax != TRUE

        lea eax, g_ListHead
        RemoveHeadList eax
        sub eax, SOME_STRUCTURE.ListEntry
        mov esi, eax

        invoke DbgPrint, $CTA0("LookasideList: - Entry #%d removed\n"), \
                            (SOME_STRUCTURE PTR [esi]).SomeField1

        invoke ExFreeToPagedLookasideList, g_pPagedLookasideList, esi

        invoke DbgPrint, \
            $CTA0("LookasideList: - Memory block at address %08X returned to lookaside list\n"), esi
    .else
        invoke DbgPrint, \
            $CTA0("LookasideList: An attempt was made to remove entry from empty lookaside list\n")
    .endif

    ret

RemoveEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;                                       DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

DriverEntry proc uses ebx pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

    invoke DbgPrint, $CTA0("\nLookasideList: Entering DriverEntry\n")

    invoke ExAllocatePool, NonPagedPool, sizeof PAGED_LOOKASIDE_LIST
    .if eax != NULL

        mov g_pPagedLookasideList, eax

        invoke DbgPrint, \
        $CTA0("LookasideList: Nonpaged memory for lookaside list allocated at address %08X\n"), \
        g_pPagedLookasideList

        invoke ExInitializePagedLookasideList, g_pPagedLookasideList, NULL, NULL, \
                                               0, sizeof SOME_STRUCTURE, 'msaW', 0

        invoke DbgPrint, $CTA0("LookasideList: Lookaside list initialized\n")

        lea eax, g_ListHead
        InitializeListHead eax

        invoke DbgPrint, $CTA0("LookasideList: Doubly linked list head initialized\n")

        invoke DbgPrint, $CTA0("\nLookasideList: Start to allocate/free from/to lookaside list\n")

        and g_dwIndex, 0

        xor ebx, ebx
        .while ebx < 5

            invoke AddEntry
            invoke AddEntry

            invoke RemoveEntry

            inc ebx
        .endw

        invoke DbgPrint, $CTA0("\nLookasideList: Free the rest to lookaside list\n")

        .while TRUE

            invoke RemoveEntry

            lea eax, g_ListHead
            IsListEmpty eax
            .if eax == TRUE
                invoke DbgPrint, $CTA0("LookasideList: Doubly linked list is empty\n\n")
                .break
            .endif

        .endw

        invoke ExDeletePagedLookasideList, g_pPagedLookasideList

        invoke DbgPrint, $CTA0("LookasideList: Lookaside list deleted\n")

        invoke ExFreePool, g_pPagedLookasideList

        invoke DbgPrint, \
        $CTA0("LookasideList: Nonpaged memory for lookaside list at address %08X released\n"), \
        g_pPagedLookasideList

    .else
        invoke DbgPrint, \
         $CTA0("LookasideList: Couldn't allocate nonpaged memory for lookaside list control structure")
    .endif

    invoke DbgPrint, $CTA0("LookasideList: Leaving DriverEntry\n")

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

DriverEntry endp

;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

end DriverEntry

:make

set drv=LookasideList

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause

7.3 Working with Lookaside List

    invoke ExAllocatePool, NonPagedPool, sizeof PAGED_LOOKASIDE_LIST
    .if eax != NULL

        mov g_pPagedLookasideList, eax

We allocate nonpaged memory for PAGED_LOOKASIDE_LIST structure, which is used to manage lookaside list and save the pointer into the g_pPagedLookasideList variable. Note that lookaside list itself we use here is pageable, i.e. the memory we get from it can be paged out. The documentation is pretty clear about it.

        invoke ExInitializePagedLookasideList, g_pPagedLookasideList, NULL, NULL, \
                                                0, sizeof SOME_STRUCTURE, 'msaW', 0

ExInitializePagedLookasideList fills the PAGED_LOOKASIDE_LIST structure we allocated on the previous step. Now lookaside list is ready to use.

Note that during initialization we did not specify how many blocks we are require. But how the system knows exactly how much memory it should allocate? Indeed, if memory allocation hasn't been done beforehand the lookaside list will not work faster than common system pool allocation. The point is that initially the system allocates just a few (the quantity is defined by the system itself) blocks. So if we start to allocate from the lookaside list we'll get the pointers to those preallocated memory blocks. Once per second the system adjusts all system lookaside lists calling ExAdjustLookasideDepth. The system will allocate new blocks if it founds free block spare diminished during the adjustment. The number of the extra blocks depends on lookaside list's load, i.e. its allocation frequency. The system tries to adjust lookaside lists more effective way. If we had exhausted all preallocated blocks within adjustment time, the system just uses system pool allocation until the next adjustment. The important thing to understand is that if the allocation speed is too high we have no performance gain comparing the allocation form the system pool. You can estimate the efficiency of the lookaside list using MS Kernel Debugger's command "!lookaside".

kd> !lookaside ed374840

Lookaside "" @ ed374840 "Regm"
    Type     =     0001 PagedPool
    Current Depth  =        2   Max Depth  =        4
    Size           =     1024   Max Alloc  =     4096
    AllocateMisses =        4   FreeMisses =        0
    TotalAllocates =  1319722   TotalFrees =  1319720
    Hit Rate       =       99%  Hit Rate   =      100%

Let's see the lookaside list use efficiency of the RegMon ( http://www.sysinternals.com ) utility. As you can see the efficiency approaches to 100% considering huge amount (above one million) of alloc/free operations. The reason is that RegMon does not keep allocated block for a long time.

        lea eax, g_ListHead
        InitializeListHead eax

Calling InitializeListHead macro we initialize doubly linked list head. Now both LIST_ENTRY's fields contain the pointers to this structure itself. This means doubly linked list is empty (figure 7.1, imgage 1)

Figure. 7-1. This picture allows you visually realize how doubly linked list functions.

        and g_dwIndex, 0

This global variable is only used to place something into SOME_STRUCTURE allocated from lookaside list and to output its value through debug messages.

        xor ebx, ebx
        .while ebx < 5

            invoke AddEntry
            invoke AddEntry

            invoke RemoveEntry

            inc ebx
        .endw

Loop five times. Each pass adds two entries and removes one entry. Each entry represents SOME_STRUCTURE. All allocated structures linked to each other using doubly linked list.

This loop simulates the random allocation from the lookaside list. We assume here we work somehow with allocated entries. For example, we could write a driver intercepting calls of some system service, say ZwOpenKey, and saving information into the allocated memory.

        .while TRUE

            invoke RemoveEntry

            lea eax, g_ListHead
            IsListEmpty eax
            .if eax == TRUE
                .break
            .endif

        .endw

At this point we have some amount of allocated from lookaside list entries linked together in doubly linked lists. We assume these entries are not needed for us anymore.

We call RemoveEntry routine in the endless loop. RemoveEntry removes an entry from the head of a doubly linked list and releases it back to the lookaside list. The loop runs until doubly linked list is empty. Calling IsListEmpty macro checks this condition. IsListEmpty checks to see whether both fields of the doubly linked list head (LIST_ENTRY structure) point to the head itself. At this point we came to the state we have right after InitializeListHead macro call (figure 7-1, imgage 1).

        invoke ExDeletePagedLookasideList, g_pPagedLookasideList

When doubly linked list is empty all entries allocated from lookaside list are returned back as well, since we used doubly linked list to manage allocations from the lookaside list. Now we can delete lookaside list by calling ExDeletePagedLookasideList. It frees any remaining entries in the lookaside list and then removes the list from the system-wide set of active lookaside lists.

        invoke ExFreePool, g_pPagedLookasideList

ExFreePool releases nonpaged memory allocated for PAGED_LOOKASIDE_LIST structure. In case you forget (I didn't) to call ExDeletePagedLookasideList beforehand you will see BSOD since in about a second the system will try to adjust missing lookaside list.

    mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
    ret

Since we have released all allocated resources we can unload the driver.

7.4 AddEntry routine

We call AddEntry when we need a new memory block. It allocates new entry from the lookaside list and adds it into doubly linked list.

    invoke ExAllocateFromPagedLookasideList, g_pPagedLookasideList
    .if eax != NULL
        mov esi, eax

Calling ExAllocateFromPagedLookasideList we get the new memory block pointer and save it in esi. Note we don't tell the system about block size because the size was defined while calling ExInitializePagedLookasideList and is equal to the size of SOME_STRUCTURE.

        invoke memset, esi, 0, sizeof SOME_STRUCTURE

Zeroing allocated block. It doesn't need though.

        assume esi:ptr SOME_STRUCTURE

Now we have new instance of the SOME_STRUCTURE. We should link it to our doubly linked list, which is empty during the first AddEntry run.

        lea eax, g_ListHead
        lea ecx, [esi].ListEntry
        InsertHeadList eax, ecx

Figure 7-1, image 2 depicts a state after the first InsertHeadList macro call. Pay your attention to the second argument of the InsertHeadList macro, which is not an address of the SOME_STRUCTURE but an address of its ListEntry field. I.e. InsertHeadList macro accepts the pointers to the two LIST_ENTRY structures, the first one is the head of doubly linked list and the second one is a structure member we need to link to this doubly linked list. InsertHeadList links the new structure at the head of the doubly linked list (to the right in Figure 7-1). You can use InsertTailList macro to link at the tail.

Both macros produce the same result if you add the first entry to the doubly linked list and after that it will looks like it reflected on the Figure 7-1, image 2.

If doubly linked list is not empty InsertHeadList macro will split the doubly linked list between its head and the entry to the right and place the new entry in between (see Figure 7-1, image 3). InsertTailList macro does the same but at the tail.

Hope everything is pretty clear now.

        inc g_dwIndex
        mov eax, g_dwIndex
        mov [esi].SomeField1, eax

We save newly created entry number in the SomeField1. You can watch the order in which those structures are added/removed in DbgView.

7.5 RemoveEntry routine

RemoveEntry routine is the reciprocal of the AddEntry. It unlinks the entry from the head of the doubly linked list and returns it back to the lookaside list.

    IsListEmpty addr g_ListHead
    .if eax != TRUE

Make sure the doubly linked list is empty.

        lea eax, g_ListHead
        RemoveHeadList eax

RemoveHeadList unlinks the entry from the doubly linked list's head (as you already guess RemoveTailList macro does the same but from the tail. You can also remove any entry using RemoveEntryList macro). At this point extracted from the doubly linked list entry exist on its own and doubly linked list is enclosed to link remaining entries together.

        sub eax, SOME_STRUCTURE.ListEntry
        mov esi, eax

Pay your attention to this point. RemoveTailList/RemoveHeadList/RemoveEntryList return a pointer to the entry (nested LIST_ENTRY structure) that was at the tail/head/middle of the list but not the pointer to unlinked SOME_STRUCTURE structure itself. The macros don't know exact place of the LIST_ENTRY in the structure. And there is no way for them to know about. This is entirely up to you to calculate offset to the ListEntry field in the SOME_STRUCTUREto get the pointer to the structure itself (that's what DDK's CONTAINING_RECORD macro does).

        invoke ExFreeToPagedLookasideList, g_pPagedLookasideList, esi

ExFreeToPagedLookasideList returns the entry to the lookaside list or to paged pool.

Translated by masquer


To Be Continued...

Some more articles (in Russian) can be seen on http://www.wasm.ru/. I don't know whether me or someone else translate it ever.