diff --git a/plugins/dwarf/dwarf_import/src/dwarfdebuginfo.rs b/plugins/dwarf/dwarf_import/src/dwarfdebuginfo.rs index 993e035991..91b77e48cb 100644 --- a/plugins/dwarf/dwarf_import/src/dwarfdebuginfo.rs +++ b/plugins/dwarf/dwarf_import/src/dwarfdebuginfo.rs @@ -14,7 +14,7 @@ use crate::{ functions::FrameBase, - helpers::{get_uid, resolve_specification, DieReference}, + helpers::{get_uid, rebase_offset, resolve_specification, DieReference}, ReaderType, }; @@ -867,8 +867,7 @@ impl DebugInfoBuilder { } if let Some(address) = func.address.as_mut() { - let (diff, overflowed) = bv.start().overflowing_sub(bv.original_image_base()); - if !overflowed { + if let Some(diff) = rebase_offset(bv) { *address = (*address).overflowing_add(diff).0; // rebase the address let existing_functions = bv.functions_at(*address); match existing_functions.len().cmp(&1) { @@ -884,6 +883,13 @@ impl DebugInfoBuilder { } } + if let Some(diff) = rebase_offset(bv).filter(|&diff| diff != 0) { + self.data_variables = std::mem::take(&mut self.data_variables) + .into_iter() + .map(|(address, variable)| (address.wrapping_add(diff), variable)) + .collect(); + } + self } diff --git a/plugins/dwarf/dwarf_import/src/helpers.rs b/plugins/dwarf/dwarf_import/src/helpers.rs index abffc6d8f5..96545b0aed 100644 --- a/plugins/dwarf/dwarf_import/src/helpers.rs +++ b/plugins/dwarf/dwarf_import/src/helpers.rs @@ -307,6 +307,11 @@ pub(crate) fn get_subrange_size(entry: &DebuggingInformationEntry } } +/// How far `bv` is rebased from the image base of the file, if not below it. +pub(crate) fn rebase_offset(bv: &BinaryView) -> Option { + bv.start().checked_sub(bv.original_image_base()) +} + // Get the start address of a function pub(crate) fn get_start_address( dwarf: &Dwarf, diff --git a/plugins/dwarf/dwarf_import/src/lib.rs b/plugins/dwarf/dwarf_import/src/lib.rs index 8d9068b92b..d73421293a 100644 --- a/plugins/dwarf/dwarf_import/src/lib.rs +++ b/plugins/dwarf/dwarf_import/src/lib.rs @@ -18,13 +18,13 @@ mod functions; mod helpers; mod types; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use crate::dwarfdebuginfo::{DebugInfoBuilder, DebugInfoBuilderContext}; use crate::functions::parse_function_entry; use crate::helpers::{ - find_local_debug_file_from_path, get_attr_die, get_name, get_uid, DieReference, + find_local_debug_file_from_path, get_attr_die, get_name, get_uid, rebase_offset, DieReference, }; use crate::types::parse_variable; @@ -34,7 +34,7 @@ use binaryninja::{ debuginfo::{CustomDebugInfoParser, DebugInfo, DebugInfoParser}, settings::Settings, }; -use dwarfreader::create_section_reader_object; +use dwarfreader::{create_section_reader_object, SectionAddresses}; use functions::parse_lexical_block; use gimli::{ @@ -45,7 +45,7 @@ use gimli::{ use helpers::{get_build_id, load_debug_info_for_build_id}; use iset::IntervalMap; use object::read::macho::FatArch; -use object::{Object, ObjectSection}; +use object::{BinaryFormat, Object, ObjectKind, ObjectSection, SectionFlags}; trait ReaderType: Reader {} impl> ReaderType for T {} @@ -488,7 +488,10 @@ where } } -fn parse_range_data_offsets(file: &object::File) -> Result, String> { +fn parse_range_data_offsets( + file: &object::File, + section_addresses: &SectionAddresses, +) -> Result, String> { let dwo_file = file.section_by_name(".debug_info.dwo").is_some(); let endian = match file.endianness() { object::Endianness::Little => gimli::RunTimeEndian::Little, @@ -496,7 +499,7 @@ fn parse_range_data_offsets(file: &object::File) -> Result }; let section_reader = |section_id: SectionId| -> _ { - create_section_reader_object(section_id, &file, endian, dwo_file) + create_section_reader_object(section_id, &file, endian, dwo_file, section_addresses) }; if file.section_by_name(".eh_frame").is_some() { @@ -537,6 +540,53 @@ fn parse_range_data_offsets(file: &object::File) -> Result } } +/// Where `bv` placed the allocated sections of `file`, if it's a relocatable ELF object. +/// +/// The addresses are before rebasing, like the other addresses read from the DWARF info, since +/// [`DebugInfoBuilder::post_process`] rebases them all. Linked files keep their own addresses. +fn object_section_addresses(bv: &BinaryView, file: &object::File) -> SectionAddresses { + let mut section_addresses = SectionAddresses::new(); + if file.format() != BinaryFormat::Elf || file.kind() != ObjectKind::Relocatable { + return section_addresses; + } + + let rebase = rebase_offset(bv).unwrap_or(0); + let mut seen_names = HashSet::new(); + for section in file.sections() { + let Ok(name) = section.name() else { + continue; + }; + // The ELF view keeps the first section with a given name as is, and suffixes the + // name of each later one with `#` and its section index. + let view_name = if seen_names.insert(name) { + name.to_string() + } else { + format!("{name}#{}", section.index().0) + }; + + let SectionFlags::Elf { sh_flags } = section.flags() else { + continue; + }; + if sh_flags & u64::from(object::elf::SHF_ALLOC) == 0 || section.size() == 0 { + continue; + } + + match bv.section_by_name(view_name.as_str()) { + Some(view_section) if view_section.len() as u64 == section.size() => { + section_addresses.insert( + section.index(), + view_section.start().wrapping_sub(rebase), + ); + } + _ => tracing::debug!( + "Section {} of the object is not in the view; addresses in it will not be relocated", + view_name + ), + } + } + section_addresses +} + fn parse_dwarf( bv: &BinaryView, debug_file: &object::File, @@ -552,7 +602,9 @@ fn parse_dwarf( None => bv.address_size(), }; - let range_data_offsets = parse_range_data_offsets(debug_file).unwrap_or_default(); + let section_addresses = object_section_addresses(bv, debug_file); + let range_data_offsets = + parse_range_data_offsets(debug_file, §ion_addresses).unwrap_or_default(); let dwo_file = debug_file.section_by_name(".debug_info.dwo").is_some(); let endian = match debug_file.endianness() { @@ -561,7 +613,13 @@ fn parse_dwarf( }; let mut section_reader = |section_id: SectionId| -> _ { - create_section_reader_object(section_id, &debug_file, endian, dwo_file) + create_section_reader_object( + section_id, + &debug_file, + endian, + dwo_file, + §ion_addresses, + ) }; let mut dwarf = Dwarf::load(&mut section_reader) @@ -581,7 +639,13 @@ fn parse_dwarf( let sup_dwo_file = sup_file.section_by_name(".debug_info.dwo").is_some(); let sup_section_reader = |section_id: SectionId| -> _ { - create_section_reader_object(section_id, &sup_file, sup_endian, sup_dwo_file) + create_section_reader_object( + section_id, + &sup_file, + sup_endian, + sup_dwo_file, + &SectionAddresses::new(), + ) }; if let Err(e) = dwarf.load_sup(sup_section_reader) { tracing::error!("Failed to load supplementary file: {}", e); diff --git a/plugins/dwarf/shared/src/lib.rs b/plugins/dwarf/shared/src/lib.rs index 5502788a27..543a9ab968 100644 --- a/plugins/dwarf/shared/src/lib.rs +++ b/plugins/dwarf/shared/src/lib.rs @@ -13,7 +13,7 @@ // limitations under the License. use gimli::{EndianRcSlice, Endianity, RunTimeEndian, SectionId}; -use object::{Object, ObjectSection}; +use object::{elf, Object, ObjectSection, ObjectSymbol, RelocationTarget}; use binaryninja::{ binary_view::{BinaryView, BinaryViewBase}, @@ -22,6 +22,7 @@ use binaryninja::{ }; use binaryninja::settings::QueryOptions; +use std::collections::HashMap; use std::rc::Rc; ////////////////////// // Dwarf Validation @@ -39,6 +40,12 @@ pub enum Error { #[error("{0}")] ObjectError(#[from] object::Error), + + #[error("relocation at {0:#x} is outside its section")] + RelocationOutOfBounds(u64), + + #[error("invalid relocation at {offset:#x}: {reason}")] + InvalidRelocation { offset: u64, reason: &'static str }, } pub fn is_non_dwo_dwarf(view: &BinaryView) -> bool { @@ -102,9 +109,16 @@ pub fn create_section_reader( let raw_view = view.raw_view().unwrap(); let view_data = raw_view.read_vec(0, raw_view.len() as usize); let file = object::File::parse(&*view_data)?; - create_section_reader_object(section_id, &file, endian, is_dwo) + create_section_reader_object(section_id, &file, endian, is_dwo, &SectionAddresses::new()) } +/// The addresses to place the sections of a relocatable object at, by section index. +/// +/// Every section of a relocatable object is at address 0, so an address in its debug info is +/// an offset into the section that the relocation's symbol is defined in. Adding the address +/// the section was placed at turns it into an address in the view. +pub type SectionAddresses = HashMap; + #[derive(Debug, Clone)] pub struct RelocationMap(Rc); @@ -115,14 +129,223 @@ impl Default for RelocationMap { } impl RelocationMap { - fn add(&mut self, file: &object::File, section: &object::Section) -> Result<(), Error> { + /// Adds the relocations of `section`, whose uncompressed contents are `data`. Relocations + /// that can't be expressed as an entry in the map are applied to `data` instead. + /// + /// Relocations that go in the map and refer into a section in `section_addresses` are moved + /// to where that section was placed. The ones applied to `data` aren't: they compute + /// differences between labels in the same section, which don't depend on where it is. + fn add( + &mut self, + file: &object::File, + section: &object::Section, + data: &mut [u8], + section_addresses: &SectionAddresses, + ) -> Result<(), Error> { let map = Rc::get_mut(&mut self.0).expect("Failed to get mutable reference to RelocationMap"); - for (offset, relocation) in section.relocations() { + let mut riscv = RiscvRelocations::default(); + for (offset, mut relocation) in section.relocations() { + let keep = match file.architecture() { + object::Architecture::Riscv32 | object::Architecture::Riscv64 => { + riscv.apply(file, offset, &relocation, data)? + } + _ => true, + }; + if !keep { + continue; + } + if let Some(address) = target_section_address(file, &relocation, section_addresses) { + relocation.set_addend(relocation.addend().wrapping_add(address as i64)); + } map.add(file, offset, relocation)? } - Ok(()) + riscv.finish() + } +} + +/// The address in `section_addresses` of the section that `relocation` refers into. +fn target_section_address( + file: &object::File, + relocation: &object::Relocation, + section_addresses: &SectionAddresses, +) -> Option { + if section_addresses.is_empty() { + return None; + } + let section_index = match relocation.target() { + RelocationTarget::Symbol(index) => file.symbol_by_index(index).ok()?.section_index()?, + RelocationTarget::Section(index) => index, + _ => return None, + }; + section_addresses.get(§ion_index).copied() +} + +/// Applies the RISC-V relocations that `object::read::RelocationMap` can't represent. +/// +/// With linker relaxation enabled, the assembler can't resolve differences between labels in +/// code, so it emits `R_RISCV_ADD*`/`R_RISCV_SUB*`, `R_RISCV_SET*`/`R_RISCV_SUB6` and +/// `R_RISCV_SET_ULEB128`/`R_RISCV_SUB_ULEB128` pairs at the same offset for the linker to +/// evaluate (e.g. `DW_AT_high_pc`, line table advances, CFA advances and location list ranges). +/// They are applied to the section data in order, as `perform_relocation` in binutils' +/// `bfd/elfnn-riscv.c` does; since the file isn't relaxed, this gives the unrelaxed difference. +#[derive(Default)] +struct RiscvRelocations { + /// The offset and value of an `R_RISCV_SET_ULEB128` waiting for its `R_RISCV_SUB_ULEB128`. + pending_set_uleb128: Option<(u64, u64)>, +} + +impl RiscvRelocations { + /// Applies `relocation` to `data` if needed. Returns `false` if the relocation has been + /// handled and shouldn't be added to the relocation map. + fn apply( + &mut self, + file: &object::File, + offset: u64, + relocation: &object::Relocation, + data: &mut [u8], + ) -> Result { + let object::RelocationFlags::Elf { r_type } = relocation.flags() else { + return Ok(true); + }; + // The psABI requires `R_RISCV_SUB_ULEB128` to immediately follow its `R_RISCV_SET_ULEB128`. + if let Some((set_offset, set_value)) = self.pending_set_uleb128.take() { + if r_type != elf::R_RISCV_SUB_ULEB128 || offset != set_offset { + return Err(Error::InvalidRelocation { + offset: set_offset, + reason: "R_RISCV_SET_ULEB128 without a matching R_RISCV_SUB_ULEB128", + }); + } + // Like binutils, ignore the addend: older assemblers emitted a bogus non-zero one. + let value = set_value.wrapping_sub(riscv_symbol_value(file, offset, relocation)?); + write_uleb128_in_place(data, offset, value)?; + return Ok(false); + } + + let (size, apply): (usize, fn(u64, u64) -> u64) = match r_type { + elf::R_RISCV_NONE | elf::R_RISCV_RELAX => return Ok(false), + elf::R_RISCV_ADD8 => (1, u64::wrapping_add), + elf::R_RISCV_ADD16 => (2, u64::wrapping_add), + elf::R_RISCV_ADD32 => (4, u64::wrapping_add), + elf::R_RISCV_ADD64 => (8, u64::wrapping_add), + elf::R_RISCV_SUB6 => (1, |old, value| { + (old & !0x3f) | (old.wrapping_sub(value) & 0x3f) + }), + elf::R_RISCV_SUB8 => (1, u64::wrapping_sub), + elf::R_RISCV_SUB16 => (2, u64::wrapping_sub), + elf::R_RISCV_SUB32 => (4, u64::wrapping_sub), + elf::R_RISCV_SUB64 => (8, u64::wrapping_sub), + elf::R_RISCV_SET6 => (1, |old, value| (old & !0x3f) | (value & 0x3f)), + elf::R_RISCV_SET8 => (1, |_, value| value), + elf::R_RISCV_SET16 => (2, |_, value| value), + elf::R_RISCV_SET32 => (4, |_, value| value), + elf::R_RISCV_SET_ULEB128 => { + let value = riscv_symbol_value(file, offset, relocation)? + .wrapping_add(relocation.addend() as u64); + self.pending_set_uleb128 = Some((offset, value)); + return Ok(false); + } + elf::R_RISCV_SUB_ULEB128 => { + return Err(Error::InvalidRelocation { + offset, + reason: "R_RISCV_SUB_ULEB128 without a preceding R_RISCV_SET_ULEB128", + }) + } + // Leave the rest (`R_RISCV_32` and `R_RISCV_64` in particular) to the map. + _ => return Ok(true), + }; + let value = + riscv_symbol_value(file, offset, relocation)?.wrapping_add(relocation.addend() as u64); + update_word_in_place(data, offset, size, file.is_little_endian(), |old| { + apply(old, value) + })?; + Ok(false) + } + + /// Checks that no relocation pair was left incomplete. + fn finish(self) -> Result<(), Error> { + match self.pending_set_uleb128 { + Some((offset, _)) => Err(Error::InvalidRelocation { + offset, + reason: "R_RISCV_SET_ULEB128 without a matching R_RISCV_SUB_ULEB128", + }), + None => Ok(()), + } + } +} + +/// Returns `S`, the value of the symbol or section that `relocation` refers to. +/// +/// In a relocatable object this is the offset into the symbol's section, not where the view +/// placed it, which is fine for the label differences it's used for. +fn riscv_symbol_value( + file: &object::File, + offset: u64, + relocation: &object::Relocation, +) -> Result { + match relocation.target() { + RelocationTarget::Symbol(index) => Ok(file.symbol_by_index(index)?.address()), + RelocationTarget::Section(index) => Ok(file.section_by_index(index)?.address()), + RelocationTarget::Absolute => Ok(0), + _ => Err(Error::InvalidRelocation { + offset, + reason: "unsupported relocation target", + }), + } +} + +/// Replaces the `size`-byte word at `offset` in `data` with `update` applied to it, truncated. +fn update_word_in_place( + data: &mut [u8], + offset: u64, + size: usize, + little_endian: bool, + update: impl FnOnce(u64) -> u64, +) -> Result<(), Error> { + let bytes = usize::try_from(offset) + .ok() + .and_then(|start| data.get_mut(start..)?.get_mut(..size)) + .ok_or(Error::RelocationOutOfBounds(offset))?; + let mut word = [0; 8]; + let old = if little_endian { + word[..size].copy_from_slice(bytes); + u64::from_le_bytes(word) + } else { + word[8 - size..].copy_from_slice(bytes); + u64::from_be_bytes(word) + }; + let new = update(old); + if little_endian { + bytes.copy_from_slice(&new.to_le_bytes()[..size]); + } else { + bytes.copy_from_slice(&new.to_be_bytes()[8 - size..]); + } + Ok(()) +} + +/// Replaces the ULEB128 at `offset` in `data` with `value`, keeping its encoded length: the +/// data that follows can't move, and the assembler leaves enough room for the final value. +fn write_uleb128_in_place(data: &mut [u8], offset: u64, mut value: u64) -> Result<(), Error> { + let bytes = usize::try_from(offset) + .ok() + .and_then(|start| { + let bytes = data.get_mut(start..)?; + let len = bytes.iter().position(|byte| byte & 0x80 == 0)? + 1; + bytes.get_mut(..len) + }) + .ok_or(Error::RelocationOutOfBounds(offset))?; + let last = bytes.len() - 1; + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = (value & 0x7f) as u8 | if i < last { 0x80 } else { 0 }; + value >>= 7; + } + if value != 0 { + return Err(Error::InvalidRelocation { + offset, + reason: "ULEB128 value doesn't fit in the space the assembler left", + }); } + Ok(()) } impl gimli::read::Relocate for RelocationMap { @@ -142,6 +365,7 @@ pub fn create_section_reader_object( file: &object::File, endian: Endian, is_dwo: bool, + section_addresses: &SectionAddresses, ) -> Result>, Error> { let mut relocations = RelocationMap::default(); let name = if is_dwo { @@ -153,11 +377,12 @@ pub fn create_section_reader_object( }; let data = match name.and_then(|name| file.section_by_name(name)) { Some(ref section) => { + let mut data = section.uncompressed_data()?.into_owned(); // DWO sections never have relocations, so don't bother. if !is_dwo { - relocations.add(file, section)?; + relocations.add(file, section, &mut data, section_addresses)?; } - section.uncompressed_data()?.into_owned() + data } // Use a non-zero capacity so that `ReaderOffsetId`s are unique. None => Vec::with_capacity(1),