Skip to content

Commit 6043711

Browse files
committed
Support Mach-O backtraces without dsymutil
1 parent 9a0673e commit 6043711

File tree

7 files changed

+133
-10
lines changed

7 files changed

+133
-10
lines changed

.github/workflows/main.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ${{ matrix.os }}
1515
strategy:
1616
matrix:
17-
thing: [stable, beta, nightly, macos, windows-msvc64, windows-msvc32, windows-gnu64, windows-gnu32]
17+
thing: [stable, beta, nightly, macos, macos-nightly, windows-msvc64, windows-msvc32, windows-gnu64, windows-gnu32]
1818
include:
1919
- thing: stable
2020
os: ubuntu-latest
@@ -28,6 +28,9 @@ jobs:
2828
- thing: macos
2929
os: macos-latest
3030
rust: stable
31+
- thing: macos-nightly
32+
os: macos-latest
33+
rust: nightly
3134
# Note that these are on nightly due to rust-lang/rust#63700 not being
3235
# on stable yet
3336
- thing: windows-msvc64
@@ -82,6 +85,8 @@ jobs:
8285
if: contains(matrix.os, 'ubuntu')
8386
- run: RUSTFLAGS="-C link-arg=-Wl,--compress-debug-sections=zlib-gnu" cargo test --features gimli-symbolize
8487
if: contains(matrix.os, 'ubuntu')
88+
- run: cargo clean && RUSTFLAGS="-Z run-dsymutil=no" cargo test --features gimli-symbolize
89+
if: matrix.thing == 'macos-nightly'
8590
- run: cargo build --manifest-path crates/as-if-std/Cargo.toml
8691

8792
windows_arm64:

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ miniz_oxide = { version = "0.4.0", optional = true, default-features = false }
4141
version = "0.22"
4242
optional = true
4343
default-features = false
44-
features = ['read_core', 'elf', 'macho', 'pe', 'unaligned']
44+
features = ['read_core', 'elf', 'macho', 'pe', 'unaligned', 'archive']
4545

4646
[target.'cfg(windows)'.dependencies]
4747
winapi = { version = "0.3.3", optional = true }

crates/as-if-std/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ miniz_oxide = { version = "0.4.0", default-features = false }
2121
[dependencies.object]
2222
version = "0.22"
2323
default-features = false
24-
features = ['read_core', 'elf', 'macho', 'pe', 'unaligned']
24+
features = ['read_core', 'elf', 'macho', 'pe', 'unaligned', 'archive']
2525

2626
[features]
2727
default = ['gimli-symbolize']

src/symbolize/gimli.rs

+17-4
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ impl Cache {
552552
.next()
553553
}
554554

555-
fn mapping_for_lib<'a>(&'a mut self, lib: usize) -> Option<&'a Context<'a>> {
555+
fn mapping_for_lib<'a>(&'a mut self, lib: usize) -> Option<&'a mut Context<'a>> {
556556
let idx = self.mappings.iter().position(|(idx, _)| *idx == lib);
557557

558558
// Invariant: after this conditional completes without early returning
@@ -578,10 +578,10 @@ impl Cache {
578578
self.mappings.insert(0, (lib, mapping));
579579
}
580580

581-
let cx: &'a Context<'static> = &self.mappings[0].1.cx;
581+
let cx: &'a mut Context<'static> = &mut self.mappings[0].1.cx;
582582
// don't leak the `'static` lifetime, make sure it's scoped to just
583583
// ourselves
584-
Some(unsafe { mem::transmute::<&'a Context<'static>, &'a Context<'a>>(cx) })
584+
Some(unsafe { mem::transmute::<&'a mut Context<'static>, &'a mut Context<'a>>(cx) })
585585
}
586586
}
587587

