📜 ⬆️ ⬇️

We write our first Windows driver

So, after my previous article, I realized that the topic of Windows driver programming is interesting for habrovans, so I’ll continue. In this article, I decided to parse a simple driver program that only does what the debugging message “Hello world!” Writes when the driver starts and “Goodbye!” At the end, and also describe those development tools that we will need to build and run the driver.



So, for starters we give the text of this simple program.
')
  1. // TestDriver.c
  2. #include <ntddk.h>
  3. NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath);
  4. VOID UnloadRoutine (IN PDRIVER_OBJECT DriverObject);
  5. #pragma alloc_text (INIT, DriverEntry)
  6. #pragma alloc_text (PAGE, UnloadRoutine)
  7. NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
  8. {
  9. DriverObject-> DriverUnload = UnloadRoutine;
  10. DbgPrint ( "Hello world! \ N" );
  11. return STATUS_SUCCESS;
  12. }
  13. VOID UnloadRoutine (IN PDRIVER_OBJECT DriverObject)
  14. {
  15. DbgPrint ( "Goodbye! \ N" );
  16. }
* This source code was highlighted with Source Code Highlighter .


So, now we first understand what each instruction does. First of all, we include the header file ntddk.h . This is one of the basic include files in all drivers: it contains the NTSTATUS, PDRIVER_OBJECT, PUNICODE_STRING type declarations , and the DbgPrint functions.

Next comes the declaration of two functions: DriverEntry and UnloadRoutine . I will tell about the first in more detail. So, as dear readers know, every program has an entry point, in C programs this is a function of main or WinMain . In the driver, the role of the entry point is performed by the DriverEntry function, which receives a pointer to the DriverObject structure as well as a pointer to the registry string corresponding to the driver being loaded.

The DriverObject structure contains many fields that define the behavior of a future driver. The most key of them are pointers to so-called called (or callback) functions, that is, functions that will be called when a certain event occurs. We define one of these functions: this is the UnloadRoutine function. A pointer to this function is placed in the field DriverUnload . Thus, when the driver is unloaded, the UnloadRoutine function will first be called. This is very convenient when the driver has some temporary data that should be cleared before shutting down. In our example, this function is needed only to track the fact that the driver has been terminated.

In order to display debugging messages, we use the DbgPrint function, which has a syntax similar to the printf function from the user mode (userspace).

In this simple example, we also used the directives #pragma alloc_text (INIT, DriverEntry) and #pragma alloc_text (PAGE, UnloadRoutine) . Let me explain what they mean: the first places the DriverEntry function in the INIT section, that is, it says that DriverEntry will be executed once and after that the function code can be safely unloaded from memory. The second marks the UnloadRoutine function code as unloaded, i.e. if necessary, the system can move it to the paging file, and then take it from there.

You may think, well, the first directive is understandable, such as optimization and all that, but why do we use the second directive, why should we mark the code as possible to be downloaded to the paging file? Let me clarify this question: each process in the system has such a parameter as IRQL (read more at Interrupt request level for this is the material of a separate article), that is, some parameter responsible for the ability to interrupt the process: the higher the IRQL the less chance of interrupting the process. The capabilities of the process also depend on the IRQL : the higher the IRQL the less the possibilities of the process, this is quite logical, i.e. This approach encourages developers to perform only the most necessary operations with a high IRQL , and all other actions to do at a low. Let us return to the main topic, about why we make it possible for the UnloadRoutine function to upload to the paging file: everything again boils down to optimization: working with the paging file is not available with a high IRQL , and the driver unloading procedure is guaranteed to be performed with a low IRQL , so we specifically specify Hands that the code of the function of unloading the driver can be placed in a swap.

Wow, it seems that the discussion of the code of this seemingly small program is over, now let's figure out how to compile and run our driver.

For this we need:


Now the sequence of actions: first we write two files, one is called MAKEFILE , with such content

##################################################
# DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source
# file to this component. This file merely indirects to the real make file
# that is shared by all the driver components of the Windows NT DDK
#

!INCLUDE $(NTMAKEENV)\makefile.def
##################################################


and the second is called sources and contains the following:

##################################################
TARGETNAME=TestDriver
TARGETTYPE=DRIVER

SOURCES=TestDriver.c
##################################################


These files are needed to build the driver. Yes, I forgot to say that the WDK does not have a built-in development environment, so you need a text editor to type the text of the drivers. For this purpose, you can use both Visual Studio (some even integrate the ability to build drivers from VS), and any other text editor.

Save the driver code to the TestDriver.c file and put it in the same directory as the MAKEFILE and souces files . After that, run the installed build environment (this is a command line with the given environment variables for compiling the driver; it is included in the WDK, and you can start it like this: “Start-> Programs-> Windows Driver Kits-> ....-> Build Environments-> WindowsXP-> Windows XP x86 Checked Build Environment ”). Go to the directory where we put the driver file (I have C: \ Drivers \ TestDriver) using the cd command (my command looks like this: cd C: \ Drivers \ TestDriver) and type the build command.

This command will assemble us the TestDriver.sys driver and put it in the “objchk_wxp_x86 \ i386” folder.

Now we need to run the DbgView program to see the messages that the driver will issue. After starting this program, we need to specify that we want to view messages from the kernel (Capture-> Capture Kernel).

Now we start the KmdManager program, specify the path to our driver (file TestDriver.sys), press the Register button, then the Run button. Now the driver is registered in the system and running. In the DbgView program, we should see our “Hello World!” Message. Now we finish the driver operation with the Stop button and remove the driver registration with the Unregister button. By the way, one more line should appear in DbgView.

So, what we have achieved: we wrote, compiled and launched our first Windows driver! I will only add that when writing complex drivers for debugging, a two-machine configuration is used, when on one computer the driver is being written, and on the other - launching and testing. This is done because an incorrectly written driver can crash the entire system, and there can be a lot of valuable data on it. Often a virtual machine is used as the second computer.

Source: https://habr.com/ru/post/40466/


All Articles