From 0f72cb7990c7039258b7988602b0aec83c5864f7 Mon Sep 17 00:00:00 2001 From: "Seva Alekseyev (he/him)" Date: Tue, 16 Jul 2024 09:08:20 -0400 Subject: [PATCH] CIEv4 and FDE ahead of its CIE (#563) * CIEv4 and FDE ahead of its CIE * Comment --- elftools/dwarf/callframe.py | 28 ++++++++++----------- elftools/dwarf/descriptions.py | 6 ++++- elftools/dwarf/structs.py | 22 +++++++--------- elftools/elf/segments.py | 10 ++++++-- scripts/dwarfdump.py | 9 +++++-- scripts/readelf.py | 11 ++++++-- test/run_readelf_tests.py | 9 +++++++ test/test_dwarf_expr.py | 3 ++- test/testfiles_for_readelf/dwarf_v4cie.elf | Bin 0 -> 19188 bytes 9 files changed, 62 insertions(+), 36 deletions(-) create mode 100644 test/testfiles_for_readelf/dwarf_v4cie.elf diff --git a/elftools/dwarf/callframe.py b/elftools/dwarf/callframe.py index 8fa9990f..2caaf2f7 100644 --- a/elftools/dwarf/callframe.py +++ b/elftools/dwarf/callframe.py @@ -6,7 +6,7 @@ # Eli Bendersky (eliben@gmail.com) # This code is in the public domain #------------------------------------------------------------------------------- -import copy +import copy, os from collections import namedtuple from ..common.utils import ( struct_parse, dwarf_assert, preserve_stream_pos, iterbytes) @@ -84,10 +84,13 @@ def _parse_entries(self): def _parse_entry_at(self, offset): """ Parse an entry from self.stream starting with the given offset. Return the entry object. self.stream will point right after the - entry. + entry (even if pulled from the cache). """ if offset in self._entry_cache: - return self._entry_cache[offset] + entry = self._entry_cache[offset] + self.stream.seek(entry.header.length + + entry.structs.initial_length_field_size(), os.SEEK_CUR) + return entry entry_length = struct_parse( self.base_structs.the_Dwarf_uint32, self.stream, offset) @@ -97,6 +100,9 @@ def _parse_entry_at(self, offset): dwarf_format = 64 if entry_length == 0xFFFFFFFF else 32 + # Theoretically possible to have a DWARF bitness transition here. + # DWARF version doesn't matter (CIEs are versioned separately), endianness can't change. + # The structs are cached though, so no extraneous creation. entry_structs = DWARFStructs( little_endian=self.base_structs.little_endian, dwarf_format=dwarf_format, @@ -124,15 +130,6 @@ def _parse_entry_at(self, offset): else: header = self._parse_fde_header(entry_structs, offset) - - # If this is DWARF version 4 or later, we can have a more precise - # address size, read from the CIE header. - if not self.for_eh_frame and entry_structs.dwarf_version >= 4: - entry_structs = DWARFStructs( - little_endian=entry_structs.little_endian, - dwarf_format=entry_structs.dwarf_format, - address_size=header.address_size) - # If the augmentation string is not empty, hope to find a length field # in order to skip the data specified augmentation. if is_CIE: @@ -161,7 +158,7 @@ def _parse_entry_at(self, offset): entry_structs, self.stream.tell(), end_offset) if is_CIE: - self._entry_cache[offset] = CIE( + entry = CIE( header=header, instructions=instructions, offset=offset, augmentation_dict=aug_dict, augmentation_bytes=aug_bytes, @@ -169,13 +166,14 @@ def _parse_entry_at(self, offset): else: # FDE cie = self._parse_cie_for_fde(offset, header, entry_structs) - self._entry_cache[offset] = FDE( + entry = FDE( header=header, instructions=instructions, offset=offset, structs=entry_structs, cie=cie, augmentation_bytes=aug_bytes, lsda_pointer=lsda_pointer, ) - return self._entry_cache[offset] + self._entry_cache[offset] = entry + return entry def _parse_instructions(self, structs, offset, end_offset): """ Parse a list of CFI instructions from self.stream, starting with diff --git a/elftools/dwarf/descriptions.py b/elftools/dwarf/descriptions.py index 2da46556..a1ebf4b9 100644 --- a/elftools/dwarf/descriptions.py +++ b/elftools/dwarf/descriptions.py @@ -100,6 +100,8 @@ def _full_reg_name(regnum): instr.args[1] * cie['data_alignment_factor']) elif name in ('DW_CFA_def_cfa_offset', 'DW_CFA_GNU_args_size'): s += ' %s: %s\n' % (name, instr.args[0]) + elif name == 'DW_CFA_def_cfa_offset_sf': + s += ' %s: %s\n' % (name, instr.args[0]*entry.cie['data_alignment_factor']) elif name == 'DW_CFA_def_cfa_expression': expr_dumper = ExprDumper(entry.structs) # readelf output is missing a colon for DW_CFA_def_cfa_expression @@ -618,7 +620,7 @@ def _init_lookups(self): for n in range(0, 32): self._ops_with_decimal_arg.add('DW_OP_breg%s' % n) - self._ops_with_two_decimal_args = set(['DW_OP_bregx', 'DW_OP_bit_piece']) + self._ops_with_two_decimal_args = set(['DW_OP_bregx']) self._ops_with_hex_arg = set( ['DW_OP_addr', 'DW_OP_call2', 'DW_OP_call4', 'DW_OP_call_ref']) @@ -674,5 +676,7 @@ def _dump_to_string(self, opcode, opcode_name, args, cu_offset=None): return "%s: <0x%x> %d byte block: %s " % (opcode_name, args[0] + cu_offset, len(args[1]), ' '.join("%x" % b for b in args[1])) elif opcode_name in ('DW_OP_GNU_regval_type', 'DW_OP_regval_type'): return "%s: %d (%s) <0x%x>" % (opcode_name, args[0], describe_reg_name(args[0], _MACHINE_ARCH), args[1] + cu_offset) + elif opcode_name == 'DW_OP_bit_piece': + return '%s: size: %s offset: %s' % (opcode_name, args[0], args[1]) else: return '' % opcode_name diff --git a/elftools/dwarf/structs.py b/elftools/dwarf/structs.py index df6075e8..75c1e797 100644 --- a/elftools/dwarf/structs.py +++ b/elftools/dwarf/structs.py @@ -449,23 +449,19 @@ def _create_callframe_entry_headers(self): self.Dwarf_offset('CIE_id'), self.Dwarf_uint8('version'), CString('augmentation'), + If(lambda ctx: ctx.version >= 4, self.Dwarf_uint8('address_size')), + If(lambda ctx: ctx.version >= 4, self.Dwarf_uint8('segment_size')), self.Dwarf_uleb128('code_alignment_factor'), self.Dwarf_sleb128('data_alignment_factor'), - self.Dwarf_uleb128('return_address_register')) + IfThenElse('return_address_register', lambda ctx: ctx.version > 1, + self.Dwarf_uleb128(''), + self.Dwarf_uint8(''))) self.EH_CIE_header = self.Dwarf_CIE_header - # The CIE header was modified in DWARFv4. - if self.dwarf_version == 4: - self.Dwarf_CIE_header = Struct('Dwarf_CIE_header', - self.Dwarf_initial_length('length'), - self.Dwarf_offset('CIE_id'), - self.Dwarf_uint8('version'), - CString('augmentation'), - self.Dwarf_uint8('address_size'), - self.Dwarf_uint8('segment_size'), - self.Dwarf_uleb128('code_alignment_factor'), - self.Dwarf_sleb128('data_alignment_factor'), - self.Dwarf_uleb128('return_address_register')) + # The CIE header was modified in DWARFv4, but the + # CIE header version is driven by the version # in the header + # itself, independent of the DWARF version + # in the CUs. self.Dwarf_FDE_header = Struct('Dwarf_FDE_header', self.Dwarf_initial_length('length'), diff --git a/elftools/elf/segments.py b/elftools/elf/segments.py index 0c318e17..9ddda235 100644 --- a/elftools/elf/segments.py +++ b/elftools/elf/segments.py @@ -69,9 +69,14 @@ def section_in_segment(self, section): # The third condition is the 'strict' one - an empty section will # not match at the very end of the segment (unless the segment is # also zero size, which is handled by the second condition). + + # Seva 2024-07-12: a zero length section at a zero offset + # in a zero length segment should match - in GNU readelf, p_memsz + # is unsigned, on a zero length segment p_memsz-1 wraps around + # and the third condition matches. if not (secaddr >= vaddr and secaddr - vaddr + section['sh_size'] <= self['p_memsz'] and - secaddr - vaddr <= self['p_memsz'] - 1): + (self['p_memsz'] == 0 or secaddr - vaddr <= self['p_memsz'] - 1)): return False # If we've come this far and it's a NOBITS section, it's in the segment @@ -83,9 +88,10 @@ def section_in_segment(self, section): # Same logic as with secaddr vs. vaddr checks above, just on offsets in # the file + # Seva 2024-07-12: similar discrepancy with readelf from unsignedness of p_filesz return (secoffset >= poffset and secoffset - poffset + section['sh_size'] <= self['p_filesz'] and - secoffset - poffset <= self['p_filesz'] - 1) + (self['p_filesz'] == 0 or secoffset - poffset <= self['p_filesz'] - 1)) class InterpSegment(Segment): diff --git a/scripts/dwarfdump.py b/scripts/dwarfdump.py index 6d46ba59..d92f18be 100644 --- a/scripts/dwarfdump.py +++ b/scripts/dwarfdump.py @@ -84,6 +84,8 @@ def _safe_DIE_linkage_name(die, default=None): def _desc_ref(attr, die, extra=''): if extra: extra = " \"%s\"" % extra + # TODO: leading zeros on the addend to CU - sometimes present, sometimes not. + # Check by the LLVM sources. return "cu + 0x%04x => {0x%08x}%s" % ( attr.raw_value, die.cu.cu_offset + attr.raw_value, @@ -99,7 +101,7 @@ def _desc_strx(attr, die): return "indexed (%08x) string = \"%s\"" % (attr.raw_value, bytes2str(attr.value).replace("\\", "\\\\")) FORM_DESCRIPTIONS = dict( - DW_FORM_string=lambda attr, die: "\"%s\"" % (bytes2str(attr.value),), + DW_FORM_string=lambda attr, die: "\"%s\"" % (bytes2str(attr.value).replace("\\", "\\\\"),), DW_FORM_strp=lambda attr, die: " .debug_str[0x%08x] = \"%s\"" % (attr.raw_value, bytes2str(attr.value).replace("\\", "\\\\")), DW_FORM_strx1=_desc_strx, DW_FORM_strx2=_desc_strx, @@ -391,7 +393,10 @@ def dump_info(self): '(0x%08x)' % die.get_parent().offset if die.get_parent() is not None else '')) for attr_name in die.attributes: attr = die.attributes[attr_name] - self._emitline(" %s [%s] (%s)" % (attr_name, attr.form, self.describe_attr_value(die, attr))) + self._emitline(" %s [%s] (%s)" % ( + attr_name if isinstance(attr_name, str) else "DW_AT_unknown_%x" % (attr_name,), + attr.form, + self.describe_attr_value(die, attr))) else: self._emitline("0x%08x: NULL" % (die.offset,)) parent = die.get_parent() diff --git a/scripts/readelf.py b/scripts/readelf.py index 32496eb9..2a7d49c8 100755 --- a/scripts/readelf.py +++ b/scripts/readelf.py @@ -1282,6 +1282,9 @@ def _dump_frames_info(self, section, cfi_entries): self._format_hex(entry['CIE_id'], fieldsize=8, lead0x=False))) self._emitline(' Version: %d' % entry['version']) self._emitline(' Augmentation: "%s"' % bytes2str(entry['augmentation'])) + if(entry['version'] >= 4): + self._emitline(' Pointer Size: %d' % entry['address_size']) + self._emitline(' Segment Size: %d' % entry['segment_size']) self._emitline(' Code alignment factor: %u' % entry['code_alignment_factor']) self._emitline(' Data alignment factor: %d' % entry['data_alignment_factor']) self._emitline(' Return address column: %d' % entry['return_address_register']) @@ -1293,9 +1296,11 @@ def _dump_frames_info(self, section, cfi_entries): self._emitline() elif isinstance(entry, FDE): + # Readelf bug #31973 + length = entry['length'] if entry.cie.offset < entry.offset else entry.cie['length'] self._emitline('\n%08x %s %s FDE cie=%08x pc=%s..%s' % ( entry.offset, - self._format_hex(entry['length'], fullhex=True, lead0x=False), + self._format_hex(length, fullhex=True, lead0x=False), self._format_hex(entry['CIE_pointer'], fieldsize=8, lead0x=False), entry.cie.offset, self._format_hex(entry['initial_location'], fullhex=True, lead0x=False), @@ -1428,9 +1433,11 @@ def _dump_frames_interp_info(self, section, cfi_entries): ra_regnum = entry['return_address_register'] elif isinstance(entry, FDE): + # Readelf bug #31973 - FDE length misreported if FDE precedes its CIE + length = entry['length'] if entry.cie.offset < entry.offset else entry.cie['length'] self._emitline('\n%08x %s %s FDE cie=%08x pc=%s..%s' % ( entry.offset, - self._format_hex(entry['length'], fullhex=True, lead0x=False), + self._format_hex(length, fullhex=True, lead0x=False), self._format_hex(entry['CIE_pointer'], fieldsize=8, lead0x=False), entry.cie.offset, self._format_hex(entry['initial_location'], fullhex=True, lead0x=False), diff --git a/test/run_readelf_tests.py b/test/run_readelf_tests.py index c1fc48c2..9c6747fb 100755 --- a/test/run_readelf_tests.py +++ b/test/run_readelf_tests.py @@ -84,6 +84,15 @@ def run_test_on_file(filename, verbose=False, opt=None): testlog.info('.......................SKIPPED') continue + # TODO(sevaa): excluding the binary with CIE ahead of FDE until binutils' bug #31975 is fixed + if "dwarf_v4cie" in filename and option == "--debug-dump=frames-interp": + continue + + # TODO(sevaa): excluding the binary with unaligned aranges entries. Readelf tried to recover + # but produces nonsensical output, but ultimately it's a toolchain bug (in IAR I presume). + if "dwarf_v4cie" in filename and option == "--debug-dump=aranges": + continue + # sevaa says: there is another shorted out test; in dwarf_lineprogramv5.elf, the two bytes at 0x2072 were # patched from 0x07 0x10 to 00 00. # Those represented the second instruction in the first FDE in .eh_frame. This changed the instruction diff --git a/test/test_dwarf_expr.py b/test/test_dwarf_expr.py index 93dc30f8..0d228d83 100644 --- a/test/test_dwarf_expr.py +++ b/test/test_dwarf_expr.py @@ -38,7 +38,8 @@ def test_basic_single(self): 'DW_OP_regx: 16 (rip)') self.assertEqual(self.visitor.dump_expr([0x9d, 0x8f, 0x0A, 0x90, 0x01]), - 'DW_OP_bit_piece: 1295 144') + # Explaining the arguments is what the latest readelf does + 'DW_OP_bit_piece: size: 1295 offset: 144') self.assertEqual(self.visitor.dump_expr([0x0e, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00]), 'DW_OP_const8u: 71777214294589695') diff --git a/test/testfiles_for_readelf/dwarf_v4cie.elf b/test/testfiles_for_readelf/dwarf_v4cie.elf new file mode 100644 index 0000000000000000000000000000000000000000..f3cc7b8727be80021e498bd520416cb66ef0dcde GIT binary patch literal 19188 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 literal 0 HcmV?d00001