Skip to content

Commit fc334fe

Browse files
committed
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.
1 parent 3cf8cbf commit fc334fe

File tree

6 files changed

+138
-29
lines changed

6 files changed

+138
-29
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ verify-winapi = [
100100
'winapi/minwindef',
101101
'winapi/processthreadsapi',
102102
'winapi/synchapi',
103+
'winapi/tlhelp32',
103104
'winapi/winbase',
104105
'winapi/winnt',
105106
]

crates/without_debuginfo/tests/smoke.rs

+14-4
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@
22
fn all_frames_have_symbols() {
33
println!("{:?}", backtrace::Backtrace::new());
44

5-
let mut all_have_symbols = true;
5+
let mut missing_symbols = 0;
6+
let mut has_symbols = 0;
67
backtrace::trace(|frame| {
78
let mut any = false;
89
backtrace::resolve_frame(frame, |sym| {
910
if sym.name().is_some() {
1011
any = true;
1112
}
1213
});
13-
if !any && !frame.ip().is_null() {
14-
all_have_symbols = false;
14+
if any {
15+
has_symbols += 1;
16+
} else if !frame.ip().is_null() {
17+
missing_symbols += 1;
1518
}
1619
true
1720
});
18-
assert!(all_have_symbols);
21+
22+
// FIXME(#346) currently on MinGW we can't symbolize kernel32.dll and other
23+
// system libraries, which means we miss the last few symbols.
24+
if cfg!(windows) && cfg!(target_env = "gnu") {
25+
assert!(missing_symbols < 3 && has_symbols > 5);
26+
} else {
27+
assert_eq!(missing_symbols, 0);
28+
}
1929
}

src/symbolize/gimli.rs

+79-22
Original file line numberDiff line numberDiff line change
@@ -88,36 +88,93 @@ fn mmap(path: &Path) -> Option<Mmap> {
8888

8989
cfg_if::cfg_if! {
9090
if #[cfg(windows)] {
91-
// Windows uses COFF object files and currently doesn't implement
92-
// functionality to load a list of native libraries. This seems to work
93-
// well enough for the main executable but seems pretty likely to not
94-
// work for loaded DLLs. For now this seems sufficient, but we may have
95-
// to extend this over time.
96-
//
97-
// Note that the native_libraries loading here simply returns one
98-
// library encompassing the entire address space. This works naively
99-
// but likely indicates something about ASLR is busted. Let's try to
100-
// fix this over time if necessary!
91+
use core::mem::MaybeUninit;
92+
use crate::windows::*;
93+
use std::os::windows::prelude::*;
10194

10295
mod coff;
10396
use self::coff::Object;
10497

98+
// For loading native libraries on Windows, see some discussion on
99+
// rust-lang/rust#71060 for the various strategies here.
105100
fn native_libraries() -> Vec<Library> {
106101
let mut ret = Vec::new();
107-
if let Ok(path) = std::env::current_exe() {
108-
let mut segments = Vec::new();
109-
segments.push(LibrarySegment {
110-
stated_virtual_memory_address: 0,
111-
len: usize::max_value(),
112-
});
113-
ret.push(Library {
114-
name: path.into(),
115-
segments,
116-
bias: 0,
117-
});
118-
}
102+
unsafe { add_loaded_images(&mut ret); }
119103
return ret;
120104
}
105+
106+
unsafe fn add_loaded_images(ret: &mut Vec<Library>) {
107+
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0);
108+
if snap == INVALID_HANDLE_VALUE {
109+
return;
110+
}
111+
112+
let mut me = MaybeUninit::<MODULEENTRY32W>::zeroed().assume_init();
113+
me.dwSize = mem::size_of_val(&me) as DWORD;
114+
if Module32FirstW(snap, &mut me) == TRUE {
115+
loop {
116+
if let Some(lib) = load_library(&me) {
117+
ret.push(lib);
118+
}
119+
120+
if Module32NextW(snap, &mut me) != TRUE {
121+
break;
122+
}
123+
}
124+
125+
}
126+
127+
CloseHandle(snap);
128+
}
129+
130+
unsafe fn load_library(me: &MODULEENTRY32W) -> Option<Library> {
131+
let pos = me
132+
.szExePath
133+
.iter()
134+
.position(|i| *i == 0)
135+
.unwrap_or(me.szExePath.len());
136+
let name = OsString::from_wide(&me.szExePath[..pos]);
137+
138+
// MinGW libraries currently don't support ASLR
139+
// (rust-lang/rust#16514), but DLLs can still be relocated around in
140+
// the address space. It appears that addresses in debug info are
141+
// all as-if this library was loaded at its "image base", which is a
142+
// field in its COFF file headers. Since this is what debuginfo
143+
// seems to list we parse the symbol table and store addresses as if
144+
// the library was loaded at "image base" as well.
145+
//
146+
// The library may not be loaded at "image base", however.
147+
// (presumably something else may be loaded there?) This is where
148+
// the `bias` field comes into play, and we need to figure out the
149+
// value of `bias` here. Unfortunately though it's not clear how to
150+
// acquire this from a loaded module. What we do have, however, is
151+
// the actual load address (`modBaseAddr`).
152+
//
153+
// As a bit of a cop-out for now we mmap the file, read the file
154+
// header information, then drop the mmap. This is wasteful because
155+
// we'll probably reopen the mmap later, but this should work well
156+
// enough for now.
157+
//
158+
// Once we have the `image_base` (desired load location) and the
159+
// `base_addr` (actual load location) we can fill in the `bias`
160+
// (difference between the actual and desired) and then the stated
161+
// address of each segment is the `image_base` since that's what the
162+
// file says.
163+
//
164+
// For now it appears that unlike ELF/MachO we can make do with one
165+
// segment per library, using `modBaseSize` as the whole size.
166+
let mmap = mmap(name.as_ref())?;
167+
let image_base = coff::get_image_base(&mmap)?;
168+
let base_addr = me.modBaseAddr as usize;
169+
Some(Library {
170+
name,
171+
bias: base_addr.wrapping_sub(image_base),
172+
segments: vec![LibrarySegment {
173+
stated_virtual_memory_address: image_base,
174+
len: me.modBaseSize as usize,
175+
}],
176+
})
177+
}
121178
} else if #[cfg(target_os = "macos")] {
122179
// macOS uses the Mach-O file format and uses DYLD-specific APIs to
123180
// load a list of native libraries that are part of the appplication.

src/symbolize/gimli/coff.rs

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ pub struct Object<'a> {
2525
strings: StringTable<'a>,
2626
}
2727