@@ -618,7 +618,20 @@ pub unsafe fn resolve(what: ResolveWhat<'_>, cb: &mut dyn FnMut(&super::Symbol))
618618
});
619619
}
620620
}
621-
621+
if !any_frames {
622+
if let Some((object_cx, object_addr)) = cx.object.search_object_map(addr as u64) {
623+
if let Ok(mut frames) = object_cx.dwarf.find_frames(object_addr) {
624+
while let Ok(Some(frame)) = frames.next() {
625+
any_frames = true;
626+
call(Symbol::Frame {
627+
addr: addr as *mut c_void,
628+
location: frame.location,
629+
name: frame.function.map(|f| f.name.slice()),
630+
});
631+
}
632+
}
633+
}
634+
}
622635
if !any_frames {
623636
if let Some(name) = cx.object.search_symtab(addr as u64) {
624637
call(Symbol::Symtab {

src/symbolize/gimli/coff.rs

+4
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,8 @@ impl<'a> Object<'a> {
102102
};
103103
self.symbols[i].1.name(self.strings).ok()
104104
}
105+
106+
pub(super) fn search_object_map(&self, _addr: u64) -> Option<(&Context, u64)> {
107+
None
108+
}
105109
}

src/symbolize/gimli/elf.rs

+4
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ impl<'a> Object<'a> {
162162
None
163163
}
164164
}
165+
166+
pub(super) fn search_object_map(&self, _addr: u64) -> Option<(&Context, u64)> {
167+
None
168+
}
165169
}
166170

167171
fn decompress_zlib(input: &[u8], output: &mut [u8]) -> Option<()> {

src/symbolize/gimli/macho.rs

+100-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{Context, Mapping, Mmap, Path, Stash, Vec};
1+
use super::{Box, Context, Mapping, Mmap, Path, Stash, Vec};
22
use core::convert::TryInto;
33
use object::macho;
44
use object::read::macho::{MachHeader, Nlist, Section, Segment as _};
@@ -137,21 +137,32 @@ fn find_header(mut data: Bytes<'_>) -> Option<(&'_ Mach, Bytes<'_>)> {
137137
Mach::parse(data).ok().map(|h| (h, data))
138138
}
139139

140+
// This is used both for executables/libraries and source object files.
140141
pub struct Object<'a> {
141142
endian: NativeEndian,
142143
data: Bytes<'a>,
143144
dwarf: Option<&'a [MachSection]>,
144145
syms: Vec<(&'a [u8], u64)>,
146+
syms_sort_by_name: bool,
147+
// Only set for executables/libraries, and not the source object files.
148+
object_map: Option<object::ObjectMap<'a>>,
149+
// The outer Option is for lazy loading, and the inner Option allows load errors to be cached.
150+
object_mappings: Box<[Option<Option<Mapping>>]>,
145151
}
146152

147153
impl<'a> Object<'a> {
148154
fn parse(mach: &'a Mach, endian: NativeEndian, data: Bytes<'a>) -> Option<Object<'a>> {
155+
let is_object = mach.filetype(endian) == object::macho::MH_OBJECT;
149156
let mut dwarf = None;
150157
let mut syms = Vec::new();
158+
let mut syms_sort_by_name = false;
151159
let mut commands = mach.load_commands(endian, data).ok()?;
160+
let mut object_map = None;
161+
let mut object_mappings = Vec::new();
152162
while let Ok(Some(command)) = commands.next() {
153163
if let Some((segment, section_data)) = MachSegment::from_command(command).ok()? {
154-
if segment.name() == b"__DWARF" {
164+
// Object files should have all sections in a single unnamed segment load command.
165+
if segment.name() == b"__DWARF" || (is_object && segment.name() == b"") {
155166
dwarf = segment.sections(endian, section_data).ok();
156167
}
157168
} else if let Some(symtab) = command.symtab().ok()? {
@@ -167,7 +178,18 @@ impl<'a> Object<'a> {
167178
}
168179
})
169180
.collect();
170-
syms.sort_unstable_by_key(|(_, addr)| *addr);
181+
if is_object {
182+
// We never search object file symbols by address.
183+
// Instead, we already know the symbol name from the executable, and we
184+
// need to search by name to find the matching symbol in the object file.
185+
syms.sort_unstable_by_key(|(name, _)| *name);
186+
syms_sort_by_name = true;
187+
} else {
188+
syms.sort_unstable_by_key(|(_, addr)| *addr);
189+
let map = symbols.object_map(endian);
190+
object_mappings.resize_with(map.objects().len(), || None);
191+
object_map = Some(map);
192+
}
171193
}
172194
}
173195

@@ -176,6 +198,9 @@ impl<'a> Object<'a> {
176198
data,
177199
dwarf,
178200
syms,
201+
syms_sort_by_name,
202+
object_map,
203+
object_mappings: object_mappings.into_boxed_slice(),
179204
})
180205
}
181206

