From 875abb69558a2c37f07ff369c1252bde284a3998 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Fri, 25 Oct 2024 15:34:20 +0800 Subject: [PATCH] Support "long format" DLL import libraries (#15119) `Crystal::System::LibraryArchive.imported_dlls` is used by the interpreter to obtain all dependent DLLs of a given import library. Currently, the method only supports libraries using the [short format](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-library-format), emitted by MSVC's linker. This PR implements the long format used by MinGW-w64 when passing the `-Wl,--out-implib` flag to `cc`. Specs will be added when `Crystal::Loader` supports MinGW-w64. In the mean time, if you have MSYS2, you could try this on the UCRT64 import libraries: ```crystal require "crystal/system/win32/library_archive" Crystal::System::LibraryArchive.imported_dlls("C:/msys64/ucrt64/lib/libpcre2-8.dll.a") # => Set{"libpcre2-8-0.dll"} Crystal::System::LibraryArchive.imported_dlls("C:/msys64/ucrt64/lib/libucrt.a") # => Set{"api-ms-win-crt-utility-l1-1-0.dll", "api-ms-win-crt-time-l1-1-0.dll", "api-ms-win-crt-string-l1-1-0.dll", "api-ms-win-crt-stdio-l1-1-0.dll", "api-ms-win-crt-runtime-l1-1-0.dll", "api-ms-win-crt-process-l1-1-0.dll", "api-ms-win-crt-private-l1-1-0.dll", "api-ms-win-crt-multibyte-l1-1-0.dll", "api-ms-win-crt-math-l1-1-0.dll", "api-ms-win-crt-locale-l1-1-0.dll", "api-ms-win-crt-heap-l1-1-0.dll", "api-ms-win-crt-filesystem-l1-1-0.dll", "api-ms-win-crt-environment-l1-1-0.dll", "api-ms-win-crt-convert-l1-1-0.dll", "api-ms-win-crt-conio-l1-1-0.dll"} ``` --- src/crystal/system/win32/library_archive.cr | 74 +++++++++++++++++---- src/lib_c/x86_64-windows-msvc/c/winnt.cr | 2 + 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/crystal/system/win32/library_archive.cr b/src/crystal/system/win32/library_archive.cr index 775677938bac..24c50f3405fa 100644 --- a/src/crystal/system/win32/library_archive.cr +++ b/src/crystal/system/win32/library_archive.cr @@ -17,6 +17,10 @@ module Crystal::System::LibraryArchive private struct COFFReader getter dlls = Set(String).new + # MSVC-style import libraries include the `__NULL_IMPORT_DESCRIPTOR` symbol, + # MinGW-style ones do not + getter? msvc = false + def initialize(@ar : ::File) end @@ -39,6 +43,7 @@ module Crystal::System::LibraryArchive if first first = false return unless filename == "/" + handle_first_member(io) elsif !filename.in?("/", "//") handle_standard_member(io) end @@ -62,26 +67,69 @@ module Crystal::System::LibraryArchive @ar.seek(new_pos) end + private def handle_first_member(io) + symbol_count = io.read_bytes(UInt32, IO::ByteFormat::BigEndian) + + # 4-byte offset per symbol + io.skip(symbol_count * 4) + + symbol_count.times do + symbol = io.gets('\0', chomp: true) + if symbol == "__NULL_IMPORT_DESCRIPTOR" + @msvc = true + break + end + end + end + private def handle_standard_member(io) - sig1 = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) - return unless sig1 == 0x0000 # IMAGE_FILE_MACHINE_UNKNOWN + machine = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) + section_count = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) - sig2 = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) - return unless sig2 == 0xFFFF + if machine == 0x0000 && section_count == 0xFFFF + # short import library + version = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) + return unless version == 0 # 1 and 2 are used by object files (ANON_OBJECT_HEADER) - version = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) - return unless version == 0 # 1 and 2 are used by object files (ANON_OBJECT_HEADER) + # machine(2) + time(4) + size(4) + ordinal/hint(2) + flags(2) + io.skip(14) - # machine(2) + time(4) + size(4) + ordinal/hint(2) + flags(2) - io.skip(14) + # TODO: is there a way to do this without constructing a temporary string, + # but with the optimizations present in `IO#gets`? + return unless io.gets('\0') # symbol name - # TODO: is there a way to do this without constructing a temporary string, - # but with the optimizations present in `IO#gets`? - return unless io.gets('\0') # symbol name + if dll_name = io.gets('\0', chomp: true) + @dlls << dll_name if valid_dll?(dll_name) + end + else + # long import library, code based on GNU binutils `dlltool -I`: + # https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=binutils/dlltool.c;hb=967dc35c78adb85ee1e2e596047d9dc69107a9db#l3231 + + # timeDateStamp(4) + pointerToSymbolTable(4) + numberOfSymbols(4) + sizeOfOptionalHeader(2) + characteristics(2) + io.skip(16) + + section_count.times do |i| + section_header = uninitialized LibC::IMAGE_SECTION_HEADER + return unless io.read_fully?(pointerof(section_header).to_slice(1).to_unsafe_bytes) + + name = String.new(section_header.name.to_unsafe, section_header.name.index(0) || section_header.name.size) + next unless name == (msvc? ? ".idata$6" : ".idata$7") + + if msvc? ? section_header.characteristics.bits_set?(LibC::IMAGE_SCN_CNT_INITIALIZED_DATA) : section_header.pointerToRelocations == 0 + bytes_read = sizeof(LibC::IMAGE_FILE_HEADER) + sizeof(LibC::IMAGE_SECTION_HEADER) * (i + 1) + io.skip(section_header.pointerToRawData - bytes_read) + if dll_name = io.gets('\0', chomp: true, limit: section_header.sizeOfRawData) + @dlls << dll_name if valid_dll?(dll_name) + end + end - if dll_name = io.gets('\0', chomp: true) - @dlls << dll_name + return + end end end + + private def valid_dll?(name) + name.size >= 5 && name[-4..].compare(".dll", case_insensitive: true) == 0 + end end end diff --git a/src/lib_c/x86_64-windows-msvc/c/winnt.cr b/src/lib_c/x86_64-windows-msvc/c/winnt.cr index e9aecc01e033..99c8f24ac9e1 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winnt.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winnt.cr @@ -396,6 +396,8 @@ lib LibC IMAGE_DIRECTORY_ENTRY_IMPORT = 1 IMAGE_DIRECTORY_ENTRY_IAT = 12 + IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 + struct IMAGE_SECTION_HEADER name : BYTE[8] virtualSize : DWORD