In one of my pet projects, NT Objects – or ntobjx in short – I am making use of the delay loading mechanism offered by the Visual C++ toolchain, while at the same time using it on a DLL that the linker will refuse to delay-load.
And for a reason, after all ntdll.dll
is loaded into every or nearly every usermode process on Windows.
However, I didn’t quite like how the loader gets to show some error message and my program doesn’t get to do error handling at all. So I worked around the limitation.
In order to do that, I created a file which contains empty stubs for the functions I want to import. This is necessary, because unlike with 64-bit (x64) we can not create a working import library from just a module definition file when targeting 32-bit (x86), due to name mangling. Oh well.
So the recipe for x64 is simple:
lib.exe /nologo /nodefaultlib "/def:ntdll-stubs\\ntdll-delayed.txt" "/out:$(IntDir)\ntdll-delayed.lib" /machine:x64"
… or something along those lines should be in your pre-build step. For 32-bit (x86) it will have to be:
cl.exe /nologo /c /TC /Ob0 /Gz ntdll-stubs\ntdll-delayed-stubs.c "/Fo$(IntDir)\ntdll-delayed-stubs.obj" lib.exe /nologo "/def:ntdll-stubs\ntdll-delayed.txt" "/out:$(IntDir)\ntdll-delayed.lib" /machine:x86 "$(IntDir)\ntdll-delayed-stubs.obj"
You can find the ntdll-delayed.txt
and ntdll-delayed-stubs.c
in the above linked project, along with these pre-build commands (inside premake4.lua
).
The effect is that we will have delay-load entries for a DLL of the (invented) name ntdll-delayed.dll
. Inside our delay loading code, we can then handle this special DLL name and supply the proper module handle for ntdll.dll
. The symbol names will be the same as those found in ntdll.dll
anyway.
Consult delayimp.cpp
for the full details, but suffice it to say, we supply our own hook by assigning to PfnDliHook __pfnDliNotifyHook2
. The code looks like this (easy to convert to plain C):
namespace { LPCSTR lpszNtDllName = "ntdll.dll"; LPCSTR lpszNtDelayedDllName = "ntdll-delayed.dll"; } static FARPROC WINAPI NtdllDliHook(unsigned dliNotify, PDelayLoadInfo pdli) { switch(dliNotify) { case dliNotePreLoadLibrary: if(0 == lstrcmpiA(pdli->szDll, lpszNtDelayedDllName)) { if(HMODULE hNtDll = ::GetModuleHandleA(lpszNtDllName)) { /*lint -save -e611 */ return reinterpret_cast<FARPROC>(hNtDll); /*lint -restore */ } } break; /* proceed with default processing */ default: break; } return NULL; } PfnDliHook __pfnDliNotifyHook2 = NtdllDliHook;
Because I like my programs to fail early (albeit wanting to be in control of error handling), I am using the delayimp.lib
facilities to forcibly resolve all functions at startup. This then completes the circle and we’re able to delay-load symbols from ntdll.dll
at will:
static LONG WINAPI DelayLoadFilter(PEXCEPTION_POINTERS pExcPointers) { LONG lDisposition = EXCEPTION_EXECUTE_HANDLER; PDelayLoadInfo pdli = (PDelayLoadInfo)(pExcPointers->ExceptionRecord->ExceptionInformation[0]); switch(pExcPointers->ExceptionRecord->ExceptionCode) { case VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND): (void)DelayLoadError(_T("The DLL %hs could not be loaded."), pdli->szDll); break; case VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND): { LPCSTR lpszDllName = pdli->szDll; if(0 == lstrcmpiA(pdli->szDll, lpszNtDelayedDllName)) { lpszDllName = lpszNtDllName; } if (pdli->dlp.fImportByName) { (void)DelayLoadError(_T("Function %hs::%hs not found."), lpszDllName, pdli->dlp.szProcName); } else { (void)DelayLoadError(_T("Function %hs::#%u not found."), lpszDllName, pdli->dlp.dwOrdinal); } } break; default: lDisposition = EXCEPTION_CONTINUE_SEARCH; break; } return lDisposition; } EXTERN_C void force_resolve_all(void) { __try { /* Force all delay-loaded symbols to be resolved at once */ (void)__HrLoadAllImportsForDll(lpszNtDelayedDllName); } __except(DelayLoadFilter(GetExceptionInformation())) { ExitProcess(1); } }
Hope this enlightens someone,
// Oliver
PS: I realize that the title is a misnomer. We’re not delay-loading the DLL in question (ntdll.dll is among the first to load in any Windows user mode process), but we’re technically delay-binding to the functions from ntdll.dll to allow for more flexibility in error handling.