Using GNU toolchain for Windows kernel-mode drivers
For a long time, I was curious about using GNU toolchain on Windows platforms, especially when boils down to kernel-mode driver development. I like GNU toolchain (binutils, gcc, libstdc++). I use for embedded development, but compiling, and especially linking binaries with GNU ld linker, has always been tricky. Why is it important? Microsoft Visual Studio is an exellent tool, but it kind of obscure what is happening behind the scenes. Having free and opensource software toolchain even on a proprietary platform is a goodthing.
This time, it took me a while to figure out how to link a trivial kernel-mode driver, and this brief post is to document these findings.
Compiling GCC from souces for the mingw-w64 target is a topic for a different post (binutils is easy though). So for these experiments I just used MSYS2 with UCRT64 environment. This painlessly bring you startup libraries (CRT), C standard library (mingw-w64), and compiler support libraries (libgcc and friends) together with libstdc++ if you use C++.
Running kernel-mode drivers on Windows requires a bunch tricks (disabling secure boot, enabling test signing, and installing sysinternals). If you are not familiar with it, read a few introductory chapters from Windows Kernel Programming, or message me in the comments, and I will update this post with some details.
Driver Hellow World
This is a typical “Hellow World!” example for Windows kernel-mode drivers:
#include <ntddk.h>
static void test_driver_unload(PDRIVER_OBJECT driverObject);
NTSTATUS
DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath)
{
UNREFERENCED_PARAMETER(registryPath);
DbgPrint("Sample driver initialized successfullyn");
driverObject->DriverUnload = test_driver_unload;
return STATUS_SUCCESS;
}
void
test_driver_unload(PDRIVER_OBJECT driverObject)
{
UNREFERENCED_PARAMETER(driverObject);
DbgPrint("Driver unload calledn");
}
For tracing purposes, DbgPrint is provided by ntoskrnl.exe. DriverEntry is our entry point to the driver that is called by the kernel. test_driver_unload is our custom callback that should be called when module is unloaded.
Using GNU ld for linkig
Compiling into object code is easy but linkig is tricky. For reference of possilbe options, I used an example from ReactOS project. ReactOS is the only working (sort of) full reverse-engineered NT operating system kernel. It is a cool project. ReactOS Github repo has a collection of build options for kernel-mode drivers that can be carved out of CMake files.
Unfortunately, with options from ReactOs, I could not make my example fully working 64-bit Windows 11 — driver loaded but refused to unload. The next repo that helped me with a list of options was Mingw64 Driver Plus Plus.
Finally, this combination of options provided a loadable and unloadable image:
gcc -std=gnu99 -Wall -Wextra -pedantic -shared -fPIC
-O0 -municode -nostartfiles -nostdlib -nodefaultlibs
-I/ucrt64/include/ddk -Wl,-subsystem,native
-Wl,--exclude-all-symbols,-file-alignment=0x200,-section-alignment=0x1000
-Wl,-entry,DriverEntry -Wl,-image-base,0x140000000
-Wl,--dynamicbase -Wl,--nxcompat -Wl,--gc-sections
-Wl,--stack,0x100000
-o test_driver.sys test_driver.c -lntoskrnl
First time, I was also using -Wl,--wdmdriver options, but it was not helping. This mess can be organized in a Makefile:
TARGET = test_driver
OPT = -O0
CSTANDARD = -std=gnu99
DDK_INCLUDE_PATH = /ucrt64/include/ddk
CC = gcc
LD = ld
RM = rm -f
STRIP = strip
OBJDUMP = objdump -x
CFLAGS += -Wall
CFLAGS += -Wextra
CFLAGS += -pedantic
CFLAGS += -municode
CFLAGS += $(CTANDARD)
CFLAGS += $(OPT)
# Do not link startup, compiler support
# or C standard libraries.
CFLAGS += -nostartfiles
CFLAGS += -nostdlib
CFLAGS += -nodefaultlibs
CFLAGS += -fPIC
CFLAGS += -shared
# Include path to ntddk.h or wdm.h.
CFLAGS += -I$(DDK_INCLUDE_PATH)
LDFLAGS = --exclude-all-symbols
LDFLAGS += --gc-sections
LDFLAGS += --dynamicbase
LDFLAGS += --nxcompat
LDFLAGS += -subsystem=native
LDFLAGS += -file-alignment=0x200
LDFLAGS += -section-alignment=0x1000
LDFLAGS += -image-base=0x140000000
LDFLAGS += --stack=0x100000
LDFLAGS +=-entry=DriverEntry
SRC = $(TARGET).c
OBJ = $(SRC:%.c=%.o)
LIBS = -lntoskrnl
LIBS += -lhal
all: sys strip dump
dump: strip
$(OBJDUMP) $(TARGET).sys
strip: sys
$(STRIP) $(TARGET).sys
sys: $(TARGET).sys
.SECONDARY: $(TARGET).sys
.PRECIOUS: $(OBJ)
$(TARGET).sys: $(OBJ)
$(LD) $(LDFLAGS) -o $@ $(OBJ) $(LIBS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
$(RM) $(TARGET).sys
$(RM) $(SRC:%.c=%.o)
.PHONY: all sys clean strip dump
This Makefile can be found in my Github repo here.
Loading and unloading
On Windows 11, the driver also requires signing (using a Visual Studio tool). This is easy:
PS C:UsersIgor> signtool sign /v /fd sha256 /n WDKTestCert test_driver.sys
The following certificate was selected:
Issued to: WDKTestCert Igor,133660689149334675
Issued by: WDKTestCert Igor,133660689149334675
Expires: Thu Jul 20 16:00:00 2034
SHA1 hash: E76CFF2C68E75A85631906D4BC2F55A6D7B32597
Done Adding Additional Store
Successfully signed: test_driver.sys
Number of files successfully Signed: 1
Number of warnings: 0
Number of errors: 0
After that we can use Service Contorl utility (it is alos easy to write a custom loader based on Service Manager API):
sc create test_driver type= kernel binPath= C:UsersIgortest_driver.sys
which creates a record in HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicestest_driver. After that the driver can be started and stopped (and deleted if necessary):
PS C:UsersIgor> sc start test_driver
SERVICE_NAME: test_driver
TYPE : 1 KERNEL_DRIVER
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
PID : 0
FLAGS :
PS C:UsersIgor> sc stop test_driver
SERVICE_NAME: test_driver
TYPE : 1 KERNEL_DRIVER
STATE : 1 STOPPED
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
The debug output (has to be enabled in the registry) shows tracing messages.

Summary
It is possible to link a simple Windows kernel-mode driver using GNU toolchain (mingw32-w64). For reference, source code for this example can be found in my Github repo here.