From 21d5915bfb78e5764eb602d0902e8ad170d2fecd Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 10 Jun 2020 08:53:57 -0700 Subject: [PATCH] Add support for MinGW dynamic libs This commit uses the information and APIs learned from rust-lang/rust#71060 to implement support for dynamic libraries on MinGW. Previously symbols only worked if they came from the main executable, but now the process's list of dynamic libraries are also searched so we can symbolicate, for example, symbols in the compiler which come from loaded libraries rather than the main executable. --- Cargo.toml | 1 + src/symbolize/gimli.rs | 101 ++++++++++++++++++++++++++++-------- src/symbolize/gimli/coff.rs | 7 +++ src/windows.rs | 32 ++++++++++++ tests/accuracy/main.rs | 8 +-- 5 files changed, 124 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 66f431d9d..9e6c2f283 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ verify-winapi = [ 'winapi/minwindef', 'winapi/processthreadsapi', 'winapi/synchapi', + 'winapi/tlhelp32', 'winapi/winbase', 'winapi/winnt', ] diff --git a/src/symbolize/gimli.rs b/src/symbolize/gimli.rs index 9f1ed9679..4848a63fc 100644 --- a/src/symbolize/gimli.rs +++ b/src/symbolize/gimli.rs @@ -88,36 +88,93 @@ fn mmap(path: &Path) -> Option { cfg_if::cfg_if! { if #[cfg(windows)] { - // Windows uses COFF object files and currently doesn't implement - // functionality to load a list of native libraries. This seems to work - // well enough for the main executable but seems pretty likely to not - // work for loaded DLLs. For now this seems sufficient, but we may have - // to extend this over time. - // - // Note that the native_libraries loading here simply returns one - // library encompassing the entire address space. This works naively - // but likely indicates something about ASLR is busted. Let's try to - // fix this over time if necessary! + use core::mem::MaybeUninit; + use crate::windows::*; + use std::os::windows::prelude::*; mod coff; use self::coff::Object; + // For loading native libraries on Windows, see some discussion on + // rust-lang/rust#71060 for the various strategies here. fn native_libraries() -> Vec { let mut ret = Vec::new(); - if let Ok(path) = std::env::current_exe() { - let mut segments = Vec::new(); - segments.push(LibrarySegment { - stated_virtual_memory_address: 0, - len: usize::max_value(), - }); - ret.push(Library { - name: path.into(), - segments, - bias: 0, - }); - } + unsafe { add_loaded_images(&mut ret); } return ret; } + + unsafe fn add_loaded_images(ret: &mut Vec) { + let snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0); + if snap == INVALID_HANDLE_VALUE { + return; + } + + let mut me = MaybeUninit::::zeroed().assume_init(); + me.dwSize = mem::size_of_val(&me) as DWORD; + if Module32FirstW(snap, &mut me) == TRUE { + loop { + if let Some(lib) = load_library(&me) { + ret.push(lib); + } + + if Module32NextW(snap, &mut me) != TRUE { + break; + } + } + + } + + CloseHandle(snap); + } + + unsafe fn load_library(me: &MODULEENTRY32W) -> Option { + let pos = me + .szExePath + .iter() + .position(|i| *i == 0) + .unwrap_or(me.szExePath.len()); + let name = OsString::from_wide(&me.szExePath[..pos]); + + // MinGW libraries currently don't support ASLR + // (rust-lang/rust#16514), but DLLs can still be relocated around in + // the address space. It appears that addresses in debug info are + // all as-if this library was loaded at its "image base", which is a + // field in its COFF file headers. Since this is what debuginfo + // seems to list we parse the symbol table and store addresses as if + // the library was loaded at "image base" as well. + // + // The library may not be loaded at "image base", however. + // (presumably something else may be loaded there?) This is where + // the `bias` field comes into play, and we need to figure out the + // value of `bias` here. Unfortunately though it's not clear how to + // acquire this from a loaded module. What we do have, however, is + // the actual load address (`modBaseAddr`). + // + // As a bit of a cop-out for now we mmap the file, read the file + // header information, then drop the mmap. This is wasteful because + // we'll probably reopen the mmap later, but this should work well + // enough for now. + // + // Once we have the `image_base` (desired load location) and the + // `base_addr` (actual load location) we can fill in the `bias` + // (difference between the actual and desired) and then the stated + // address of each segment is the `image_base` since that's what the + // file says. + // + // For now it appears that unlike ELF/MachO we can make do with one + // segment per library, using `modBaseSize` as the whole size. + let mmap = mmap(name.as_ref())?; + let image_base = coff::get_image_base(&mmap)?; + let base_addr = me.modBaseAddr as usize; + Some(Library { + name, + bias: base_addr.wrapping_sub(image_base), + segments: vec![LibrarySegment { + stated_virtual_memory_address: image_base, + len: me.modBaseSize as usize, + }], + }) + } } else if #[cfg(target_os = "macos")] { // macOS uses the Mach-O file format and uses DYLD-specific APIs to // load a list of native libraries that are part of the appplication. diff --git a/src/symbolize/gimli/coff.rs b/src/symbolize/gimli/coff.rs index ce96bb9d1..c32782f38 100644 --- a/src/symbolize/gimli/coff.rs +++ b/src/symbolize/gimli/coff.rs @@ -25,6 +25,13 @@ pub struct Object<'a> { strings: StringTable<'a>, } +pub fn get_image_base(data: &[u8]) -> Option { + let data = Bytes(data); + let dos_header = ImageDosHeader::parse(data).ok()?; + let (nt_headers, _, _) = dos_header.nt_headers::(data).ok()?; + usize::try_from(nt_headers.optional_header().image_base()).ok() +} + impl<'a> Object<'a> { fn parse(data: &'a [u8]) -> Option> { let data = Bytes(data); diff --git a/src/windows.rs b/src/windows.rs index 17e90dafc..d091874f1 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -32,6 +32,7 @@ cfg_if::cfg_if! { pub use winapi::um::minwinbase::*; pub use winapi::um::processthreadsapi::*; pub use winapi::um::synchapi::*; + pub use winapi::um::tlhelp32::*; pub use winapi::um::winbase::*; pub use winapi::um::winnt::*; } @@ -311,6 +312,20 @@ ffi! { pub Reserved1: [DWORD64; 4], } + #[repr(C)] + pub struct MODULEENTRY32W { + pub dwSize: DWORD, + pub th32ModuleID: DWORD, + pub th32ProcessID: DWORD, + pub GlblcntUsage: DWORD, + pub ProccntUsage: DWORD, + pub modBaseAddr: *mut u8, + pub modBaseSize: DWORD, + pub hModule: HMODULE, + pub szModule: [WCHAR; MAX_MODULE_NAME32 + 1], + pub szExePath: [WCHAR; MAX_PATH], + } + pub const MAX_SYM_NAME: usize = 2000; pub const AddrModeFlat: ADDRESS_MODE = 3; pub const TRUE: BOOL = 1; @@ -327,6 +342,10 @@ ffi! { pub const INFINITE: DWORD = !0; pub const PAGE_READONLY: DWORD = 2; pub const FILE_MAP_READ: DWORD = 4; + pub const TH32CS_SNAPMODULE: DWORD = 0x00000008; + pub const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE; + pub const MAX_MODULE_NAME32: usize = 255; + pub const MAX_PATH: usize = 260; pub type DWORD = u32; pub type PDWORD = *mut u32; @@ -350,6 +369,7 @@ ffi! { pub type SIZE_T = usize; pub type LPVOID = *mut c_void; pub type LPCVOID = *const c_void; + pub type LPMODULEENTRY32W = *mut MODULEENTRY32W; extern "system" { pub fn GetCurrentProcess() -> HANDLE; @@ -401,6 +421,18 @@ ffi! { dwNumberOfBytesToMap: SIZE_T, ) -> LPVOID; pub fn UnmapViewOfFile(lpBaseAddress: LPCVOID) -> BOOL; + pub fn CreateToolhelp32Snapshot( + dwFlags: DWORD, + th32ProcessID: DWORD, + ) -> HANDLE; + pub fn Module32FirstW( + hSnapshot: HANDLE, + lpme: LPMODULEENTRY32W, + ) -> BOOL; + pub fn Module32NextW( + hSnapshot: HANDLE, + lpme: LPMODULEENTRY32W, + ) -> BOOL; } } diff --git a/tests/accuracy/main.rs b/tests/accuracy/main.rs index 56365b11d..b339d3161 100644 --- a/tests/accuracy/main.rs +++ b/tests/accuracy/main.rs @@ -16,11 +16,13 @@ type Pos = (&'static str, u32); #[test] fn doit() { + if // Skip musl which is by default statically linked and doesn't support // dynamic libraries. - // - // FIXME(#333) doesn't work on MinGW yet - if !cfg!(target_env = "musl") && !(cfg!(windows) && cfg!(target_env = "gnu")) { + !cfg!(target_env = "musl") + // Skip MinGW on libbacktrace which doesn't have support for DLLs. + && !(cfg!(windows) && cfg!(target_env = "gnu") && cfg!(feature = "libbacktrace")) + { // TODO(#238) this shouldn't have to happen first in this function, but // currently it does. let mut dir = std::env::current_exe().unwrap();