Skip to content

Commit

Permalink
Add support for Mach-O unpacked debuginfo
Browse files Browse the repository at this point in the history
N_OSO symbols in the executable file contain the path to the object files.
  • Loading branch information
philipc committed May 21, 2024
1 parent 307be19 commit 5ae81ac
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 13 deletions.
12 changes: 11 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ jobs:
run: |
cargo test --verbose --release
cargo test --verbose --release --features all
- name: Test unpacked debuginfo
if: (matrix.os == "ubuntu-latest") || (matrix.os == "macOS-latest")
run: cargo clean && cargo test correctness --features loader
env:
RUSTFLAGS: "-C split-debuginfo=unpacked"
- name: Test packed debuginfo
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: cargo clean && cargo test correctness --features loader
env:
RUSTFLAGS: "-C split-debuginfo=packed"

features:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -75,7 +85,7 @@ jobs:
- name: Install rust
run: rustup update 1.65.0 && rustup default 1.65.0
- name: Build
run: cargo build
run: cargo build --features loader

rustfmt:
runs-on: ubuntu-latest
Expand Down
143 changes: 131 additions & 12 deletions src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use memmap2::Mmap;
use object::{Object, ObjectSection, SymbolMap, SymbolMapName};
use typed_arena::Arena;

use crate::lazy::LazyCell;
use crate::{
Context, FrameIter, Location, LocationRangeIter, LookupContinuation, LookupResult,
SplitDwarfLoad,
Expand Down Expand Up @@ -95,7 +96,7 @@ impl Loader {
///
/// This calls [`Context::find_location`] with the given address.
pub fn find_location(&self, probe: u64) -> Result<Option<Location<'_>>> {
self.borrow_internal(|i, _data, _mmap| Ok(i.ctx.find_location(probe)?))
self.borrow_internal(|i, data, mmap| i.find_location(probe, data, mmap))
}

/// Return source file and lines for a range of addresses.
Expand All @@ -106,8 +107,8 @@ impl Loader {
probe_low: u64,
probe_high: u64,
) -> Result<LocationRangeIter<'_, Reader>> {
self.borrow_internal(|i, _data, _mmap| {
Ok(i.ctx.find_location_range(probe_low, probe_high)?)
self.borrow_internal(|i, data, mmap| {
i.find_location_range(probe_low, probe_high, data, mmap)
})
}

Expand All @@ -130,6 +131,10 @@ struct LoaderInternal<'a> {
relative_address_base: u64,
symbols: SymbolMap<SymbolMapName<'a>>,
dwarf_package: Option<gimli::DwarfPackage<Reader<'a>>>,
// Map from address to Mach-O object file path.
object_map: object::ObjectMap<'a>,
// A context for each Mach-O object file.
objects: Vec<LazyCell<Option<ObjectContext<'a>>>>,
}

impl<'a> LoaderInternal<'a> {
Expand All @@ -145,6 +150,9 @@ impl<'a> LoaderInternal<'a> {

let relative_address_base = object.relative_address_base();
let symbols = object.symbol_map();
let object_map = object.object_map();
let mut objects = Vec::new();
objects.resize_with(object_map.objects().len(), LazyCell::new);

// Load supplementary object file.
// TODO: use debuglink and debugaltlink
Expand Down Expand Up @@ -233,20 +241,71 @@ impl<'a> LoaderInternal<'a> {
relative_address_base,
symbols,
dwarf_package,
object_map,
objects,
})
}

fn ctx(
&self,
probe: u64,
arena_data: &'a Arena<Vec<u8>>,
arena_mmap: &'a Arena<Mmap>,
) -> (&Context<Reader<'a>>, u64) {
self.object_ctx(probe, arena_data, arena_mmap)
.unwrap_or((&self.ctx, probe))
}

fn object_ctx(
&self,
probe: u64,
arena_data: &'a Arena<Vec<u8>>,
arena_mmap: &'a Arena<Mmap>,
) -> Option<(&Context<Reader<'a>>, u64)> {
let symbol = self.object_map.get(probe)?;
let object_context = self.objects[symbol.object_index()]
.borrow_with(|| {
ObjectContext::new(symbol.object(&self.object_map), arena_data, arena_mmap)
})
.as_ref()?;
object_context.ctx(symbol.name(), probe - symbol.address())
}

fn find_symbol(&self, probe: u64) -> Option<&str> {
self.symbols.get(probe).map(|x| x.name())
}

fn find_location(
&'a self,
probe: u64,
arena_data: &'a Arena<Vec<u8>>,
arena_mmap: &'a Arena<Mmap>,
) -> Result<Option<Location<'a>>> {
let (ctx, probe) = self.ctx(probe, arena_data, arena_mmap);
Ok(ctx.find_location(probe)?)
}

