For a long time, Windows kernel-mode driver development has been synonymous with Microsoft Visual Studio. While Visual Studio is a powerful IDE, understanding the underlying compilation and linking processes can sometimes feel opaque. This leads many developers to seek open-source alternatives. The GNU toolchain, comprising Binutils, GCC, and GNU ld, offers a robust and transparent solution, commonly used in embedded systems. This article details the journey of compiling and, more critically, linking a basic Windows kernel-mode driver using the GNU toolchain, specifically focusing on the challenges and solutions encountered with the ld linker.
Why GNU Toolchain for Windows Drivers?
The primary motivation for using the GNU toolchain on a proprietary platform like Windows is the desire for open-source transparency and to avoid the often-significant overhead associated with larger IDEs like Visual Studio. For developers already familiar with GCC and its ecosystem, it provides a consistent environment.
Setting Up Your Environment
Compiling GCC from source for the mingw-w64 target can be intricate. For this exploration, the most straightforward approach involves using MSYS2. MSYS2, particularly with its UCRT64 environment, provides a hassle-free setup including crucial components:
* Startup libraries (CRT)
* C standard library (mingw-w64)
* Compiler support libraries (libgcc and others)
* libstdc++ for C++ development
Prerequisites for Running Kernel Drivers
Before you can run a custom kernel-mode driver on Windows, a few system configurations are necessary:
* Disabling Secure Boot: Typically required for loading unsigned drivers.
* Enabling Test Signing: Windows must be set to test-signing mode to allow drivers signed with a test certificate.
* Sysinternals: Tools like DebugView from the Sysinternals Suite are invaluable for observing debug output from kernel-mode drivers.
For those new to Windows kernel development, a foundational understanding from resources like “Windows Kernel Programming” by Pavel Yosifovich is highly recommended.
A “Hello World” Kernel Driver
Let’s begin with a minimal “Hello World” example for a Windows kernel-mode driver written in C:
#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 successfully\\n");
driverObject->DriverUnload = test_driver_unload;
return STATUS_SUCCESS;
}
void
test_driver_unload(PDRIVER_OBJECT driverObject)
{
UNREFERENCED_PARAMETER(driverObject);
DbgPrint("Driver unload called\\n");
}
In this code:
* #include <ntddk.h> brings in the necessary Windows Driver Kit definitions.
* DbgPrint is a function provided by ntoskrnl.exe for debugging output.
* DriverEntry is the mandatory entry point for any Windows kernel driver, invoked by the kernel upon loading.
* test_driver_unload is a custom callback function registered with the driver object, which the kernel calls when the driver is being unloaded.
The Nuance of GNU ld Linking
While compiling the C source into an object file (.o) with GCC is straightforward, the linking phase using GNU ld presents the most significant challenge. Achieving a functional, loadable, and unloadable 64-bit Windows 11 driver required careful selection of linker options. References from projects like ReactOS and Mingw64 Driver Plus Plus were instrumental in identifying the correct flags.
The following gcc command demonstrates the essential linking options:
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
Let’s break down some of the critical linker flags (prefixed with -Wl, when passed via gcc):
* -shared -fPIC: Essential for creating a shared library (DLL-like) suitable for kernel modules.
* -nostartfiles -nostdlib -nodefaultlibs: Crucial for kernel development, as we don’t want to link against the standard C runtime or startup code meant for user-mode applications.
* -I/ucrt64/include/ddk: Specifies the include path for WDK headers like ntddk.h.
* -subsystem,native: Informs the linker that this is a native NT executable (kernel-mode driver).
* --exclude-all-symbols: Minimizes the symbol table, reducing binary size.
* -file-alignment=0x200 -section-alignment=0x1000: Specifies memory alignment for sections within the PE file, critical for kernel modules.
* -entry,DriverEntry: Explicitly sets DriverEntry as the entry point for the driver.
* -image-base,0x140000000: Sets the preferred base address for the driver in memory.
* --dynamicbase --nxcompat: Security features (ASLR and DEP compatibility).
* --gc-sections: Enables garbage collection of unused sections, optimizing the binary.
* --stack,0x100000: Sets the default stack size for the driver.
* -lntoskrnl: Links against the ntoskrnl.exe import library, providing access to kernel functions like DbgPrint.
Automating the Build with a Makefile
To streamline the build process, a Makefile is invaluable. The following Makefile encapsulates all the necessary compilation and linking options:
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 simplifies the build process to a single make command, producing test_driver.sys. The full source code and Makefile are available in the kmd_mingw32 GitHub repository.
Loading and Unloading the Driver
Once built, a Windows kernel driver typically needs to be signed and then managed via the Service Control Manager.
1. Driver Signing:
On Windows 11, even for test purposes, drivers require a digital signature. This can be done using signtool.exe, usually found within a Visual Studio/WDK installation:
PS C:\\Users\\Igor> 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
WDKTestCert refers to a test certificate that needs to be created and installed beforehand.
2. Service Creation:
The Service Control utility (sc.exe) is used to register the driver with the Windows Service Control Manager:
sc create test_driver type= kernel binPath= C:\\Users\\Igor\\test_driver.sys
This command creates a service entry for test_driver in the registry (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\test_driver).
3. Starting and Stopping the Driver:
After creation, the driver can be started and stopped using sc.exe:
PS C:\\Users\\Igor> 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:\\Users\\Igor> 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
Successful loading and unloading will result in debug messages appearing in a tool like DebugView, confirming the execution of DriverEntry and test_driver_unload.
Conclusion
This exercise demonstrates the viability of utilizing the GNU toolchain (specifically mingw-w64 GCC and ld) for developing functional Windows kernel-mode drivers. While the linking phase presents unique challenges due to the specialized nature of kernel modules, careful configuration of linker flags can overcome these hurdles. This approach offers an open-source alternative for developers seeking more control and transparency in their Windows driver development workflow. The complete code and build setup are available for reference in the kmd_mingw32 GitHub repository.