@@ -194,11 +219,83 @@ impl<'a> Object<'a> {
194219
}
195220

196221
pub fn search_symtab<'b>(&'b self, addr: u64) -> Option<&'b [u8]> {
222+
debug_assert!(!self.syms_sort_by_name);
197223
let i = match self.syms.binary_search_by_key(&addr, |(_, addr)| *addr) {
198224
Ok(i) => i,
199225
Err(i) => i.checked_sub(1)?,
200226
};
201227
let (sym, _addr) = self.syms.get(i)?;
202228
Some(sym)
203229
}
230+
231+
/// Try to load a context for an object file.
232+
///
233+
/// If dsymutil was not run, then the DWARF may be found in the source object files.
234+
pub(super) fn search_object_map<'b>(&'b mut self, addr: u64) -> Option<(&Context<'b>, u64)> {
235+
// `object_map` contains a map from addresses to symbols and object paths.
236+
// Look up the address and get a mapping for the object.
237+
let object_map = self.object_map.as_ref()?;
238+
let symbol = object_map.get(addr)?;
239+
let object_index = symbol.object_index();
240+
let mapping = self.object_mappings.get_mut(object_index)?;
241+
if mapping.is_none() {
242+
// No cached mapping, so create it.
243+
*mapping = Some(object_mapping(object_map.objects().get(object_index)?));
244+
}
245+
let cx: &'b Context<'static> = &mapping.as_ref()?.as_ref()?.cx;
246+
// Don't leak the `'static` lifetime, make sure it's scoped to just ourselves.
247+
let cx = unsafe { core::mem::transmute::<&'b Context<'static>, &'b Context<'b>>(cx) };
248+
249+
// We must translate the address in order to be able to look it up
250+
// in the DWARF in the object file.
251+
debug_assert!(cx.object.syms.is_empty() || cx.object.syms_sort_by_name);
252+
let i = cx
253+
.object
254+
.syms
255+
.binary_search_by_key(&symbol.name(), |(name, _)| *name)
256+
.ok()?;
257+
let object_symbol = cx.object.syms.get(i)?;
258+
let object_addr = addr
259+
.wrapping_sub(symbol.address())
260+
.wrapping_add(object_symbol.1);
261+
Some((cx, object_addr))
262+
}
263+
}
264+
265+
fn object_mapping(path: &[u8]) -> Option<Mapping> {
266+
use super::mystd::ffi::OsStr;
267+
use super::mystd::os::unix::prelude::*;
268+
269+
let map;
270+
271+
// `N_OSO` symbol names can be either `/path/to/object.o` or `/path/to/archive.a(object.o)`.
272+
let data = if let Some((archive_path, member_name)) = split_archive_path(path) {
273+
map = super::mmap(Path::new(OsStr::from_bytes(archive_path)))?;
274+
let archive = object::read::archive::ArchiveFile::parse(&map).ok()?;
275+
let member = archive
276+
.members()
277+
.filter_map(Result::ok)
278+
.find(|m| m.name() == member_name)?;
279+
Bytes(member.data())
280+
} else {
281+
map = super::mmap(Path::new(OsStr::from_bytes(path)))?;
282+
Bytes(&map)
283+
};
284+
285+
let (macho, data) = find_header(data)?;
286+
let endian = macho.endian().ok()?;
287+
let object = Object::parse(macho, endian, data)?;
288+
let stash = Stash::new();
289+
let inner = super::cx(&stash, object)?;
290+
Some(mk!(Mapping { map, inner, stash }))
291+
}
292+
293+
fn split_archive_path(path: &[u8]) -> Option<(&[u8], &[u8])> {
294+
let (last, path) = path.split_last()?;
295+
if *last != b')' {
296+
return None;
297+
}
298+
let index = path.iter().position(|&x| x == b'(')?;
299+
let (archive, rest) = path.split_at(index);
300+
Some((archive, &rest[1..]))
204301
}

0 commit comments

Comments
 (0)