28+
pub fn get_image_base(data: &[u8]) -> Option<usize> {
29+
let data = Bytes(data);
30+
let dos_header = ImageDosHeader::parse(data).ok()?;
31+
let (nt_headers, _, _) = dos_header.nt_headers::<Pe>(data).ok()?;
32+
usize::try_from(nt_headers.optional_header().image_base()).ok()
33+
}
34+
2835
impl<'a> Object<'a> {
2936
fn parse(data: &'a [u8]) -> Option<Object<'a>> {
3037
let data = Bytes(data);

src/windows.rs

+32
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ cfg_if::cfg_if! {
3232
pub use winapi::um::minwinbase::*;
3333
pub use winapi::um::processthreadsapi::*;
3434
pub use winapi::um::synchapi::*;
35+
pub use winapi::um::tlhelp32::*;
3536
pub use winapi::um::winbase::*;
3637
pub use winapi::um::winnt::*;
3738
}
@@ -311,6 +312,20 @@ ffi! {
311312
pub Reserved1: [DWORD64; 4],
312313
}
313314

315+
#[repr(C)]
316+
pub struct MODULEENTRY32W {
317+
pub dwSize: DWORD,
318+
pub th32ModuleID: DWORD,
319+
pub th32ProcessID: DWORD,
320+
pub GlblcntUsage: DWORD,
321+
pub ProccntUsage: DWORD,
322+
pub modBaseAddr: *mut u8,
323+
pub modBaseSize: DWORD,
324+
pub hModule: HMODULE,
325+
pub szModule: [WCHAR; MAX_MODULE_NAME32 + 1],
326+
pub szExePath: [WCHAR; MAX_PATH],
327+
}
328+
314329
pub const MAX_SYM_NAME: usize = 2000;
315330
pub const AddrModeFlat: ADDRESS_MODE = 3;
316331
pub const TRUE: BOOL = 1;
@@ -327,6 +342,10 @@ ffi! {
327342
pub const INFINITE: DWORD = !0;
328343
pub const PAGE_READONLY: DWORD = 2;
329344
pub const FILE_MAP_READ: DWORD = 4;
345+
pub const TH32CS_SNAPMODULE: DWORD = 0x00000008;
346+
pub const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
347+
pub const MAX_MODULE_NAME32: usize = 255;
348+
pub const MAX_PATH: usize = 260;
330349

331350
pub type DWORD = u32;
332351
pub type PDWORD = *mut u32;
@@ -350,6 +369,7 @@ ffi! {
350369
pub type SIZE_T = usize;
351370
pub type LPVOID = *mut c_void;
352371
pub type LPCVOID = *const c_void;
372+
pub type LPMODULEENTRY32W = *mut MODULEENTRY32W;
353373

354374
extern "system" {
355375
pub fn GetCurrentProcess() -> HANDLE;
@@ -401,6 +421,18 @@ ffi! {
401421
dwNumberOfBytesToMap: SIZE_T,
402422
) -> LPVOID;
403423
pub fn UnmapViewOfFile(lpBaseAddress: LPCVOID) -> BOOL;
424+
pub fn CreateToolhelp32Snapshot(
425+
dwFlags: DWORD,
426+
th32ProcessID: DWORD,
427+
) -> HANDLE;
428+
pub fn Module32FirstW(
429+
hSnapshot: HANDLE,
430+
lpme: LPMODULEENTRY32W,
431+
) -> BOOL;
432+
pub fn Module32NextW(
433+
hSnapshot: HANDLE,
434+
lpme: LPMODULEENTRY32W,
435+
) -> BOOL;
404436
}
405437
}
406438

tests/accuracy/main.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ type Pos = (&'static str, u32);
1616

1717
#[test]
1818
fn doit() {
19+
if
1920
// Skip musl which is by default statically linked and doesn't support
2021
// dynamic libraries.
21-
//
22-
// FIXME(#333) doesn't work on MinGW yet
23-
if !cfg!(target_env = "musl") && !(cfg!(windows) && cfg!(target_env = "gnu")) {
22+
!cfg!(target_env = "musl")
23+
// Skip MinGW on libbacktrace which doesn't have support for DLLs.
24+
&& !(cfg!(windows) && cfg!(target_env = "gnu") && cfg!(feature = "libbacktrace"))
25+
{
2426
// TODO(#238) this shouldn't have to happen first in this function, but
2527
// currently it does.
2628
let mut dir = std::env::current_exe().unwrap();

0 commit comments

Comments
 (0)