Files

423 lines
20 KiB
Python

#-------------------------------------------------------------------------------
# elftools: dwarf/locationlists.py
#
# DWARF location lists section decoding (.debug_loc)
#
# 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
from ..common.exceptions import DWARFError
from ..common.utils import struct_parse
from .dwarf_util import _iter_CUs_in_section
if TYPE_CHECKING:
from collections.abc import Callable, Iterator, Mapping
from ..construct.lib.container import Container
from .compileunit import CompileUnit
from .die import DIE, AttributeValue
from .dwarfinfo import DWARFInfo
from .structs import DWARFStructs
from .typeunit import TypeUnit
class LocationExpr(NamedTuple):
loc_expr: list[int]
class LocationEntry(NamedTuple):
entry_offset: int
entry_length: int
begin_offset: int
end_offset: int
loc_expr: list[int]
is_absolute: bool
class BaseAddressEntry(NamedTuple):
entry_offset: int
entry_length: int
base_address: int
class LocationViewPair(NamedTuple):
entry_offset: int
begin: int
end: int
if TYPE_CHECKING:
_Location = LocationExpr | LocationEntry | BaseAddressEntry | LocationViewPair
def _translate_startx_length(e: Container, cu: CompileUnit | TypeUnit) -> LocationEntry:
start_offset: int = cu.dwarfinfo.get_addr(cu, e.start_index)
return LocationEntry(e.entry_offset, e.entry_length, start_offset, start_offset + e.length, e.loc_expr, True)
# Maps parsed entries to the tuples above; LocationViewPair is mapped elsewhere
entry_translate: dict[str, Callable[[Container, CompileUnit | TypeUnit], _Location]] = {
'DW_LLE_base_address' : lambda e, cu: BaseAddressEntry(e.entry_offset, e.entry_length, e.address),
'DW_LLE_offset_pair' : lambda e, cu: LocationEntry(e.entry_offset, e.entry_length, e.start_offset, e.end_offset, e.loc_expr, False),
'DW_LLE_start_length' : lambda e, cu: LocationEntry(e.entry_offset, e.entry_length, e.start_address, e.start_address + e.length, e.loc_expr, True),
'DW_LLE_start_end' : lambda e, cu: LocationEntry(e.entry_offset, e.entry_length, e.start_address, e.end_address, e.loc_expr, True),
'DW_LLE_default_location': lambda e, cu: LocationEntry(e.entry_offset, e.entry_length, -1, -1, e.loc_expr, True),
'DW_LLE_base_addressx' : lambda e, cu: BaseAddressEntry(e.entry_offset, e.entry_length, cu.dwarfinfo.get_addr(cu, e.index)), # type: ignore[has-type]
'DW_LLE_startx_endx' : lambda e, cu: LocationEntry(e.entry_offset, e.entry_length, cu.dwarfinfo.get_addr(cu, e.start_index), cu.dwarfinfo.get_addr(cu, e.end_index), e.loc_expr, True), # type: ignore[has-type]
'DW_LLE_startx_length' : _translate_startx_length
}
class LocationListsPair:
"""For those binaries that contain both a debug_loc and a debug_loclists section,
it holds a LocationLists object for both and forwards API calls to the right one.
"""
def __init__(
self,
streamv4: IO[bytes],
streamv5: IO[bytes],
structs: DWARFStructs,
dwarfinfo: DWARFInfo | None = None,
) -> None:
self._loc = LocationLists(streamv4, structs, 4, dwarfinfo)
self._loclists = LocationLists(streamv5, structs, 5, dwarfinfo)
def get_location_list_at_offset(self, offset: int, die: DIE | None = None) -> list[_Location]:
"""See LocationLists.get_location_list_at_offset().
"""
if die is None:
raise DWARFError("For this binary, \"die\" needs to be provided")
section = self._loclists if die.cu.header.version >= 5 else self._loc
return section.get_location_list_at_offset(offset, die)
def iter_location_lists(self) -> Iterator[BaseAddressEntry | LocationEntry]:
"""Tricky proposition, since the structure of loc and loclists
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 LocationLists.iter_CUs()
There are no CUs in DWARFv4 sections.
"""
raise DWARFError("Iterating through two sections is not supported")
class LocationLists:
""" A single location list is a Python list consisting of LocationEntry or
BaseAddressEntry objects.
Starting with DWARF5, it may also contain LocationViewPair, but only
if scanning the section, never when requested for a DIE attribute.
The default location entries are returned as LocationEntry with
begin_offset == end_offset == -1
Version determines whether the executable contains a debug_loc
section, or a DWARFv5 style debug_loclists one. Only the 4/5
distinction matters.
Dwarfinfo is only needed for DWARFv5 location entry encodings
that contain references to other sections (e. g. DW_LLE_startx_endx),
and only for location list enumeration.
"""
def __init__(
self,
stream: IO[bytes],
structs: DWARFStructs,
version: int = 4,
dwarfinfo: DWARFInfo | None = None,
) -> None:
self.stream = stream
self.structs = structs
self.dwarfinfo = dwarfinfo
self.version = version
self._max_addr: int = 2 ** (self.structs.address_size * 8) - 1
def get_location_list_at_offset(self, offset: int, die: DIE | None = None) -> list[_Location]:
""" Get a location list at the given offset in the section.
Passing the die is only neccessary in DWARF5+, for decoding
location entry encodings that contain references to other sections.
"""
self.stream.seek(offset, os.SEEK_SET)
if self.version >= 5:
if die is None:
raise DWARFError("For this binary, \"die\" needs to be provided")
return self._parse_location_list_from_stream_v5(die.cu)
return self._parse_location_list_from_stream()
def iter_location_lists(self) -> Iterator[list[_Location]]:
""" Iterates through location lists and view pairs. Returns lists of
LocationEntry, BaseAddressEntry, and LocationViewPair objects.
"""
# The location lists section was never meant for sequential access.
# Location lists are referenced by DIE attributes by offset or by index.
# As of DWARFv5, it may contain, in addition to proper location lists,
# location list view pairs, which are referenced by the nonstandard DW_AT_GNU_locviews
# attribute. A set of locview pairs (which is a couple of ULEB128 values) may preceed
# a location list; the former is referenced by the DW_AT_GNU_locviews attribute, the
# latter - by DW_AT_location (in the same DIE). Binutils' readelf dumps those.
# There is a view pair for each location-type entry in the list.
#
# Also, the section may contain gaps.
#
# Taking a cue from binutils, we would have to scan this section while looking at
# what's in DIEs.
ver5 = self.version >= 5
stream = self.stream
stream.seek(0, os.SEEK_END)
endpos = stream.tell()
stream.seek(0, os.SEEK_SET)
# Need to provide support for DW_AT_GNU_locviews. They are interspersed in
# the locations section, no way to tell where short of checking all DIEs
all_offsets = set() # Set of offsets where either a locview pair set can be found, or a view-less loclist
locviews = dict() # Map of locview offset to the respective loclist offset
cu_map = dict() # Map of loclist offsets to CUs
assert self.dwarfinfo is not None
for cu in self.dwarfinfo.iter_CUs():
cu_ver: int = cu['version']
if (cu_ver >= 5) == ver5:
for die in cu.iter_DIEs():
# A combination of location and locviews means there is a location list
# preceed by several locview pairs
if 'DW_AT_GNU_locviews' in die.attributes:
assert('DW_AT_location' in die.attributes and
LocationParser._attribute_has_loc_list(die.attributes['DW_AT_location'], cu_ver))
views_offset: int = die.attributes['DW_AT_GNU_locviews'].value
list_offset: int = die.attributes['DW_AT_location'].value
locviews[views_offset] = list_offset
cu_map[list_offset] = cu
all_offsets.add(views_offset)
# Scan other attributes for location lists
for key in die.attributes:
attr = die.attributes[key]
if ((key != 'DW_AT_location' or 'DW_AT_GNU_locviews' not in die.attributes) and
LocationParser.attribute_has_location(attr, cu_ver) and
LocationParser._attribute_has_loc_list(attr, cu_ver)):
list_offset = attr.value
all_offsets.add(list_offset)
cu_map[list_offset] = cu
sorted_offsets = sorted(all_offsets)
if ver5:
# Loclists section is organized as an array of CUs, each length prefixed.
# We don't assume that the CUs go in the same order as the ones in info.
offset_index = 0
while stream.tell() < endpos:
# We are at the start of the CU block in the loclists now
cu_header = struct_parse(self.structs.Dwarf_loclists_CU_header, stream)
assert(cu_header.version == 5)
# GNU binutils supports two traversal modes: by offsets in CU header, and sequential.
# We don't have a binary for the former yet. On an off chance that we one day might,
# let's parse the header anyway.
cu_end_offset: int = cu_header.offset_after_length + cu_header.unit_length
# Unit_length includes the header but doesn't include the length
while stream.tell() < cu_end_offset:
# Skip the gap to the next object
next_offset = sorted_offsets[offset_index]
if next_offset == stream.tell(): # At an object, either a loc list or a loc view pair
locview_pairs = self._parse_locview_pairs(locviews)
entries = self._parse_location_list_from_stream_v5(cu_map[stream.tell()])
yield locview_pairs + entries
offset_index += 1
else: # We are at a gap - skip the gap to the next object or to the next CU
if next_offset > cu_end_offset: # Gap at the CU end - the next object is in the next CU
next_offset = cu_end_offset # And implicitly quit the loop within the CU
stream.seek(next_offset, os.SEEK_SET)
else:
for offset in sorted_offsets:
list_offset = locviews.get(offset, offset)
if cu_map[list_offset].header.version < 5:
stream.seek(offset, os.SEEK_SET)
locview_pairs = self._parse_locview_pairs(locviews)
entries = self._parse_location_list_from_stream()
yield locview_pairs + entries
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 loclists 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_loclists_CU_header)
#------ PRIVATE ------#
def _parse_location_list_from_stream(self) -> list[_Location]:
lst: list[_Location] = []
while True:
entry_offset = self.stream.tell()
begin_offset: int = struct_parse(
self.structs.the_Dwarf_target_addr, self.stream)
end_offset: int = 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
entry_length = self.stream.tell() - entry_offset
lst.append(BaseAddressEntry(entry_offset=entry_offset, entry_length=entry_length, base_address=end_offset))
else:
# Location list entry
expr_len: int = struct_parse(
self.structs.the_Dwarf_uint16, self.stream)
loc_expr: list[int] = [struct_parse(self.structs.the_Dwarf_uint8,
self.stream)
for i in range(expr_len)]
entry_length = self.stream.tell() - entry_offset
lst.append(LocationEntry(
entry_offset=entry_offset,
entry_length=entry_length,
begin_offset=begin_offset,
end_offset=end_offset,
loc_expr=loc_expr,
is_absolute = False))
return lst
def _parse_location_list_from_stream_v5(
self,
cu: CompileUnit | TypeUnit | None = None,
) -> list[_Location]:
""" Returns an array with BaseAddressEntry and LocationEntry.
No terminator entries.
The cu argument is necessary if the section is a
DWARFv5 debug_loclists one, and the target loclist
contains indirect encodings.
"""
return [entry_translate[entry.entry_type](entry, cu) # type: ignore[arg-type] # ty: ignore[invalid-argument-type]
for entry
in struct_parse(self.structs.Dwarf_loclists_entries, self.stream)]
# From V5 style entries to a LocationEntry/BaseAddressEntry
def _translate_entry_v5(self, entry: Container, die: DIE) -> _Location:
off: int = entry.entry_offset
len: int = entry.entry_end_offset - off
type: str = entry.entry_type
if type == 'DW_LLE_base_address':
return BaseAddressEntry(off, len, entry.address)
elif type == 'DW_LLE_offset_pair':
return LocationEntry(off, len, entry.start_offset, entry.end_offset, entry.loc_expr, False)
elif type == 'DW_LLE_start_length':
return LocationEntry(off, len, entry.start_address, entry.start_address + entry.length, entry.loc_expr, True)
elif type == 'DW_LLE_start_end': # No test for this yet, but the format seems straightforward
return LocationEntry(off, len, entry.start_address, entry.end_address, entry.loc_expr, True)
elif type == 'DW_LLE_default_location': # No test for this either, and this is new in the API
return LocationEntry(off, len, -1, -1, entry.loc_expr, True)
elif type in ('DW_LLE_base_addressx', 'DW_LLE_startx_endx', 'DW_LLE_startx_length'):
# We don't have sample binaries for those LLEs. Their proper parsing would
# require knowing the CU context (so that indices can be resolved to code offsets)
raise NotImplementedError("Location list entry type %s is not supported yet" % (type,))
else:
raise DWARFError(False, "Unknown DW_LLE code: %s" % (type,))
# Locviews is the dict, mapping locview offsets to corresponding loclist offsets
def _parse_locview_pairs(self, locviews: Mapping[int, int]) -> list[LocationViewPair]:
stream = self.stream
list_offset: int | None = locviews.get(stream.tell(), None)
pairs: list[LocationViewPair] = []
if list_offset is not None:
while stream.tell() < list_offset:
pair = struct_parse(self.structs.Dwarf_locview_pair, stream)
pairs.append(LocationViewPair(pair.entry_offset, pair.begin, pair.end))
assert(stream.tell() == list_offset)
return pairs
class LocationParser:
""" A parser for location information in DIEs.
Handles both location information contained within the attribute
itself (represented as a LocationExpr object) and references to
location lists in the .debug_loc section (represented as a
list).
"""
def __init__(self, location_lists: LocationLists | LocationListsPair | None) -> None:
self.location_lists = location_lists
@staticmethod
def attribute_has_location(attr: AttributeValue, dwarf_version: int) -> bool:
""" Checks if a DIE attribute contains location information.
"""
return (LocationParser._attribute_is_loclistptr_class(attr) and
(LocationParser._attribute_has_loc_expr(attr, dwarf_version) or
LocationParser._attribute_has_loc_list(attr, dwarf_version)))
def parse_from_attribute(
self,
attr: AttributeValue,
dwarf_version: int,
die: DIE | None = None,
) -> LocationExpr | list[_Location]:
""" Parses a DIE attribute and returns either a LocationExpr or
a list.
"""
if self.attribute_has_location(attr, dwarf_version):
if self._attribute_has_loc_expr(attr, dwarf_version):
return LocationExpr(attr.value)
elif self._attribute_has_loc_list(attr, dwarf_version):
assert self.location_lists is not None
return self.location_lists.get_location_list_at_offset(
attr.value, die)
# We don't yet know if the DIE context will be needed.
# We might get it without a full tree traversal using
# attr.offset as a key, but we assume a good DWARF5
# aware consumer would pass a DIE along.
raise ValueError("Attribute does not have location information")
#------ PRIVATE ------#
@staticmethod
def _attribute_has_loc_expr(attr: AttributeValue, dwarf_version: int) -> bool:
return ((dwarf_version < 4 and attr.form.startswith('DW_FORM_block') and
not attr.name == 'DW_AT_const_value') or
attr.form == 'DW_FORM_exprloc')
@staticmethod
def _attribute_has_loc_list(attr: AttributeValue, dwarf_version: int) -> bool:
return (((dwarf_version < 4 and
attr.form in ('DW_FORM_data1', 'DW_FORM_data2', 'DW_FORM_data4', 'DW_FORM_data8') and
not attr.name == 'DW_AT_const_value') or
attr.form in ('DW_FORM_sec_offset', 'DW_FORM_loclistx')) and
not LocationParser._attribute_is_constant(attr, dwarf_version))
# Starting with DWARF3, DW_AT_data_member_location may contain an integer offset
# instead of a location expression. Need to prevent false positives on attribute_has_location().
# As for DW_AT_upper_bound/DW_AT_count, we've seen it in form DW_FORM_locexpr in a V5 binary. usually it's a constant,
# but the constant sholdn't be misinterpreted as a loclist pointer.
@staticmethod
def _attribute_is_constant(attr: AttributeValue, dwarf_version: int) -> bool:
return (((dwarf_version >= 3 and attr.name == 'DW_AT_data_member_location') or
(attr.name in ('DW_AT_upper_bound', 'DW_AT_count'))) and
attr.form in ('DW_FORM_data1', 'DW_FORM_data2', 'DW_FORM_data4', 'DW_FORM_data8', 'DW_FORM_sdata', 'DW_FORM_udata'))
@staticmethod
def _attribute_is_loclistptr_class(attr: AttributeValue) -> bool:
return (attr.name in ( 'DW_AT_location', 'DW_AT_string_length',
'DW_AT_const_value', 'DW_AT_return_addr',
'DW_AT_data_member_location',
'DW_AT_frame_base', 'DW_AT_segment',
'DW_AT_static_link', 'DW_AT_use_location',
'DW_AT_vtable_elem_location',
'DW_AT_call_value',
'DW_AT_GNU_call_site_value',
'DW_AT_GNU_call_site_target',
'DW_AT_GNU_call_site_data_value',
'DW_AT_call_target',
'DW_AT_call_target_clobbered',
'DW_AT_call_data_location',
'DW_AT_call_data_value',
'DW_AT_upper_bound',
'DW_AT_count'))