diff --git a/scripts/west-commands.yml b/scripts/west-commands.yml index 5f63b44225..803d2b7b2d 100644 --- a/scripts/west-commands.yml +++ b/scripts/west-commands.yml @@ -56,3 +56,8 @@ west-commands: - name: blobs class: Blobs help: work with binary blobs + - file: scripts/west_commands/bindesc.py + commands: + - name: bindesc + class: Bindesc + help: work with Binary Descriptors diff --git a/scripts/west_commands/bindesc.py b/scripts/west_commands/bindesc.py new file mode 100644 index 0000000000..a4bc852bfa --- /dev/null +++ b/scripts/west_commands/bindesc.py @@ -0,0 +1,316 @@ +# 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') + + 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}'