blob: 17d877d210ad94f6e488fe4667eb95e672d327ef [file] [log] [blame]
# 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())))