fn find_location_range(
&self,
probe_low: u64,
probe_high: u64,
arena_data: &'a Arena<Vec<u8>>,
arena_mmap: &'a Arena<Mmap>,
) -> Result<LocationRangeIter<'a, Reader>> {
let (ctx, probe) = self.ctx(probe_low, arena_data, arena_mmap);
// TODO: handle ranges that cover multiple objects
let probe_high = probe + (probe_high - probe_low);
Ok(ctx.find_location_range(probe, probe_high)?)
}

fn find_frames(
&self,
probe: u64,
arena_data: &'a Arena<Vec<u8>>,
arena_mmap: &'a Arena<Mmap>,
) -> Result<FrameIter<'a, Reader>> {
let mut frames = self.ctx.find_frames(probe);
let (ctx, probe) = self.ctx(probe, arena_data, arena_mmap);
let mut frames = ctx.find_frames(probe);
loop {
let (load, continuation) = match frames {
LookupResult::Output(output) => return Ok(output?),
Expand Down Expand Up @@ -274,12 +333,12 @@ impl<'a> LoaderInternal<'a> {
// Determine the path to the DWO file.
let mut path = PathBuf::new();
if let Some(p) = load.comp_dir.as_ref() {
path.push(convert_path(p)?);
path.push(convert_path(p.slice())?);
}
let Some(p) = load.path.as_ref() else {
return Ok(None);
};
path.push(convert_path(p)?);
path.push(convert_path(p.slice())?);

// Load the DWO file, ignoring errors.
let dwo = (|| {
Expand All @@ -306,6 +365,58 @@ impl<'a> LoaderInternal<'a> {
}
}

struct ObjectContext<'a> {
ctx: Context<Reader<'a>>,
symbols: SymbolMap<SymbolMapName<'a>>,
}

impl<'a> ObjectContext<'a> {
fn new(
object_name: &[u8],
arena_data: &'a Arena<Vec<u8>>,
arena_mmap: &'a Arena<Mmap>,
) -> Option<Self> {
// `N_OSO` symbol names can be either `/path/to/object.o` or `/path/to/archive.a(object.o)`.
let (path, member_name) = split_archive_path(object_name).unwrap_or((object_name, None));
let file = File::open(convert_path(path).ok()?).ok()?;
let map = &**arena_mmap.alloc(unsafe { Mmap::map(&file) }.ok()?);
let data = if let Some(member_name) = member_name {
let archive = object::read::archive::ArchiveFile::parse(map).ok()?;
let member = archive.members().find_map(|member| {
let member = member.ok()?;
if member.name() == member_name {
Some(member)
} else {
None
}
})?;
member.data(map).ok()?
} else {
map
};
let object = object::File::parse(data).ok()?;
let endian = if object.is_little_endian() {
gimli::RunTimeEndian::Little
} else {
gimli::RunTimeEndian::Big
};
let dwarf =
gimli::Dwarf::load(|id| load_section(Some(id.name()), &object, endian, arena_data))
.ok()?;
let ctx = Context::from_dwarf(dwarf).ok()?;
let symbols = object.symbol_map();
Some(ObjectContext { ctx, symbols })
}

fn ctx(&self, symbol_name: &[u8], probe: u64) -> Option<(&Context<Reader<'a>>, u64)> {
self.symbols
.symbols()
.iter()
.find(|symbol| symbol.name().as_bytes() == symbol_name)
.map(|symbol| (&self.ctx, probe + symbol.address()))
}
}

fn load_section<'input, Endian: gimli::Endianity>(
name: Option<&'static str>,
file: &object::File<'input>,
Expand All @@ -323,16 +434,24 @@ fn load_section<'input, Endian: gimli::Endianity>(
}

#[cfg(unix)]
fn convert_path<R: gimli::Reader<Endian = gimli::RunTimeEndian>>(r: &R) -> Result<PathBuf> {
fn convert_path(bytes: &[u8]) -> Result<PathBuf> {
use std::os::unix::ffi::OsStrExt;
let bytes = r.to_slice()?;
let s = OsStr::from_bytes(&bytes);
let s = OsStr::from_bytes(bytes);
Ok(PathBuf::from(s))
}

#[cfg(not(unix))]
fn convert_path<R: gimli::Reader<Endian = gimli::RunTimeEndian>>(r: &R) -> Result<PathBuf> {
let bytes = r.to_slice()?;
let s = std::str::from_utf8(&bytes)?;
fn convert_path(bytes: &[u8]) -> Result<PathBuf> {
let s = std::str::from_utf8(bytes)?;
Ok(PathBuf::from(s))
}

fn split_archive_path(path: &[u8]) -> Option<(&[u8], Option<&[u8]>)> {
let (last, path) = path.split_last()?;
if *last != b')' {
return None;
}
let index = path.iter().position(|&x| x == b'(')?;
let (archive, rest) = path.split_at(index);
Some((archive, Some(&rest[1..])))
}

0 comments on commit 5ae81ac

Please sign in to comment.