Go from zero to a running kernel driver. This blog covers the essential tools, code, and deployment steps. Learn to write, build, and test a simple Windows driver, demystifying the first steps into kernel programming.

Heads up! This post builds on concepts from our previous blogs, so make sure you're caught up before diving in. and all the following steps from the windows kernel programming.
To get stated with driver development, the following tools must be installed in this order on your machine.
Note:The SDK and WDK versions must match. Follow the guidelines in the WDK download
The template you’ll use in this section is WDM Empty Driver. image down shows what the New Project dialog looks like for this type of driver in Visual Studio 2019.
second in the same initial wizard with Visual Studio 2019 if the Classic Project Dialog extension is installed and enabled. The project in both figures is named “Sample”.
Now it’s time to add a source file. Right-click the Source Files node in Solution Explorer and select Add / New Item… from the File menu. Select a C++ source file and name it Sample.cpp. Click OK to create it.
DriverEntry and Unload RoutinesWhen your driver is loaded, the operating system needs a starting point to execute its code. This entry point is a function called DriverEntry. Think of it as the driver's equivalent of the main() function in a standard application it's the first code to run. The kernel's I/O Manager calls this function to kick things off, making it the ideal place to perform initial setup tasks.
This function is called by a system thread at IRQL PASSIVE_LEVEL.
PASSIVE_LEVEL is the most fundamental and least restrictive execution level within the kernel. Running at this level means the DriverEntry function has the flexibility to perform a wide range of initialization tasks, such as allocating memory or registering other driver functions (like the DriverUnload routine), without interfering with more critical, time-sensitive hardware operations. It's the baseline state where a driver can safely prepare itself for its duties. (We'll explore IRQLs in more detail in future blogs).
DriverEntry has a predefined prototype, shown here:
1NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);PDRIVER_OBJECT DriverObject pointer to the DRIVER_OBJECT the I/O manager created for your driver. It’s how you register callbacks (dispatch routines), set DriverUnload, DriverStartIo, etc. see morePUNICODE_STRING RegistryPath counted UNICODE_STRING containing the registry path to your driver’s Parameters key; use it to read configuration.second we need to include the <ntddk.h> header file provides the necessary definitions for our code, but it still won't compile successfully. By default, the project is configured to treat compiler warnings as errors, which is a good practice for catching potential bugs.
In our current code, the compiler will issue a warning because the DriverObject and RegistryPath parameters are provided to the DriverEntry function but are never used. While you could just delete the parameter names, a more explicit and classic C/C++ solution is to use the UNREFERENCED_PARAMETER macro. This macro signals to both the compiler and other developers that you are intentionally not using a parameter, thus resolving the warning without disabling the "warnings as errors" setting.
Building the project now compiles fine, but causes a linker error. The DriverEntry function must have C-linkage, which is not the default in C++ compilation. Here’s the final version of a successful build of the driver consisting of a DriverEntry function only:
1extern "C" NTSTATUS
2DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
3 UNREFERENCED_PARAMETER(DriverObject);
4 UNREFERENCED_PARAMETER(RegistryPath);
5 return STATUS_SUCCESS;
6 }anything done in the DriverEntry function must be undone Failure to do so creates a leak, which the kernel will not clean up until the next reboot. Drivers can have an Unload routine that is automatically called before the driver is unloaded from memory. Its pointer must be set using the DriverUnload member of the driver object.
DriverObject->DriverUnload = SampleUnload;
The unload routine accepts the driver object (the same one passed to DriverEntry) and returns void. As our sample driver has done nothing in terms of resource allocation in DriverEntry,there is nothing to do in the Unload routine, so we can leave it empty for now:
1void SampleUnload(In PDRIVER_OBJECT DriverObject) {
2UNREFERENCED_PARAMETER(DriverObject);
3}
4 1#include <ntddk.h>
2
3void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
4 UNREFERENCED_PARAMETER(DriverObject);
5}
6
7extern "C" NTSTATUS
8DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
9 UNREFERENCED_PARAMETER(RegistryPath);
10 DriverObject->DriverUnload = SampleUnload;
11 return STATUS_SUCCESS;
12}With the driver compiled, we can now install and load it. Important: Always use a virtual machine for driver testing. This protects your primary machine from potential crashes.
Installing a software driver, just like installing a user-mode service, requires calling the CreateService API with proper arguments, or using a comparable tool. One of the well-known tools for this purpose is Sc.exe (short for Service Control), a built-in Windows tool for managing services. We’ll use this tool to install and then load the driver. Note that installation and loading of drivers is a privileged operation, normally available for administrators.
Open an elevated command window and type the following (the last part should be the path on your
1sc create sample type= kernel binPath= c:\dev\sample\x64\debug\sample.sysNote there is no space between type and the equal sign, and there is a space between the equal sign and kernel; same goes for the second part. to test the installations you can open the registry editor oy regedit.exe and look for driver details at: HKLM\System\CurrentControlSet\Services\Sample
Now we need to load the driver, we can use the same tool Sc.exe with the following command sc start sample the tool use the StartService API to load the driver. But in 64 bit system drivers must be signed or it will fail.
On modern 64-bit systems, Windows requires all drivers to be digitally signed for security. However, signing a driver repeatedly during development isn't practical. A better option is to put the system into "test signing mode," which allows unsigned drivers to be loaded for testing purposes. To do this, run the following command in an elevated (Administrator) Command Prompt:bcdedit /set testsigning on
Note that this command requires a system reboot to take effect.
However, from a red team or offensive security perspective, enabling test mode is not a viable option t's too noisy and requires administrator rights. Instead, an attacker would use stealthier techniques to load a malicious driver:
there is another step we need to do if we want to test our driver. go to Visual Studio 2019 (or earlier) only. in this case you have to set the OS target version.
Once test signing mode is on, and the driver is loaded, this is the output you should see:
to show more details of the sample driver image loaded into the system space
At this point, we can unload the driver using the following command: sc stop sample
to make sure our code is executed let's add a tracing. Drivers can use the DbgPrint function to output printf-style text that can be viewed using the kernel debugger. let's update our code.
1void SampleUnload(PDRIVER_OBJECT DriverObject) {
2 UNREFERENCED_PARAMETER(DriverObject);
3 DbgPrint("Sample driver Unload called\n");
4}
5extern "C" NTSTATUS
6DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
7
8 UNREFERENCED_PARAMETER(RegistryPath);
9
10 DriverObject->DriverUnload = SampleUnload;
11
12 DbgPrint("Sample driver initialized successfully\n");
13
14
15 return STATUS_SUCCESS;
16}Dbgprint has some overhead that you may want to avoid in Release builds. KdPrint is macro that is only compiled in Debug builds and calls the underlying Dbgprint kernel API.
1void SampleUnload(PDRIVER_OBJECT DriverObject) {
2 UNREFERENCED_PARAMETER(DriverObject);
3
4 KdPrint(("Sample driver Unload called
5}
6extern "C" NTSTATUS
7DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
8
9 UNREFERENCED_PARAMETER(RegistryPath);
10
11 DriverObject->DriverUnload = SampleUnload;
12
13 KdPrint(("Sample driver initialized successfully\n"))
14
15
16 return STATUS_SUCCESS;
17}An important detail is that KdPrint is a macro, not a regular function. It's a wrapper around another kernel function called DbgPrint. You'll notice it uses double parentheses, like KdPrint(("<message>")). This is a classic C/C++ macro trick to handle functions that can take a variable number of arguments (like printf). The outer parentheses belong to the macro call, while the inner parentheses group the format string and any optional arguments together, passing them cleanly to DbgPrint internally.
With these statements in place, we would like to load the driver again and see these messages. We’ll use. useful Sysinternals tool named DebugView. but first we need to do the following, starting with Windows Vista, DbgPrint output is not actually generated unless a certain value is in the registry. You’ll have to add a key named Debug Print Filter under.
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager (the key typically does not exist). Within this new key, add a DWORD value named DEFAULT (not the default value that exists in any key) and set its value to 8 (technically, any value with bit 3 set will do)
Once this setting has been applied, run DebugView (DbgView.exe) elevated. In the Options menu,make sure Capture Kernel is selected (or press Ctrl+K). You can safely deselect Capture Win32 and Capture Global Win32, so that user-mode output from various processes does not clutter the display.
Build the driver, if you haven’t already. Now you can load the driver again from an elevated command window (sc start sample). You should see output in DebugView.If you unload the driver, you’ll see another message appearing because the Unload routine was called. (The third output line is from another driver and has nothing to do with our sample driver)
in the second chapter we will cover some kernel programming basics with some sample code.
Published Nov 1, 2025