| #!/usr/bin/env fuchsia-vendored-python | |
| # | |
| # Copyright 2017 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import argparse | |
| import json | |
| import mmap | |
| import os | |
| import struct | |
| import sys | |
| from collections import namedtuple | |
| from contextlib import contextmanager | |
| # Standard ELF constants. | |
| ELFMAG = b"\x7fELF" | |
| EI_CLASS = 4 | |
| ELFCLASS32 = 1 | |
| ELFCLASS64 = 2 | |
| EI_DATA = 5 | |
| ELFDATA2LSB = 1 | |
| ELFDATA2MSB = 2 | |
| ET_EXEC = 2 | |
| ET_DYN = 3 | |
| EM_386 = 3 | |
| EM_ARM = 40 | |
| EM_X86_64 = 62 | |
| EM_AARCH64 = 183 | |
| PT_LOAD = 1 | |
| PT_DYNAMIC = 2 | |
| PT_INTERP = 3 | |
| PT_NOTE = 4 | |
| PT_GNU_RELRO = 0x6474E552 | |
| PF_X = 1 | |
| PF_W = 2 | |
| PF_R = 4 | |
| DT_NEEDED = 1 | |
| DT_PLTRELSZ = 2 | |
| DT_STRTAB = 5 | |
| DT_RELA = 7 | |
| DT_RELASZ = 8 | |
| DT_REL = 17 | |
| DT_RELSZ = 18 | |
| DT_RELRSZ = 35 | |
| DT_RELR = 36 | |
| DT_PLTREL = 20 | |
| DT_SONAME = 14 | |
| DT_RELACOUNT = 0x6FFFFFF9 | |
| DT_RELCOUNT = 0x6FFFFFFA | |
| NT_GNU_BUILD_ID = 3 | |
| SHT_SYMTAB = 2 | |
| SHF_ALLOC = 2 | |
| class elf_note( | |
| namedtuple( | |
| "elf_note", | |
| [ | |
| "name", | |
| "type", | |
| "desc", | |
| ], | |
| ) | |
| ): | |
| # An ELF note is identified by (name_string, type_integer). | |
| def ident(self): | |
| return (self.name, self.type) | |
| def is_build_id(self): | |
| return self.ident() == ("GNU\0", NT_GNU_BUILD_ID) | |
| def build_id_hex(self): | |
| if self.is_build_id(): | |
| return "".join(("%02x" % byte) for byte in self.desc) | |
| return None | |
| def __repr__(self): | |
| return "elf_note(%r, %#x, <%d bytes>)" % ( | |
| self.name, | |
| self.type, | |
| len(self.desc), | |
| ) | |
| def gen_elf(): | |
| # { 'Struct1': (ELFCLASS32 fields, ELFCLASS64 fields), | |
| # 'Struct2': fields_same_for_both, ... } | |
| elf_types = { | |
| "Ehdr": ( | |
| [ | |
| ("e_ident", "16s"), | |
| ("e_type", "H"), | |
| ("e_machine", "H"), | |
| ("e_version", "I"), | |
| ("e_entry", "I"), | |
| ("e_phoff", "I"), | |
| ("e_shoff", "I"), | |
| ("e_flags", "I"), | |
| ("e_ehsize", "H"), | |
| ("e_phentsize", "H"), | |
| ("e_phnum", "H"), | |
| ("e_shentsize", "H"), | |
| ("e_shnum", "H"), | |
| ("e_shstrndx", "H"), | |
| ], | |
| [ | |
| ("e_ident", "16s"), | |
| ("e_type", "H"), | |
| ("e_machine", "H"), | |
| ("e_version", "I"), | |
| ("e_entry", "Q"), | |
| ("e_phoff", "Q"), | |
| ("e_shoff", "Q"), | |
| ("e_flags", "I"), | |
| ("e_ehsize", "H"), | |
| ("e_phentsize", "H"), | |
| ("e_phnum", "H"), | |
| ("e_shentsize", "H"), | |
| ("e_shnum", "H"), | |
| ("e_shstrndx", "H"), | |
| ], | |
| ), | |
| "Phdr": ( | |
| [ | |
| ("p_type", "I"), | |
| ("p_offset", "I"), | |
| ("p_vaddr", "I"), | |
| ("p_paddr", "I"), | |
| ("p_filesz", "I"), | |
| ("p_memsz", "I"), | |
| ("p_flags", "I"), | |
| ("p_align", "I"), | |
| ], | |
| [ | |
| ("p_type", "I"), | |
| ("p_flags", "I"), | |
| ("p_offset", "Q"), | |
| ("p_vaddr", "Q"), | |
| ("p_paddr", "Q"), | |
| ("p_filesz", "Q"), | |
| ("p_memsz", "Q"), | |
| ("p_align", "Q"), | |
| ], | |
| ), | |
| "Shdr": ( | |
| [ | |
| ("sh_name", "L"), | |
| ("sh_type", "L"), | |
| ("sh_flags", "L"), | |
| ("sh_addr", "L"), | |
| ("sh_offset", "L"), | |
| ("sh_size", "L"), | |
| ("sh_link", "L"), | |
| ("sh_info", "L"), | |
| ("sh_addralign", "L"), | |
| ("sh_entsize", "L"), | |
| ], | |
| [ | |
| ("sh_name", "L"), | |
| ("sh_type", "L"), | |
| ("sh_flags", "Q"), | |
| ("sh_addr", "Q"), | |
| ("sh_offset", "Q"), | |
| ("sh_size", "Q"), | |
| ("sh_link", "L"), | |
| ("sh_info", "L"), | |
| ("sh_addralign", "Q"), | |
| ("sh_entsize", "Q"), | |
| ], | |
| ), | |
| "Dyn": ( | |
| [ | |
| ("d_tag", "i"), | |
| ("d_val", "I"), | |
| ], | |
| [ | |
| ("d_tag", "q"), | |
| ("d_val", "Q"), | |
| ], | |
| ), | |
| "Nhdr": [ | |
| ("n_namesz", "I"), | |
| ("n_descsz", "I"), | |
| ("n_type", "I"), | |
| ], | |
| "dwarf2_line_header": [ | |
| ("unit_length", "L"), | |
| ("version", "H"), | |
| ("header_length", "L"), | |
| ("minimum_instruction_length", "B"), | |
| ("default_is_stmt", "B"), | |
| ("line_base", "b"), | |
| ("line_range", "B"), | |
| ("opcode_base", "B"), | |
| ], | |
| "dwarf4_line_header": [ | |
| ("unit_length", "L"), | |
| ("version", "H"), | |
| ("header_length", "L"), | |
| ("minimum_instruction_length", "B"), | |
| ("maximum_operations_per_instruction", "B"), | |
| ("default_is_stmt", "B"), | |
| ("line_base", "b"), | |
| ("line_range", "b"), | |
| ("opcode_base", "B"), | |
| ], | |
| } | |
| # There is an accessor for each struct, e.g. Ehdr. | |
| # Ehdr.read is a function like Struct.unpack_from. | |
| # Ehdr.size is the size of the struct. | |
| elf_accessor = namedtuple("elf_accessor", ["size", "read", "write", "pack"]) | |
| # All the accessors for a format (class, byte-order) form one elf, | |
| # e.g. use elf.Ehdr and elf.Phdr. | |
| elf = namedtuple("elf", list(elf_types.keys())) | |
| def gen_accessors(is64, struct_byte_order): | |
| def make_accessor(type, decoder): | |
| return elf_accessor( | |
| size=decoder.size, | |
| read=lambda buffer, offset=0: type._make( | |
| decoder.unpack_from(buffer, offset) | |
| ), | |
| write=lambda buffer, offset, x: decoder.pack_into( | |
| buffer, offset, *x | |
| ), | |
| pack=lambda x: decoder.pack(*x), | |
| ) | |
| for name, fields in elf_types.items(): | |
| if isinstance(fields, tuple): | |
| fields = fields[1 if is64 else 0] | |
| type = namedtuple(name, [field_name for field_name, fmt in fields]) | |
| decoder = struct.Struct( | |
| struct_byte_order + "".join(fmt for field_name, fmt in fields) | |
| ) | |
| yield make_accessor(type, decoder) | |
| for elfclass, is64 in [(ELFCLASS32, False), (ELFCLASS64, True)]: | |
| for elf_bo, struct_bo in [(ELFDATA2LSB, "<"), (ELFDATA2MSB, ">")]: | |
| yield ((elfclass, elf_bo), elf(*gen_accessors(is64, struct_bo))) | |
| # e.g. ELF[file[EI_CLASS], file[EI_DATA]].Ehdr.read(file).e_phnum | |
| ELF = dict(gen_elf()) | |
| def get_elf_accessor(file): | |
| # If it looks like an ELF file, whip out the decoder ring. | |
| if file[: len(ELFMAG)] == ELFMAG: | |
| return ELF.get( | |
| ( | |
| ord(file[EI_CLASS : EI_CLASS + 1]), | |
| ord(file[EI_DATA : EI_DATA + 1]), | |
| ), | |
| None, | |
| ) | |
| return None | |
| def gen_phdrs(file, elf, ehdr): | |
| for pos in range(0, ehdr.e_phnum * elf.Phdr.size, elf.Phdr.size): | |
| yield elf.Phdr.read(file, ehdr.e_phoff + pos) | |
| def gen_shdrs(file, elf, ehdr): | |
| for pos in range(0, ehdr.e_shnum * elf.Shdr.size, elf.Shdr.size): | |
| yield elf.Shdr.read(file, ehdr.e_shoff + pos) | |
| cpu = namedtuple( | |
| "cpu", | |
| [ | |
| "e_machine", # ELF e_machine int | |
| "llvm", # LLVM triple CPU component | |
| "gn", # GN target_cpu | |
| ], | |
| ) | |
| ELF_MACHINE_TO_CPU = { | |
| elf: cpu(elf, llvm, gn) | |
| for elf, llvm, gn in [ | |
| (EM_386, "i386", "x86"), | |
| (EM_ARM, "arm", "arm"), | |
| (EM_X86_64, "x86_64", "x64"), | |
| (EM_AARCH64, "aarch64", "arm64"), | |
| ] | |
| } | |
| @contextmanager | |
| def mmapper(filename): | |
| """A context manager that yields (fd, file_contents) given a file name. | |
| This ensures that the mmap and file objects are closed at the end of the | |
| 'with' statement.""" | |
| fileobj = open(filename, "rb") | |
| fd = fileobj.fileno() | |
| size = os.fstat(fd).st_size | |
| if size == 0: | |
| # mmap can't handle empty files. | |
| try: | |
| yield fd, "", 0 | |
| finally: | |
| fileobj.close() | |
| else: | |
| mmapobj = mmap.mmap(fd, 0, access=mmap.ACCESS_READ) | |
| try: | |
| yield fd, mmapobj, size | |
| finally: | |
| mmapobj.close() | |
| fileobj.close() | |
| def makedirs(dirs): | |
| try: | |
| os.makedirs(dirs) | |
| except OSError as e: | |
| if e.errno != os.errno.EEXIST: | |
| raise e | |
| elf_sizes = namedtuple( | |
| "elf_sizes", | |
| [ | |
| "file", # st_size | |
| "memory", # lowest p_vaddr to highest p_memsz | |
| "rel", # DT_RELSZ (+ DT_PLTRELSZ if matching) | |
| "rela", # DT_RELASZ (+ DT_PLTRELSZ if matching) | |
| "relr", # DT_RELRSZ | |
| "relcount", # DT_RELCOUNT + DT_RELACOUNT | |
| "relro", # PT_GNU_RELRO p_memsz | |
| "code", # executable segment p_filesz | |
| "rodata", # read-only data segment p_filesz | |
| "data", # writable segment p_filesz | |
| "bss", # writable segment p_memsz past p_filesz | |
| ], | |
| ) | |
| # elf_info objects are only created by `get_elf_info` or the `copy` or | |
| # `rename` methods. | |
| class elf_info( | |
| namedtuple( | |
| "elf_info", | |
| [ | |
| "filename", | |
| "sizes", # elf_sizes | |
| "cpu", # cpu tuple | |
| "notes", # list of (ident, desc): selected notes | |
| "build_id", # string: lowercase hex | |
| "stripped", # bool: Has no symbols or .debug_* sections | |
| "interp", # string or None: PT_INTERP (without \0) | |
| "soname", # string or None: DT_SONAME | |
| "needed", # list of strings: DT_NEEDED | |
| ], | |
| ) | |
| ): | |
| def rename(self, filename): | |
| assert os.path.samefile(self.filename, filename) | |
| return self._replace(filename=filename) | |
| def with_soname(self, soname): | |
| return self._replace(soname=soname) | |
| def copy(self): | |
| return self._replace() | |
| def _replace(self, **kwargs): | |
| # Copy the tuple. | |
| clone = self.__class__(*super()._replace(**kwargs)) | |
| # Copy the lazy state. | |
| clone.elf = self.elf | |
| if self.get_sources == clone.get_sources: | |
| raise Exception("uninitialized elf_info object!") | |
| clone.get_sources = self.get_sources | |
| return clone | |
| # This is replaced with a closure by the creator in get_elf_info. | |
| def get_sources(self): | |
| raise Exception("uninitialized elf_info object!") | |
| def strip(self, stripped_filename): | |
| """Write stripped output to the given file unless it already exists | |
| with identical contents. Returns True iff the file was changed.""" | |
| with mmapper(self.filename) as mapped: | |
| fd, file, filesize = mapped | |
| ehdr = self.elf.Ehdr.read(file) | |
| stripped_ehdr = ehdr._replace(e_shoff=0, e_shnum=0, e_shstrndx=0) | |
| stripped_size = max( | |
| phdr.p_offset + phdr.p_filesz | |
| for phdr in gen_phdrs(file, self.elf, ehdr) | |
| if phdr.p_type == PT_LOAD | |
| ) | |
| assert ( | |
| ehdr.e_phoff + (ehdr.e_phnum * ehdr.e_phentsize) | |
| <= stripped_size | |
| ) | |
| def gen_stripped_contents(): | |
| yield self.elf.Ehdr.pack(stripped_ehdr) | |
| yield file[self.elf.Ehdr.size : stripped_size] | |
| def old_file_matches(): | |
| old_size = os.path.getsize(stripped_filename) | |
| new_size = sum(len(x) for x in gen_stripped_contents()) | |
| if old_size != new_size: | |
| return False | |
| with open(stripped_filename, "rb") as f: | |
| for chunk in gen_stripped_contents(): | |
| if f.read(len(chunk)) != chunk: | |
| return False | |
| return True | |
| if os.path.exists(stripped_filename): | |
| if old_file_matches(): | |
| return False | |
| else: | |
| os.remove(stripped_filename) | |
| # Create the new file with the same mode as the original. | |
| with os.fdopen( | |
| os.open( | |
| stripped_filename, | |
| os.O_WRONLY | os.O_CREAT | os.O_EXCL, | |
| os.fstat(fd).st_mode & 0o777, | |
| ), | |
| "wb", | |
| ) as stripped_file: | |
| stripped_file.write(self.elf.Ehdr.pack(stripped_ehdr)) | |
| stripped_file.write(file[self.elf.Ehdr.size : stripped_size]) | |
| return True | |
| def get_elf_info(filename, match_notes=False): | |
| file = None | |
| elf = None | |
| ehdr = None | |
| phdrs = None | |
| # Yields an elf_note for each note in any PT_NOTE segment. | |
| def gen_notes(): | |
| def round_up_to(size): | |
| return ((size + 3) // 4) * 4 | |
| for phdr in phdrs: | |
| if phdr.p_type == PT_NOTE: | |
| pos = phdr.p_offset | |
| while pos < phdr.p_offset + phdr.p_filesz: | |
| nhdr = elf.Nhdr.read(file, pos) | |
| pos += elf.Nhdr.size | |
| # TODO(https://fxbug.dev/42132793) PT_NOTE is expected to be valid | |
| # 7-bit ASCII or UTF-8. Remove this when clients no longer | |
| # depend on that behavior. | |
| name = str(file[pos : pos + nhdr.n_namesz].decode()) | |
| pos += round_up_to(nhdr.n_namesz) | |
| # PT_DESC is not always string-ish, just copy the bytes. | |
| desc = file[pos : pos + nhdr.n_descsz] | |
| pos += round_up_to(nhdr.n_descsz) | |
| yield elf_note(name, nhdr.n_type, desc) | |
| def gen_sections(): | |
| shdrs = list(gen_shdrs(file, elf, ehdr)) | |
| if not shdrs: | |
| return | |
| strtab_shdr = shdrs[ehdr.e_shstrndx] | |
| for shdr, i in zip(shdrs, range(len(shdrs))): | |
| if i == 0: | |
| continue | |
| assert shdr.sh_name < strtab_shdr.sh_size, ( | |
| "%s: invalid sh_name" % filename | |
| ) | |
| yield (shdr, extract_C_string(strtab_shdr.sh_offset + shdr.sh_name)) | |
| # Generates '\0'-terminated strings starting at the given offset, | |
| # until an empty string. | |
| def gen_strings(start): | |
| while True: | |
| end = file.find(b"\0", start) | |
| assert end >= start, "%s: Unterminated string at %#x" % ( | |
| filename, | |
| start, | |
| ) | |
| if start == end: | |
| break | |
| yield file[start:end].decode() | |
| start = end + 1 | |
| def extract_C_string(start): | |
| for string in gen_strings(start): | |
| return string | |
| return "" | |
| # Returns a string of hex digits (or None). | |
| def get_build_id(): | |
| build_id = None | |
| for note in gen_notes(): | |
| # Note that the last build_id note needs to be used due to https://fxbug.dev/42101443. | |
| possible_build_id = note.build_id_hex() | |
| if possible_build_id: | |
| build_id = possible_build_id | |
| return build_id | |
| # Returns a list of elf_note objects. | |
| def get_matching_notes(): | |
| if isinstance(match_notes, bool): | |
| if match_notes: | |
| return list(gen_notes()) | |
| else: | |
| return [] | |
| # If not a bool, it's an iterable of ident pairs. | |
| return [note for note in gen_notes() if note.ident() in match_notes] | |
| # Returns a string (without trailing '\0'), or None. | |
| def get_interp(): | |
| # PT_INTERP points directly to a string in the file. | |
| for interp in (phdr for phdr in phdrs if phdr.p_type == PT_INTERP): | |
| interp = file[interp.p_offset : interp.p_offset + interp.p_filesz] | |
| if interp[-1:] == b"\0": | |
| interp = interp[:-1] | |
| return interp.decode() | |
| return None | |
| def get_dynamic(): | |
| # PT_DYNAMIC points to the list of ElfNN_Dyn tags. | |
| for dynamic in (phdr for phdr in phdrs if phdr.p_type == PT_DYNAMIC): | |
| return [ | |
| elf.Dyn.read(file, dynamic.p_offset + dyn_offset) | |
| for dyn_offset in range(0, dynamic.p_filesz, elf.Dyn.size) | |
| ] | |
| return None | |
| def dyn_get(dyn, tag): | |
| for dt in dyn: | |
| if dt.d_tag == tag: | |
| return dt.d_val | |
| return None | |
| # Returns a string (or None) and a set of strings. | |
| def get_soname_and_needed(dyn): | |
| # Each DT_NEEDED or DT_SONAME points to a string in the .dynstr table. | |
| def GenDTStrings(tag): | |
| return ( | |
| extract_C_string(strtab_offset + dt.d_val) | |
| for dt in dyn | |
| if dt.d_tag == tag | |
| ) | |
| if dyn is None: | |
| return None, set() | |
| # DT_STRTAB points to the string table's vaddr (.dynstr). | |
| strtab_vaddr = dyn_get(dyn, DT_STRTAB) | |
| # Find the PT_LOAD containing the vaddr to compute the file offset. | |
| [strtab_offset] = [ | |
| strtab_vaddr - phdr.p_vaddr + phdr.p_offset | |
| for phdr in phdrs | |
| if ( | |
| phdr.p_type == PT_LOAD | |
| and phdr.p_vaddr <= strtab_vaddr | |
| and strtab_vaddr - phdr.p_vaddr < phdr.p_filesz | |
| ) | |
| ] | |
| soname = None | |
| for soname in GenDTStrings(DT_SONAME): | |
| break | |
| return soname, set(GenDTStrings(DT_NEEDED)) | |
| def get_stripped(): | |
| return all( | |
| (shdr.sh_flags & SHF_ALLOC) != 0 | |
| or (shdr.sh_type != SHT_SYMTAB and not name.startswith(".debug_")) | |
| for shdr, name in gen_sections() | |
| ) | |
| def get_memory_size(): | |
| segments = [phdr for phdr in phdrs if phdr.p_type == PT_LOAD] | |
| first = segments[0] | |
| last = segments[-1] | |
| start = first.p_vaddr & -first.p_align | |
| last = (last.p_vaddr + last.p_memsz + last.p_align - 1) & -last.p_align | |
| return last - start | |
| def get_relro_size(): | |
| return sum( | |
| phdr.p_memsz for phdr in phdrs if phdr.p_type == PT_GNU_RELRO | |
| ) | |
| def get_segment_size(phdr, file, mem): | |
| start = phdr.p_vaddr & -phdr.p_align | |
| file_end = phdr.p_vaddr + phdr.p_filesz | |
| mem_end = phdr.p_vaddr + phdr.p_memsz | |
| file_end = (file_end + phdr.p_align - 1) & -phdr.p_align | |
| mem_end = (mem_end + phdr.p_align - 1) & -phdr.p_align | |
| if file and mem: | |
| return mem_end - start | |
| if file: | |
| return file_end - start | |
| return mem_end - file_end | |
| def gen_executable_segments(): | |
| return ( | |
| phdr | |
| for phdr in phdrs | |
| if phdr.p_type == PT_LOAD and (phdr.p_flags & PF_X) != 0 | |
| ) | |
| def gen_writable_segments(): | |
| return ( | |
| phdr | |
| for phdr in phdrs | |
| if phdr.p_type == PT_LOAD and (phdr.p_flags & PF_W) != 0 | |
| ) | |
| def gen_rodata_segments(): | |
| return ( | |
| phdr | |
| for phdr in phdrs | |
| if phdr.p_type == PT_LOAD and phdr.p_flags == PF_R | |
| ) | |
| def get_code_size(): | |
| return sum( | |
| get_segment_size(phdr, file=True, mem=False) | |
| for phdr in gen_executable_segments() | |
| ) | |
| def get_rodata_size(): | |
| return sum( | |
| get_segment_size(phdr, file=True, mem=False) | |
| for phdr in gen_rodata_segments() | |
| ) | |
| def get_data_size(): | |
| return sum( | |
| get_segment_size(phdr, file=True, mem=False) | |
| for phdr in gen_writable_segments() | |
| ) | |
| def get_bss_size(): | |
| return sum( | |
| get_segment_size(phdr, file=False, mem=True) | |
| for phdr in gen_writable_segments() | |
| ) | |
| def get_relsz(dyn, tag, sizetag): | |
| if dyn is None: | |
| return 0 | |
| relsz = dyn_get(dyn, sizetag) | |
| if relsz is None: | |
| relsz = 0 | |
| if dyn_get(dyn, DT_PLTREL) == tag: | |
| pltrelsz = dyn_get(dyn, DT_PLTRELSZ) | |
| if pltrelsz is not None: | |
| relsz += pltrelsz | |
| return relsz | |
| def get_relcount(dyn): | |
| if dyn is None: | |
| return 0 | |
| return (dyn_get(dyn, DT_RELCOUNT) or 0) + ( | |
| dyn_get(dyn, DT_RELACOUNT) or 0 | |
| ) | |
| def get_cpu(): | |
| return ELF_MACHINE_TO_CPU.get(ehdr.e_machine) | |
| def gen_source_files(): | |
| # Given the file position of a CU header (starting with the | |
| # beginning of the .debug_line section), return the position | |
| # of the include_directories portion and the position of the | |
| # next CU header. | |
| def read_line_header(pos): | |
| # Decode DWARF .debug_line per-CU header. | |
| hdr_type = elf.dwarf2_line_header | |
| hdr = hdr_type.read(file, pos) | |
| assert hdr.unit_length < 0xFFFFFFF0, "%s: 64-bit DWARF" % filename | |
| assert hdr.version in [ | |
| 2, | |
| 3, | |
| 4, | |
| ], "%s: DWARF .debug_line version %r" % (filename, hdr.version) | |
| if hdr.version == 4: | |
| hdr_type = elf.dwarf4_line_header | |
| hdr = hdr_type.read(file, pos) | |
| return ( | |
| pos + hdr_type.size + hdr.opcode_base - 1, | |
| pos + 4 + hdr.unit_length, | |
| ) | |
| # Decode include_directories portion of DWARF .debug_line format. | |
| def read_include_dirs(pos): | |
| include_dirs = list(gen_strings(pos)) | |
| pos += sum(len(dir) + 1 for dir in include_dirs) + 1 | |
| return pos, include_dirs | |
| # Decode file_paths portion of DWARF .debug_line format. | |
| def gen_file_paths(start, limit): | |
| while start < limit: | |
| end = file.find(b"\0", start, limit) | |
| assert end >= start, "%s: Unterminated string at %#x" % ( | |
| filename, | |
| start, | |
| ) | |
| if start == end: | |
| break | |
| name = file[start:end].decode() | |
| start = end + 1 | |
| # Decode 3 ULEB128s to advance start, but only use the first. | |
| for i in range(3): | |
| value = 0 | |
| bits = 0 | |
| while start < limit: | |
| byte = file[start] | |
| start += 1 | |
| value |= (byte & 0x7F) << bits | |
| if (byte & 0x80) == 0: | |
| break | |
| bits += 7 | |
| if i == 0: | |
| include_idx = value | |
| # Ignore the fake file names the compiler leaks into the DWARF. | |
| if name not in ["<stdin>", "<command-line>"]: | |
| yield name, include_idx | |
| for shdr, name in gen_sections(): | |
| if name == ".debug_line": | |
| next = shdr.sh_offset | |
| while next < shdr.sh_offset + shdr.sh_size: | |
| pos, next = read_line_header(next) | |
| pos, include_dirs = read_include_dirs(pos) | |
| assert pos <= next | |
| # 0 means relative to DW_AT_comp_dir, which should be ".". | |
| # Indices into the actual table start at 1. | |
| include_dirs.insert(0, "") | |
| # Decode file_paths and apply include directories. | |
| for name, i in gen_file_paths(pos, next): | |
| name = os.path.join(include_dirs[i], name) | |
| yield os.path.normpath(name) | |
| # This closure becomes the elf_info object's `get_sources` method. | |
| def lazy_get_sources(): | |
| # Run the generator and cache its results as a set. | |
| sources_cache = set(gen_source_files()) | |
| # Replace the method to just return the cached set next time. | |
| info.get_sources = lambda: sources_cache | |
| return sources_cache | |
| # Map in the whole file's contents and use it as a string. | |
| with mmapper(filename) as mapped: | |
| fd, file, filesize = mapped | |
| elf = get_elf_accessor(file) | |
| if elf is not None: | |
| # ELF header leads to program headers. | |
| ehdr = elf.Ehdr.read(file) | |
| if ehdr.e_type not in (ET_EXEC, ET_DYN): | |
| # This is an invalid file or an ET_REL file, which don't count | |
| # as ELF files for our purposes. | |
| return None | |
| assert ( | |
| ehdr.e_phentsize == elf.Phdr.size | |
| ), "%s: invalid e_phentsize %s" % (filename, ehdr.e_phentsize) | |
| phdrs = list(gen_phdrs(file, elf, ehdr)) | |
| dyn = get_dynamic() | |
| info = elf_info( | |
| filename, | |
| elf_sizes( | |
| file=filesize, | |
| memory=get_memory_size(), | |
| code=get_code_size(), | |
| rodata=get_rodata_size(), | |
| relro=get_relro_size(), | |
| data=get_data_size(), | |
| bss=get_bss_size(), | |
| rel=get_relsz(dyn, DT_REL, DT_RELSZ), | |
| rela=get_relsz(dyn, DT_RELA, DT_RELASZ), | |
| relr=get_relsz(dyn, DT_RELR, DT_RELRSZ), | |
| relcount=get_relcount(dyn), | |
| ), | |
| get_cpu(), | |
| get_matching_notes(), | |
| get_build_id(), | |
| get_stripped(), | |
| get_interp(), | |
| *get_soname_and_needed(dyn), | |
| ) | |
| info.elf = elf | |
| info.get_sources = lazy_get_sources | |
| return info | |
| return None | |
| # Module public API. | |
| __all__ = ["cpu", "elf_info", "elf_note", "get_elf_accessor", "get_elf_info"] | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| "--strip", | |
| action="store_true", | |
| help="Strip each file into FILE.ei-strip", | |
| ) | |
| parser.add_argument( | |
| "--build-id", action="store_true", help="Print each build ID" | |
| ) | |
| parser.add_argument("--bootfs-dir", help="bootfs content", metavar="DIR") | |
| parser.add_argument("--zbi", help="Read zbi.json", metavar="FILE") | |
| parser.add_argument("--blobs", help="Read blobs.json", metavar="FILE") | |
| parser.add_argument("--sizes", help="Write elf_sizes.json", metavar="FILE") | |
| parser.add_argument("file", help="ELF file", metavar="FILE", nargs="*") | |
| args = parser.parse_args() | |
| files = args.file | |
| if args.zbi: | |
| if not args.bootfs_dir: | |
| parser.exit("--zbi and --bootfs-dir must be specified together") | |
| bootfs_map = {} | |
| with open(args.zbi) as f: | |
| zbi = json.load(f) | |
| for entry in zbi: | |
| if entry["type"] != "BOOTFS": | |
| continue | |
| for item in entry["contents"]: | |
| filepath = os.path.join(args.bootfs_dir, item["name"]) | |
| files.append(filepath) | |
| bootfs_map[filepath] = item | |
| if args.blobs: | |
| with open(args.blobs) as f: | |
| blobs = json.load(f) | |
| files += [blob["source_path"] for blob in blobs] | |
| if args.sizes: | |
| if args.blobs: | |
| blob_map = {blob["source_path"]: blob for blob in blobs} | |
| def json_size(info): | |
| sizes = info.sizes._asdict() | |
| sizes["path"] = info.filename | |
| sizes["build_id"] = info.build_id | |
| if args.zbi and info.filename in bootfs_map: | |
| item = bootfs_map[info.filename] | |
| assert ( | |
| info.sizes.file == item["length"] | |
| ), "%r != %r in %r vs %r" % ( | |
| info.sizes.file, | |
| blob["length"], | |
| info, | |
| blob, | |
| ) | |
| sizes["zbi"] = item["size"] | |
| if args.blobs and info.filename in blob_map: | |
| blob = blob_map[info.filename] | |
| assert ( | |
| info.sizes.file == blob["bytes"] | |
| ), "%r != %r in %r vs %r" % ( | |
| info.sizes.file, | |
| blob["bytes"], | |
| info, | |
| blob, | |
| ) | |
| sizes["blob"] = blob["size"] | |
| return sizes | |
| # Sort for stable output. | |
| sizes = sorted( | |
| [ | |
| json_size(info) | |
| for info in (get_elf_info(file) for file in files) | |
| if info | |
| ], | |
| key=lambda info: info["path"], | |
| ) | |
| totals = {} | |
| for file in sizes: | |
| for key, val in file.items(): | |
| if isinstance(val, int): | |
| totals[key] = totals.get(key, 0) + val | |
| with open(args.sizes, "w") as outf: | |
| json.dump( | |
| {"files": sizes, "totals": totals}, | |
| outf, | |
| sort_keys=True, | |
| indent=1, | |
| ) | |
| else: | |
| for file in files: | |
| info = get_elf_info(file) | |
| if args.strip: | |
| stripped_filename = info.filename + ".ei-strip" | |
| info.strip(stripped_filename) | |
| print(stripped_filename) | |
| elif args.build_id: | |
| print(info.build_id) | |
| else: | |
| print(info) | |
| for source in info.get_sources(): | |
| print("\t" + source) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |