-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support DLL delay-loading on Windows #13436
Merged
straight-shoota
merged 2 commits into
crystal-lang:master
from
HertzDevil:feature/windows-dll-delay-load
May 11, 2023
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
require "c/delayimp" | ||
|
||
private ERROR_SEVERITY_ERROR = 0xC0000000_u32 | ||
private FACILITY_VISUALCPP = 0x6d | ||
|
||
private macro vcpp_exception(err) | ||
{{ ERROR_SEVERITY_ERROR | (FACILITY_VISUALCPP << 16) | WinError.constant(err.id) }} | ||
end | ||
|
||
lib LibC | ||
$image_base = __ImageBase : IMAGE_DOS_HEADER | ||
end | ||
|
||
private macro p_from_rva(rva) | ||
pointerof(LibC.image_base).as(UInt8*) + {{ rva }} | ||
end | ||
|
||
module Crystal::System::DelayLoad | ||
@[Extern] | ||
record InternalImgDelayDescr, | ||
grAttrs : LibC::DWORD, | ||
szName : LibC::LPSTR, | ||
phmod : LibC::HMODULE*, | ||
pIAT : LibC::IMAGE_THUNK_DATA*, | ||
pINT : LibC::IMAGE_THUNK_DATA*, | ||
pBoundIAT : LibC::IMAGE_THUNK_DATA*, | ||
pUnloadIAT : LibC::IMAGE_THUNK_DATA*, | ||
dwTimeStamp : LibC::DWORD | ||
|
||
@[AlwaysInline] | ||
def self.pinh_from_image_base(hmod : LibC::HMODULE) | ||
(hmod.as(UInt8*) + hmod.as(LibC::IMAGE_DOS_HEADER*).value.e_lfanew).as(LibC::IMAGE_NT_HEADERS*) | ||
end | ||
|
||
@[AlwaysInline] | ||
def self.interlocked_exchange(atomic : LibC::HMODULE*, value : LibC::HMODULE) | ||
Atomic::Ops.atomicrmw(:xchg, atomic, value, :sequentially_consistent, false) | ||
end | ||
end | ||
|
||
# This is a port of the default delay-load helper function in the `DelayHlp.cpp` | ||
# file that comes with Microsoft Visual C++, except that all user-defined hooks | ||
# are omitted. It is called every time the program attempts to load a symbol | ||
# from a DLL. For more details see: | ||
# https://learn.microsoft.com/en-us/cpp/build/reference/understanding-the-helper-function | ||
# | ||
# It is available even when the `preview_dll` flag is absent, so that system | ||
# DLLs such as `advapi32.dll` and shards can be delay-loaded in the usual mixed | ||
# static/dynamic builds by passing the appropriate linker flags explicitly. | ||
# | ||
# The delay load helper cannot call functions from the library being loaded, as | ||
# that leads to an infinite recursion. In particular, if `preview_dll` is in | ||
# effect, `Crystal::System.print_error` will not work, because the C runtime | ||
# library DLLs are also delay-loaded and `LibC.snprintf` is unavailable. If you | ||
# want print debugging inside this function, try the following: | ||
# | ||
# ``` | ||
# lib LibC | ||
# STD_OUTPUT_HANDLE = -11 | ||
# | ||
# fun GetStdHandle(nStdHandle : DWORD) : HANDLE | ||
# fun FormatMessageA(dwFlags : DWORD, lpSource : Void*, dwMessageId : DWORD, dwLanguageId : DWORD, lpBuffer : LPSTR, nSize : DWORD, arguments : Void*) : DWORD | ||
# end | ||
# | ||
# buf = uninitialized LibC::CHAR[512] | ||
# args = StaticArray[dli.szDll, dli.dlp.union.szProcName] | ||
# len = LibC.FormatMessageA(LibC::FORMAT_MESSAGE_FROM_STRING | LibC::FORMAT_MESSAGE_ARGUMENT_ARRAY, "Loading `%2` from `%1`\n", 0, 0, buf, buf.size, args) | ||
# LibC.WriteFile(LibC.GetStdHandle(LibC::STD_OUTPUT_HANDLE), buf, len, out _, nil) | ||
# ``` | ||
# | ||
# `kernel32.dll` is the only DLL guaranteed to be available. It cannot be | ||
# delay-loaded and the Crystal compiler excludes it from the linker arguments. | ||
# | ||
# This function does _not_ work with the empty prelude yet! | ||
fun __delayLoadHelper2(pidd : LibC::ImgDelayDescr*, ppfnIATEntry : LibC::FARPROC*) : LibC::FARPROC | ||
# TODO: support protected delay load? (/GUARD:CF) | ||
# DloadAcquireSectionWriteAccess | ||
|
||
# Set up some data we use for the hook procs but also useful for our own use | ||
idd = Crystal::System::DelayLoad::InternalImgDelayDescr.new( | ||
grAttrs: pidd.value.grAttrs, | ||
szName: p_from_rva(pidd.value.rvaDLLName).as(LibC::LPSTR), | ||
phmod: p_from_rva(pidd.value.rvaHmod).as(LibC::HMODULE*), | ||
pIAT: p_from_rva(pidd.value.rvaIAT).as(LibC::IMAGE_THUNK_DATA*), | ||
pINT: p_from_rva(pidd.value.rvaINT).as(LibC::IMAGE_THUNK_DATA*), | ||
pBoundIAT: p_from_rva(pidd.value.rvaBoundIAT).as(LibC::IMAGE_THUNK_DATA*), | ||
pUnloadIAT: p_from_rva(pidd.value.rvaUnloadIAT).as(LibC::IMAGE_THUNK_DATA*), | ||
dwTimeStamp: pidd.value.dwTimeStamp, | ||
) | ||
|
||
dli = LibC::DelayLoadInfo.new( | ||
cb: sizeof(LibC::DelayLoadInfo), | ||
pidd: pidd, | ||
ppfn: ppfnIATEntry, | ||
szDll: idd.szName, | ||
dlp: LibC::DelayLoadProc.new, | ||
hmodCur: LibC::HMODULE.null, | ||
pfnCur: LibC::FARPROC.null, | ||
dwLastError: LibC::DWORD.zero, | ||
) | ||
|
||
if 0 == idd.grAttrs & LibC::DLAttrRva | ||
rgpdli = pointerof(dli) | ||
|
||
# DloadReleaseSectionWriteAccess | ||
|
||
LibC.RaiseException( | ||
vcpp_exception(ERROR_INVALID_PARAMETER), | ||
0, | ||
1, | ||
pointerof(rgpdli).as(LibC::ULONG_PTR*), | ||
) | ||
end | ||
|
||
hmod = idd.phmod.value | ||
|
||
# Calculate the index for the IAT entry in the import address table | ||
# N.B. The INT entries are ordered the same as the IAT entries so | ||
# the calculation can be done on the IAT side. | ||
iIAT = ppfnIATEntry.as(LibC::IMAGE_THUNK_DATA*) - idd.pIAT | ||
iINT = iIAT | ||
|
||
pitd = idd.pINT + iINT | ||
|
||
dli.dlp.fImportByName = pitd.value.u1.ordinal & LibC::IMAGE_ORDINAL_FLAG == 0 | ||
|
||
if dli.dlp.fImportByName | ||
import_by_name = p_from_rva(LibC::RVA.new!(pitd.value.u1.addressOfData)) | ||
dli.dlp.union.szProcName = import_by_name + offsetof(LibC::IMAGE_IMPORT_BY_NAME, @name) | ||
else | ||
dli.dlp.union.dwOrdinal = LibC::DWORD.new!(pitd.value.u1.ordinal & 0xFFFF) | ||
end | ||
|
||
# Check to see if we need to try to load the library. | ||
if !hmod | ||
# note: ANSI variant used here | ||
unless hmod = LibC.LoadLibraryExA(dli.szDll, nil, 0) | ||
dli.dwLastError = LibC.GetLastError | ||
|
||
rgpdli = pointerof(dli) | ||
|
||
# DloadReleaseSectionWriteAccess | ||
LibC.RaiseException( | ||
vcpp_exception(ERROR_MOD_NOT_FOUND), | ||
0, | ||
1, | ||
pointerof(rgpdli).as(LibC::ULONG_PTR*), | ||
) | ||
|
||
# If we get to here, we blindly assume that the handler of the exception | ||
# has magically fixed everything up and left the function pointer in | ||
# dli.pfnCur. | ||
return dli.pfnCur | ||
end | ||
|
||
# Store the library handle. If it is already there, we infer | ||
# that another thread got there first, and we need to do a | ||
# FreeLibrary() to reduce the refcount | ||
hmodT = Crystal::System::DelayLoad.interlocked_exchange(idd.phmod, hmod) | ||
LibC.FreeLibrary(hmod) if hmodT == hmod | ||
end | ||
|
||
# Go for the procedure now. | ||
dli.hmodCur = hmod | ||
if pidd.value.rvaBoundIAT != 0 && pidd.value.dwTimeStamp != 0 | ||
# bound imports exist...check the timestamp from the target image | ||
pinh = Crystal::System::DelayLoad.pinh_from_image_base(hmod) | ||
|
||
if pinh.value.signature == LibC::IMAGE_NT_SIGNATURE && | ||
pinh.value.fileHeader.timeDateStamp == idd.dwTimeStamp && | ||
hmod.address == pinh.value.optionalHeader.imageBase | ||
# Everything is good to go, if we have a decent address | ||
# in the bound IAT! | ||
if pfnRet = LibC::FARPROC.new(idd.pBoundIAT[iIAT].u1.function) | ||
ppfnIATEntry.value = pfnRet | ||
# DloadReleaseSectionWriteAccess | ||
return pfnRet | ||
end | ||
end | ||
end | ||
|
||
unless pfnRet = LibC.GetProcAddress(hmod, dli.dlp.union.szProcName) | ||
dli.dwLastError = LibC.GetLastError | ||
|
||
rgpdli = pointerof(dli) | ||
|
||
# DloadReleaseSectionWriteAccess | ||
LibC.RaiseException( | ||
vcpp_exception(ERROR_PROC_NOT_FOUND), | ||
0, | ||
1, | ||
pointerof(rgpdli).as(LibC::ULONG_PTR*), | ||
) | ||
# DloadAcquireSectionWriteAccess | ||
|
||
# If we get to here, we blindly assume that the handler of the exception | ||
# has magically fixed everything up and left the function pointer in | ||
# dli.pfnCur. | ||
pfnRet = dli.pfnCur | ||
end | ||
|
||
ppfnIATEntry.value = pfnRet | ||
# DloadReleaseSectionWriteAccess | ||
pfnRet | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
require "c/libloaderapi" | ||
require "c/winnt" | ||
|
||
lib LibC | ||
alias RVA = DWORD | ||
|
||
struct ImgDelayDescr | ||
grAttrs : DWORD # attributes | ||
rvaDLLName : RVA # RVA to dll name | ||
rvaHmod : RVA # RVA of module handle | ||
rvaIAT : RVA # RVA of the IAT | ||
rvaINT : RVA # RVA of the INT | ||
rvaBoundIAT : RVA # RVA of the optional bound IAT | ||
rvaUnloadIAT : RVA # RVA of optional copy of original IAT | ||
dwTimeStamp : DWORD # 0 if not bound, O.W. date/time stamp of DLL bound to (Old BIND) | ||
end | ||
|
||
DLAttrRva = 0x1 | ||
|
||
union DelayLoadProc_union | ||
szProcName : LPSTR | ||
dwOrdinal : DWORD | ||
end | ||
|
||
struct DelayLoadProc | ||
fImportByName : BOOL | ||
union : DelayLoadProc_union | ||
end | ||
|
||
struct DelayLoadInfo | ||
cb : DWORD # size of structure | ||
pidd : ImgDelayDescr* # raw form of data (everything is there) | ||
ppfn : FARPROC* # points to address of function to load | ||
szDll : LPSTR # name of dll | ||
dlp : DelayLoadProc # name or ordinal of procedure | ||
hmodCur : HMODULE # the hInstance of the library we have loaded | ||
pfnCur : FARPROC # the actual function that will be called | ||
dwLastError : DWORD # error received (if an error notification) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is just one caveat: for some reason
LibC.image_base
cannot be accessed from any LLVM module scope other than the top-level one, otherwise MSVC raises an internal linker error, and the only way out is to pass--single-module
. So this cannot be a class method ofCrystal::System::DelayLoad
, and neither can__delayLoadHelper2
itself