Skip to content

Commit

Permalink
Add support for MinGW dynamic libs (#345)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alexcrichton authored Jun 10, 2020
1 parent 3cf8cbf commit d471d23
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 29 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ verify-winapi = [
'winapi/minwindef',
'winapi/processthreadsapi',
'winapi/synchapi',
'winapi/tlhelp32',
'winapi/winbase',
'winapi/winnt',
]
Expand Down
18 changes: 14 additions & 4 deletions crates/without_debuginfo/tests/smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@
fn all_frames_have_symbols() {
println!("{:?}", backtrace::Backtrace::new());

let mut all_have_symbols = true;
let mut missing_symbols = 0;
let mut has_symbols = 0;
backtrace::trace(|frame| {
let mut any = false;
backtrace::resolve_frame(frame, |sym| {
if sym.name().is_some() {
any = true;
}
});
if !any && !frame.ip().is_null() {
all_have_symbols = false;
if any {
has_symbols += 1;
} else if !frame.ip().is_null() {
missing_symbols += 1;
}
true
});
assert!(all_have_symbols);

// FIXME(#346) currently on MinGW we can't symbolize kernel32.dll and other
// system libraries, which means we miss the last few symbols.
if cfg!(windows) && cfg!(target_env = "gnu") {
assert!(missing_symbols < has_symbols && has_symbols > 4);
} else {
assert_eq!(missing_symbols, 0);
}
}
101 changes: 79 additions & 22 deletions src/symbolize/gimli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,36 +88,93 @@ fn mmap(path: &Path) -> Option<Mmap> {

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<Library> {
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<Library>) {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0);
if snap == INVALID_HANDLE_VALUE {
return;
}

let mut me = MaybeUninit::<MODULEENTRY32W>::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<Library> {
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.
Expand Down
7 changes: 7 additions & 0 deletions src/symbolize/gimli/coff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ pub struct Object<'a> {
strings: StringTable<'a>,
}

pub fn get_image_base(data: &[u8]) -> Option<usize> {
let data = Bytes(data);
let dos_header = ImageDosHeader::parse(data).ok()?;
let (nt_headers, _, _) = dos_header.nt_headers::<Pe>(data).ok()?;
usize::try_from(nt_headers.optional_header().image_base()).ok()
}

impl<'a> Object<'a> {
fn parse(data: &'a [u8]) -> Option<Object<'a>> {
let data = Bytes(data);
Expand Down
32 changes: 32 additions & 0 deletions src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}

Expand Down
8 changes: 5 additions & 3 deletions tests/accuracy/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit d471d23

Please sign in to comment.