# Copyright (c) 2023 Yonatan Schachter # # SPDX-License-Identifier: Apache-2.0 from textwrap import dedent import struct from west.commands import WestCommand from west import log try: from elftools.elf.elffile import ELFFile from intelhex import IntelHex MISSING_REQUIREMENTS = False except ImportError: MISSING_REQUIREMENTS = True # Based on scripts/build/uf2conv.py def convert_from_uf2(buf): UF2_MAGIC_START0 = 0x0A324655 # First magic number ('UF2\n') UF2_MAGIC_START1 = 0x9E5D5157 # Second magic number numblocks = len(buf) // 512 curraddr = None outp = [] for blockno in range(numblocks): ptr = blockno * 512 block = buf[ptr:ptr + 512] hd = struct.unpack(b' 476: log.die(f'Invalid UF2 data size at {ptr}') newaddr = hd[3] if curraddr is None: curraddr = newaddr padding = newaddr - curraddr if padding < 0: log.die(f'Block out of order at {ptr}') if padding > 10*1024*1024: log.die(f'More than 10M of padding needed at {ptr}') if padding % 4 != 0: log.die(f'Non-word padding size at {ptr}') while padding > 0: padding -= 4 outp += b'\x00\x00\x00\x00' outp.append(block[32 : 32 + datalen]) curraddr = newaddr + datalen return b''.join(outp) class Bindesc(WestCommand): EXTENSIONS = ['bin', 'hex', 'elf', 'uf2'] # Corresponds to the definitions in include/zephyr/bindesc.h. # Do not change without syncing the definitions in both files! TYPE_UINT = 0 TYPE_STR = 1 TYPE_BYTES = 2 MAGIC = 0xb9863e5a7ea46046 DESCRIPTORS_END = 0xffff def __init__(self): self.TAG_TO_NAME = { # Corresponds to the definitions in include/zephyr/bindesc.h. # Do not change without syncing the definitions in both files! self.bindesc_gen_tag(self.TYPE_STR, 0x800): 'APP_VERSION_STRING', self.bindesc_gen_tag(self.TYPE_UINT, 0x801): 'APP_VERSION_MAJOR', self.bindesc_gen_tag(self.TYPE_UINT, 0x802): 'APP_VERSION_MINOR', self.bindesc_gen_tag(self.TYPE_UINT, 0x803): 'APP_VERSION_PATCHLEVEL', self.bindesc_gen_tag(self.TYPE_UINT, 0x804): 'APP_VERSION_NUMBER', self.bindesc_gen_tag(self.TYPE_STR, 0x900): 'KERNEL_VERSION_STRING', self.bindesc_gen_tag(self.TYPE_UINT, 0x901): 'KERNEL_VERSION_MAJOR', self.bindesc_gen_tag(self.TYPE_UINT, 0x902): 'KERNEL_VERSION_MINOR', self.bindesc_gen_tag(self.TYPE_UINT, 0x903): 'KERNEL_VERSION_PATCHLEVEL', self.bindesc_gen_tag(self.TYPE_UINT, 0x904): 'KERNEL_VERSION_NUMBER', self.bindesc_gen_tag(self.TYPE_UINT, 0xa00): 'BUILD_TIME_YEAR', self.bindesc_gen_tag(self.TYPE_UINT, 0xa01): 'BUILD_TIME_MONTH', self.bindesc_gen_tag(self.TYPE_UINT, 0xa02): 'BUILD_TIME_DAY', self.bindesc_gen_tag(self.TYPE_UINT, 0xa03): 'BUILD_TIME_HOUR', self.bindesc_gen_tag(self.TYPE_UINT, 0xa04): 'BUILD_TIME_MINUTE', self.bindesc_gen_tag(self.TYPE_UINT, 0xa05): 'BUILD_TIME_SECOND', self.bindesc_gen_tag(self.TYPE_UINT, 0xa06): 'BUILD_TIME_UNIX', self.bindesc_gen_tag(self.TYPE_STR, 0xa07): 'BUILD_DATE_TIME_STRING', self.bindesc_gen_tag(self.TYPE_STR, 0xa08): 'BUILD_DATE_STRING', self.bindesc_gen_tag(self.TYPE_STR, 0xa09): 'BUILD_TIME_STRING', self.bindesc_gen_tag(self.TYPE_STR, 0xb00): 'HOST_NAME', self.bindesc_gen_tag(self.TYPE_STR, 0xb01): 'C_COMPILER_NAME', self.bindesc_gen_tag(self.TYPE_STR, 0xb02): 'C_COMPILER_VERSION', self.bindesc_gen_tag(self.TYPE_STR, 0xb03): 'CXX_COMPILER_NAME', self.bindesc_gen_tag(self.TYPE_STR, 0xb04): 'CXX_COMPILER_VERSION', } self.NAME_TO_TAG = {v: k for k, v in self.TAG_TO_NAME.items()} super().__init__( 'bindesc', 'work with Binary Descriptors', dedent(''' Work with Binary Descriptors - constant data objects describing a binary image ''')) def do_add_parser(self, parser_adder): parser = parser_adder.add_parser(self.name, help=self.help, description=self.description) subparsers = parser.add_subparsers(help='sub-command to run', required=True) dump_parser = subparsers.add_parser('dump', help='Dump all binary descriptors in the image') dump_parser.add_argument('file', type=str, help='Executable file') dump_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, help='File type') dump_parser.add_argument('-b', '--big-endian', action='store_true', help='Target CPU is big endian') dump_parser.set_defaults(subcmd='dump', big_endian=False) search_parser = subparsers.add_parser('search', help='Search for a specific descriptor') search_parser.add_argument('descriptor', type=str, help='Descriptor name') search_parser.add_argument('file', type=str, help='Executable file') search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, help='File type') search_parser.add_argument('-b', '--big-endian', action='store_true', help='Target CPU is big endian') search_parser.set_defaults(subcmd='search', big_endian=False) custom_search_parser = subparsers.add_parser('custom_search', help='Search for a custom descriptor') custom_search_parser.add_argument('type', type=str, choices=['UINT', 'STR', 'BYTES'], help='Descriptor type') custom_search_parser.add_argument('id', type=str, help='Descriptor ID in hex') custom_search_parser.add_argument('file', type=str, help='Executable file') custom_search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, help='File type') custom_search_parser.add_argument('-b', '--big-endian', action='store_true', help='Target CPU is big endian') custom_search_parser.set_defaults(subcmd='custom_search', big_endian=False) list_parser = subparsers.add_parser('list', help='List all known descriptors') list_parser.set_defaults(subcmd='list', big_endian=False) return parser def dump(self, args): image = self.get_image_data(args.file) descriptors = self.parse_descriptors(image) for tag, value in descriptors.items(): if tag in self.TAG_TO_NAME: tag = self.TAG_TO_NAME[tag] log.inf(f'{tag}', self.bindesc_repr(value)) def list(self, args): for tag in self.TAG_TO_NAME.values(): log.inf(f'{tag}') def common_search(self, args, search_term): image = self.get_image_data(args.file) descriptors = self.parse_descriptors(image) if search_term in descriptors: value = descriptors[search_term] log.inf(self.bindesc_repr(value)) else: log.die('Descriptor not found') def search(self, args): try: search_term = self.NAME_TO_TAG[args.descriptor] except KeyError: log.die(f'Descriptor {args.descriptor} is invalid') self.common_search(args, search_term) def custom_search(self, args): custom_type = { 'STR': self.TYPE_STR, 'UINT': self.TYPE_UINT, 'BYTES': self.TYPE_BYTES }[args.type] custom_tag = self.bindesc_gen_tag(custom_type, int(args.id, 16)) self.common_search(args, custom_tag) def do_run(self, args, _): if MISSING_REQUIREMENTS: raise RuntimeError('one or more Python dependencies were missing; ' 'see the getting started guide for details on ' 'how to fix') self.is_big_endian = args.big_endian self.file_type = self.guess_file_type(args) subcmd = getattr(self, args.subcmd) subcmd(args) def get_image_data(self, file_name): if self.file_type == 'bin': with open(file_name, 'rb') as bin_file: return bin_file.read() if self.file_type == 'hex': return IntelHex(file_name).tobinstr() if self.file_type == 'uf2': with open(file_name, 'rb') as uf2_file: return convert_from_uf2(uf2_file.read()) if self.file_type == 'elf': with open(file_name, 'rb') as f: elffile = ELFFile(f) section = elffile.get_section_by_name('rom_start') if section: return section.data() section = elffile.get_section_by_name('text') if section: return section.data() log.die('No "rom_start" or "text" section found') log.die('Unknown file type') def parse_descriptors(self, image): magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC) index = image.find(magic) if index == -1: log.die('Could not find binary descriptor magic') descriptors = {} index += len(magic) # index points to first descriptor current_tag = self.bytes_to_short(image[index:index+2]) while current_tag != self.DESCRIPTORS_END: index += 2 # index points to length length = self.bytes_to_short(image[index:index+2]) index += 2 # index points to data data = image[index:index+length] tag_type = self.bindesc_get_type(current_tag) if tag_type == self.TYPE_STR: decoded_data = data[:-1].decode('ascii') elif tag_type == self.TYPE_UINT: decoded_data = self.bytes_to_uint(data) elif tag_type == self.TYPE_BYTES: decoded_data = data else: log.die(f'Unknown type for tag 0x{current_tag:04x}') key = f'0x{current_tag:04x}' descriptors[key] = decoded_data index += length index = self.align(index, 4) current_tag = self.bytes_to_short(image[index:index+2]) return descriptors def guess_file_type(self, args): if "file" not in args: return None # If file type is explicitly given, use it if args.file_type is not None: return args.file_type # If the file has a known extension, use it for extension in self.EXTENSIONS: if args.file.endswith(f'.{extension}'): return extension with open(args.file, 'rb') as f: header = f.read(1024) # Try the elf magic if header.startswith(b'\x7fELF'): return 'elf' # Try the uf2 magic if header.startswith(b'UF2\n'): return 'uf2' try: # if the file is textual it's probably hex header.decode('ascii') return 'hex' except UnicodeDecodeError: # Default to bin return 'bin' def bytes_to_uint(self, b): return struct.unpack('>I' if self.is_big_endian else 'I', b)[0] def bytes_to_short(self, b): return struct.unpack('>H' if self.is_big_endian else 'H', b)[0] @staticmethod def bindesc_gen_tag(_type, _id): return f'0x{(_type << 12 | _id):04x}' @staticmethod def bindesc_get_type(tag): return tag >> 12 @staticmethod def align(x, alignment): return (x + alignment - 1) & (~(alignment - 1)) @staticmethod def bindesc_repr(value): if isinstance(value, str): return f'"{value}"' if isinstance(value, (int, bytes)): return f'{value}'