# Copyright 2021 The Pigweed Authors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of # the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. """Library that parses an ELF file for a GNU build-id.""" import argparse import logging from pathlib import Path import sys from typing import BinaryIO, Optional import elftools # type: ignore from elftools.elf import elffile, notes, sections # type: ignore _LOG = logging.getLogger('build_id_parser') _PW_BUILD_ID_SYM_NAME = 'gnu_build_id_begin' class GnuBuildIdError(Exception): """An exception raised when a GNU build ID is malformed.""" def read_build_id_from_section(elf_file: BinaryIO) -> Optional[bytes]: """Reads a build ID from a .note.gnu.build-id section.""" parsed_elf_file = elffile.ELFFile(elf_file) build_id_section = parsed_elf_file.get_section_by_name('.note.gnu.build-id') if build_id_section is None: return None section_notes = list( n for n in notes.iter_notes( parsed_elf_file, build_id_section['sh_offset'], build_id_section['sh_size'], ) ) if len(section_notes) != 1: raise GnuBuildIdError('GNU build ID section contains multiple notes') build_id_note = section_notes[0] if build_id_note['n_name'] != 'GNU': raise GnuBuildIdError('GNU build ID note name invalid') if build_id_note['n_type'] != 'NT_GNU_BUILD_ID': raise GnuBuildIdError('GNU build ID note type invalid') return bytes.fromhex(build_id_note['n_desc']) def _addr_is_in_segment(addr: int, segment) -> bool: """Checks if the provided address resides within the provided segment.""" # Address references uninitialized memory. Can't read. if addr >= segment['p_vaddr'] + segment['p_filesz']: raise GnuBuildIdError('GNU build ID is runtime-initialized') return addr in range(segment['p_vaddr'], segment['p_memsz']) def _read_build_id_from_offset(elf, offset: int) -> bytes: """Attempts to read a GNU build ID from an offset in an elf file.""" note = elftools.common.utils.struct_parse( elf.structs.Elf_Nhdr, elf.stream, stream_pos=offset ) elf.stream.seek(offset + elf.structs.Elf_Nhdr.sizeof()) name = elf.stream.read(note['n_namesz']) if name != b'GNU\0': raise GnuBuildIdError('GNU build ID note name invalid') return elf.stream.read(note['n_descsz']) def read_build_id_from_symbol(elf_file: BinaryIO) -> Optional[bytes]: """Reads a GNU build ID using gnu_build_id_begin to locate the data.""" parsed_elf_file = elffile.ELFFile(elf_file) matching_syms = None for section in parsed_elf_file.iter_sections(): if not isinstance(section, sections.SymbolTableSection): continue matching_syms = section.get_symbol_by_name(_PW_BUILD_ID_SYM_NAME) if matching_syms is not None: break if matching_syms is None: return None if len(matching_syms) != 1: raise GnuBuildIdError('Multiple GNU build ID start symbols defined') gnu_build_id_sym = matching_syms[0] section_number = gnu_build_id_sym['st_shndx'] if section_number == 'SHN_UNDEF': raise GnuBuildIdError('GNU build ID start symbol undefined') matching_section = parsed_elf_file.get_section(section_number) build_id_start_addr = gnu_build_id_sym['st_value'] for segment in parsed_elf_file.iter_segments(): if segment.section_in_segment(matching_section): offset = ( build_id_start_addr - segment['p_vaddr'] + segment['p_offset'] ) return _read_build_id_from_offset(parsed_elf_file, offset) return None def read_build_id(elf_file: BinaryIO) -> Optional[bytes]: """Reads a GNU build ID from an ELF binary.""" # Prefer to read the build ID from a dedicated section. maybe_build_id = read_build_id_from_section(elf_file) if maybe_build_id is not None: return maybe_build_id # If there's no dedicated section, try and use symbol information to find # the build info. return read_build_id_from_symbol(elf_file) def find_matching_elf(uuid: bytes, search_dir: Path) -> Optional[Path]: """Recursively searches a directory for an ELF file with a matching UUID.""" elf_file_paths = search_dir.glob('**/*.elf') for elf_file in elf_file_paths: try: candidate_id = read_build_id(open(elf_file, 'rb')) except GnuBuildIdError: continue if candidate_id is None: continue if candidate_id == uuid: return elf_file return None def _main(elf_file: BinaryIO) -> int: logging.basicConfig(format='%(message)s', level=logging.INFO) build_id = read_build_id(elf_file) if build_id is None: _LOG.error('Error: No GNU build ID found.') return 1 _LOG.info(build_id.hex()) return 0 def _parse_args(): """Parses command-line arguments.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( 'elf_file', type=argparse.FileType('rb'), help='The .elf to parse build info from', ) return parser.parse_args() if __name__ == '__main__': sys.exit(_main(**vars(_parse_args())))