Problems when building NT kernel drivers with GCC / LD

Pali Rohár
Mon Dec 26 10:47:58 GMT 2022

Hello! I would like to remind this thread for gcc/binutils developers.
Most of these issues are still present and cause problems for compiling
native PE binary. If you have questions or you need any other information
please let me know.

On Sunday 30 October 2022 02:06:11 Pali Rohár wrote:
> Hello!
> I tried to develop simple NT kernel driver and build it via GNU GCC, LD
> and mingw-w64 ecosystem but due to issues with compiler and linker, it
> is not possible to generate final .sys binary without adding workarounds
> or postprocessing resulted PE .sys binary.
> I'm sending this email to all parties (binutils = linker, gcc = compiler,
> mingw-w64 = runtime) which are involved in building process of NT kernel
> driver (which is just native PE binary) and I would like to describe
> existing issues and ask if you can come up with fixes.
> For demonstration I created very simple NT kernel driver which can be
> compiled by MSVC compiler without any issues, so you can directly look
> at the code and play with it:
> I have there also GNUmakefile which uses GNU tools for compilation and
> tries to workaround issues.
> Here is the list of those GCC / LD issues which I observed during
> development and compilation of NT kernel drivers:
> * GCC does not support __declspec(code_seg("segname")) declarator for
>   specifying name of PE/COFF segment name. But instead GCC supports
>   different syntax __declspec(section("segname")) for this thing.
>   I thought that GCC added __declspec support for compatibility with
>   other PE/COFF compilers. But because declarators are different it just
>   means that __declspec segment name is not compatible neither with MSVC.
>   If the GCC's intention of __declspec is compatibility support then it
>   would be really really useful to have support for __declspec(code_seg
>   instead of custom __declspec(section syntax supported only by GCC.
>   As a workaround to have driver compilable by both MSVC and GCC it is
>   needed to add #ifdef for GCC compiler.
> * LD --dynamicbase is not working correctly. If used for PE executables
>   (not dynamic libraries) then it does *not* generate relocation info.
>   And without relocation info in PE binary, it is not possible to
>   relocate base address. Which makes dynamic base non-working.
>   As a workaround GCC -pie can be used to generate relocation info (but
>   it is buggy too, see below).
> * GCC -pie is unusable. It automatically generates export symbol table
>   even when PE binary must not export anything. This applies for Native
>   PE executables used as NT kernel .sys drivers. Normally only dynamic
>   libraries export symbols, so by default for executables should not
>   symbol table. Or at least there should be a flag which can disable
>   doing it.
>   I tried to workaround it via LD --exclude-all-symbols, but it is also
>   not usable, see below.
> * LD --exclude-all-symbols has issues for Native PE executables. It
>   causes generation of empty export table and puts name of the output
>   executable/driver (-o) as name of the export library. PE executables
>   which are in shared address space (e.g. NT driver executables) should
>   really do not be treated as libraries with library name (unless
>   explicitly asked for such thing).
>   As a workaround I had to create a custom linker script which discards
>   that nonsense export table by completely dropping .edata section.
> * LD puts discardable init sections in the middle of the PE binary. This
>   is really stupid as loaded PE binary waste memory space. Discardable
>   init sections should be at the end of the PE binary.
>   As a workaround I used custom linker script.
> * GCC or LD (not sure who) sets memory alignment characteristics
>   (IMAGE_SCN_ALIGN_MASK) into the sections of PE executable binary.
>   These characteristics should be only in COFF object files, not
>   executable binaries. Specially they should not be in NT kernel
>   drivers.
> * GCC or LD (not sure who) sets data characteristics
>   (IMAGE_SCN_CNT_INITIALIZED_DATA) for executable PE code sections.
>   Code sections should not be really marked as data, only as a code
> * GCC or LD (not sure who) sets memory writable characteristics
>   (IMAGE_SCN_MEM_WRITE) into the read-only resources PE sections.
>   Read-only sections of final PE executable really should not marked as
>   writable.
> * GCC or LD (not sure who) does *not* set memory discardable
>   characteristic (IMAGE_SCN_MEM_DISCARDABLE) for sections used only
>   during initial phase in NT kernel driver PE executables. This applies
>   for .idata, INIT and .rsrc sections. Without that characteristic there
>   is wasting of kernel memory.
> * GCC or LD (not sure who) does *not* set memory not_paged
>   characteristics (IMAGE_SCN_MEM_NOT_PAGED) for PE sections which must
>   not be paged in NT kernel driver executables. Basically most sections
>   should must not be paged unless explicitly asked/marked. By default
>   just section with name PAGE could be paged and therefore only this
>   section does not have to have IMAGE_SCN_MEM_NOT_PAGED.
> Last five issues I was not able to workaround neither via gcc or ld
> flags and neither via linker script. For example MSVC link.exe linker
> has switch /SECTION: which can be used to specify above characteristics
> specific section. But whatever I tried to do in GNU linker script, I was
> not able to set and fix those characteristics. For this reason I wrote
> custom application which post-process built PE executable and fix
> section characteristics, which GNU GCC/LD generated incorrectly.
> Then I have there rather small but sometimes annoying objcopy issue.
> objcopy does not respect --disable-deterministic-archives or
> --preserve-dates switches and always deletes PE date/timestamp from
> executable. I was not able to find any switch which can tell objcopy to
> preserve PE date/timestamp. LD has flag --insert-timestamp for this
> thing, and this LD switch is working.
> Could you please look at those issues? Is there something which can be
> done to fix them? Because it is really stop-blocker for building even
> simple NT kernel drivers via open source GNU tools. Basically one main
> problem - generate relocation info for native executable - just reveal
> chain of other issues.
> NT kernel .sys drivers are just simple Native PE executables (like
> ordinary .exe executables) but with relocation info (like .dll
> libraries) with fixed entry point, linked to ntoskrnl and have some
> characteristics.
> Following gcc flags produce such executable (but with above issues):
>   -Wl,--subsystem,native
>   -Wl,--dynamicbase -Wl,--nxcompat
>   -Wl,--image-base,0x10000 -Wl,--stack,0x40000
>   -Wl,--section-alignment,0x1000 -Wl,--file-alignment,0x200
>   -Wl,--disable-auto-import -Wl,--disable-stdcall-fixup
>   -nostartfiles -nodefaultlibs -nostdlib
>   -lntoskrnl
> Note that entry point is always stdcall "DriverEntry" function. And
> because of symbol mangling, its symbol name is "_DriverEntry@8" for IX86
> and "DriverEntry" for AMD64. LD switch --entry= takes symbol name, not
> function name. So it would be a nice feature to allow specifying entry
> point via GCC/LD also as function name (without mangling) to have just
>   -Wl,--entry=DriverEntry
> parameter for gcc for all 32-bit and 64-bit builds.
> And also it would be a nice to have some GCC flag which sets all above
> flags required for compiling NT kernel driver. Like there is -mdll for
> compiling DLL libraries or -mwindows for generating GUI WIN32
> executables. For example MSVC link.exe has for this switch /DRIVER.
> If you have some questions then please let me know. I think that it
> would be really useful if GNU tools would be able to build NT kernel
> drivers without needs for hacks.

More information about the Gcc mailing list