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..3d2066c4a 100644 --- a/tests/accuracy/main.rs +++ b/tests/accuracy/main.rs @@ -18,9 +18,7 @@ type Pos = (&'static str, u32); fn doit() { // 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")) { + if !cfg!(target_env = "musl") { // TODO(#238) this shouldn't have to happen first in this function, but // currently it does. let mut dir = std::env::current_exe().unwrap();