#------------------------------------------------------------------------------- # elftools: dwarf/ranges.py # # DWARF ranges section decoding (.debug_ranges) # # Eli Bendersky (eliben@gmail.com) # This code is in the public domain #------------------------------------------------------------------------------- from __future__ import annotations import os from typing import IO, TYPE_CHECKING, NamedTuple, NoReturn from ..common.utils import struct_parse from ..common.exceptions import DWARFError from .dwarf_util import _iter_CUs_in_section if TYPE_CHECKING: from collections.abc import Callable, Iterator from ..construct.lib.container import Container from .compileunit import CompileUnit from .dwarfinfo import DWARFInfo from .structs import DWARFStructs class RangeEntry(NamedTuple): entry_offset: int entry_length: int begin_offset: int end_offset: int is_absolute: bool class BaseAddressEntry(NamedTuple): entry_offset: int base_address: int # If we ever see a list with a base entry at the end, there will be an error that entry_length is not a field. def _translate_startx_length(e: Container, cu: CompileUnit) -> RangeEntry: start_offset = cu.dwarfinfo.get_addr(cu, e.start_index) return RangeEntry(e.entry_offset, e.entry_length, start_offset, start_offset + e.length, True) # Maps parsed entry types to RangeEntry/BaseAddressEntry objects entry_translate: dict[str, Callable[[Container, CompileUnit], RangeEntry | BaseAddressEntry]] = { 'DW_RLE_base_address' : lambda e, cu: BaseAddressEntry(e.entry_offset, e.address), 'DW_RLE_offset_pair' : lambda e, cu: RangeEntry(e.entry_offset, e.entry_length, e.start_offset, e.end_offset, False), 'DW_RLE_start_end' : lambda e, cu: RangeEntry(e.entry_offset, e.entry_length, e.start_address, e.end_address, True), 'DW_RLE_start_length' : lambda e, cu: RangeEntry(e.entry_offset, e.entry_length, e.start_address, e.start_address + e.length, True), 'DW_RLE_base_addressx': lambda e, cu: BaseAddressEntry(e.entry_offset, cu.dwarfinfo.get_addr(cu, e.index)), # type: ignore[has-type] 'DW_RLE_startx_endx' : lambda e, cu: RangeEntry(e.entry_offset, e.entry_length, cu.dwarfinfo.get_addr(cu, e.start_index), cu.dwarfinfo.get_addr(cu, e.end_index), True), # type: ignore[has-type] 'DW_RLE_startx_length': _translate_startx_length } class RangeListsPair: """For those binaries that contain both a debug_ranges and a debug_rnglists section, it holds a RangeLists object for both and forwards API calls to the right one based on the CU version. """ def __init__( self, streamv4: IO[bytes], streamv5: IO[bytes], structs: DWARFStructs, dwarfinfo: DWARFInfo | None = None, ) -> None: self._ranges = RangeLists(streamv4, structs, 4, dwarfinfo) self._rnglists = RangeLists(streamv5, structs, 5, dwarfinfo) def get_range_list_at_offset( self, offset: int, cu: CompileUnit | None = None, ) -> list[RangeEntry | BaseAddressEntry]: """Forwards the call to either v4 section or v5 one, depending on DWARF version in the CU. """ if cu is None: raise DWARFError("For this binary, \"cu\" needs to be provided") section = self._rnglists if cu.header.version >= 5 else self._ranges return section.get_range_list_at_offset(offset, cu) def get_range_list_at_offset_ex(self, offset: int) -> Container: """Gets an untranslated v5 rangelist from the v5 section. """ return self._rnglists.get_range_list_at_offset_ex(offset) def iter_range_lists(self) -> NoReturn: """Tricky proposition, since the structure of ranges and rnglists is not identical. A realistic readelf implementation needs to be aware of both. """ raise DWARFError("Iterating through two sections is not supported") def iter_CUs(self) -> Iterator[CompileUnit]: """See RangeLists.iter_CUs() CU structure is only present in DWARFv5 rnglists sections. A well written section dumper should check if one is present. """ return self._rnglists.iter_CUs() def iter_CU_range_lists_ex(self, cu: Container) -> Iterator[CompileUnit]: """See RangeLists.iter_CU_range_lists_ex() CU structure is only present in DWARFv5 rnglists sections. A well written section dumper should check if one is present. """ return self._rnglists.iter_CU_range_lists_ex(cu) def translate_v5_entry( self, entry: Container, cu: CompileUnit, ) -> RangeEntry | BaseAddressEntry: """Forwards a V5 entry translation request to the V5 section """ return self._rnglists.translate_v5_entry(entry, cu) class RangeLists: """ A single range list is a Python list consisting of RangeEntry or BaseAddressEntry objects. Since v0.29, two new parameters - version and dwarfinfo version is used to distinguish DWARFv5 rnglists section from the DWARF<=4 ranges section. Only the 4/5 distinction matters. The dwarfinfo is needed for enumeration, because enumeration requires scanning the DIEs, because ranges may overlap, even on DWARF<=4 """ def __init__( self, stream: IO[bytes], structs: DWARFStructs, version: int, dwarfinfo: DWARFInfo | None, ) -> None: self.stream = stream self.structs = structs self._max_addr = 2 ** (self.structs.address_size * 8) - 1 self.version = version self._dwarfinfo = dwarfinfo def get_range_list_at_offset( self, offset: int, cu: CompileUnit | None = None, ) -> list[RangeEntry | BaseAddressEntry]: """ Get a range list at the given offset in the section. The cu argument is necessary if the ranges section is a DWARFv5 debug_rnglists one, and the target rangelist contains indirect encodings """ self.stream.seek(offset, os.SEEK_SET) return self._parse_range_list_from_stream(cu) def get_range_list_at_offset_ex(self, offset: int) -> Container: """Get a DWARF v5 range list, addresses and offsets unresolved, at the given offset in the section """ return struct_parse(self.structs.Dwarf_rnglists_entries, self.stream, offset) def iter_range_lists(self) -> Iterator[list[RangeEntry | BaseAddressEntry]]: """ Yields all range lists found in the section according to readelf rules. Scans the DIEs for rangelist offsets, then pulls those. Returned rangelists are always translated into lists of BaseAddressEntry/RangeEntry objects. """ # Rangelists can overlap. That is, one DIE points at the rangelist beginning, and another # points at the middle of the same. Therefore, enumerating them is not a well defined # operation - do you count those as two different (but overlapping) ones, or as a single one? # For debugging utility, you want two. That's what readelf does. For faithfully # representing the section contents, you want one. # That was the behaviour of pyelftools 0.28 and below - calling # parse until the stream end. Leaving aside the question of correctless, # that's uncompatible with readelf. ver5 = self.version >= 5 # This maps list offset to CU cu_map = {die.attributes['DW_AT_ranges'].value : cu for cu in self._dwarfinfo.iter_CUs() # type: ignore[union-attr] # ty: ignore[unresolved-attribute] for die in cu.iter_DIEs() if 'DW_AT_ranges' in die.attributes and (cu['version'] >= 5) == ver5} all_offsets = list(cu_map.keys()) all_offsets.sort() for offset in all_offsets: yield self.get_range_list_at_offset(offset, cu_map[offset]) def iter_CUs(self) -> Iterator[CompileUnit]: """For DWARF5 returns an array of objects, where each one has an array of offsets """ if self.version < 5: raise DWARFError("CU iteration in rnglists is not supported with DWARF<5") assert self._dwarfinfo is not None structs = next(self._dwarfinfo.iter_CUs()).structs # Just pick one return _iter_CUs_in_section(self.stream, structs, structs.Dwarf_rnglists_CU_header) def iter_CU_range_lists_ex(self, cu: Container) -> Iterator[CompileUnit]: """For DWARF5, returns untranslated rangelists in the CU, where CU comes from iter_CUs above """ stream = self.stream stream.seek(cu.offset_table_offset + (64 if cu.is64 else 32) * cu.offset_count) while stream.tell() < cu.offset_after_length + cu.unit_length: yield struct_parse(self.structs.Dwarf_rnglists_entries, stream) def translate_v5_entry( self, entry: Container, cu: CompileUnit, ) -> RangeEntry | BaseAddressEntry: """Translates entries in a DWARFv5 rangelist from raw parsed format to a list of BaseAddressEntry/RangeEntry, using the CU """ return entry_translate[entry.entry_type](entry, cu) #------ PRIVATE ------# def _parse_range_list_from_stream( self, cu: CompileUnit | None, ) -> list[RangeEntry | BaseAddressEntry]: if self.version >= 5: assert cu is not None return list(entry_translate[entry.entry_type](entry, cu) for entry in struct_parse(self.structs.Dwarf_rnglists_entries, self.stream)) else: lst: list[RangeEntry | BaseAddressEntry] = [] while True: entry_offset = self.stream.tell() begin_offset = struct_parse( self.structs.the_Dwarf_target_addr, self.stream) end_offset = struct_parse( self.structs.the_Dwarf_target_addr, self.stream) if begin_offset == 0 and end_offset == 0: # End of list - we're done. break elif begin_offset == self._max_addr: # Base address selection entry lst.append(BaseAddressEntry(entry_offset=entry_offset, base_address=end_offset)) else: # Range entry lst.append(RangeEntry( entry_offset=entry_offset, entry_length=self.stream.tell() - entry_offset, begin_offset=begin_offset, end_offset=end_offset, is_absolute=False)) return lst