|
| 1 | +# Copyright (c) 2023 Yonatan Schachter |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +from textwrap import dedent |
| 6 | +import struct |
| 7 | + |
| 8 | +from west.commands import WestCommand |
| 9 | +from west import log |
| 10 | + |
| 11 | + |
| 12 | +try: |
| 13 | + from elftools.elf.elffile import ELFFile |
| 14 | + from intelhex import IntelHex |
| 15 | + MISSING_REQUIREMENTS = False |
| 16 | +except ImportError: |
| 17 | + MISSING_REQUIREMENTS = True |
| 18 | + |
| 19 | + |
| 20 | +# Based on scripts/build/uf2conv.py |
| 21 | +def convert_from_uf2(buf): |
| 22 | + UF2_MAGIC_START0 = 0x0A324655 # First magic number ('UF2\n') |
| 23 | + UF2_MAGIC_START1 = 0x9E5D5157 # Second magic number |
| 24 | + numblocks = len(buf) // 512 |
| 25 | + curraddr = None |
| 26 | + outp = [] |
| 27 | + for blockno in range(numblocks): |
| 28 | + ptr = blockno * 512 |
| 29 | + block = buf[ptr:ptr + 512] |
| 30 | + hd = struct.unpack(b'<IIIIIIII', block[0:32]) |
| 31 | + if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1: |
| 32 | + log.inf('Skipping block at ' + ptr + '; bad magic') |
| 33 | + continue |
| 34 | + if hd[2] & 1: |
| 35 | + # NO-flash flag set; skip block |
| 36 | + continue |
| 37 | + datalen = hd[4] |
| 38 | + if datalen > 476: |
| 39 | + log.die(f'Invalid UF2 data size at {ptr}') |
| 40 | + newaddr = hd[3] |
| 41 | + if curraddr is None: |
| 42 | + curraddr = newaddr |
| 43 | + padding = newaddr - curraddr |
| 44 | + if padding < 0: |
| 45 | + log.die(f'Block out of order at {ptr}') |
| 46 | + if padding > 10*1024*1024: |
| 47 | + log.die(f'More than 10M of padding needed at {ptr}') |
| 48 | + if padding % 4 != 0: |
| 49 | + log.die(f'Non-word padding size at {ptr}') |
| 50 | + while padding > 0: |
| 51 | + padding -= 4 |
| 52 | + outp += b'\x00\x00\x00\x00' |
| 53 | + outp.append(block[32 : 32 + datalen]) |
| 54 | + curraddr = newaddr + datalen |
| 55 | + return b''.join(outp) |
| 56 | + |
| 57 | + |
| 58 | +class Bindesc(WestCommand): |
| 59 | + EXTENSIONS = ['bin', 'hex', 'elf', 'uf2'] |
| 60 | + |
| 61 | + # Corresponds to the definitions in include/zephyr/bindesc.h. |
| 62 | + # Do not change without syncing the definitions in both files! |
| 63 | + TYPE_UINT = 0 |
| 64 | + TYPE_STR = 1 |
| 65 | + TYPE_BYTES = 2 |
| 66 | + MAGIC = 0xb9863e5a7ea46046 |
| 67 | + DESCRIPTORS_END = 0xffff |
| 68 | + |
| 69 | + def __init__(self): |
| 70 | + self.TAG_TO_NAME = { |
| 71 | + # Corresponds to the definitions in include/zephyr/bindesc.h. |
| 72 | + # Do not change without syncing the definitions in both files! |
| 73 | + self.bindesc_gen_tag(self.TYPE_STR, 0x800): 'APP_VERSION_STRING', |
| 74 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x801): 'APP_VERSION_MAJOR', |
| 75 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x802): 'APP_VERSION_MINOR', |
| 76 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x803): 'APP_VERSION_PATCHLEVEL', |
| 77 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x804): 'APP_VERSION_NUMBER', |
| 78 | + self.bindesc_gen_tag(self.TYPE_STR, 0x900): 'KERNEL_VERSION_STRING', |
| 79 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x901): 'KERNEL_VERSION_MAJOR', |
| 80 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x902): 'KERNEL_VERSION_MINOR', |
| 81 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x903): 'KERNEL_VERSION_PATCHLEVEL', |
| 82 | + self.bindesc_gen_tag(self.TYPE_UINT, 0x904): 'KERNEL_VERSION_NUMBER', |
| 83 | + self.bindesc_gen_tag(self.TYPE_UINT, 0xa00): 'BUILD_TIME_YEAR', |
| 84 | + self.bindesc_gen_tag(self.TYPE_UINT, 0xa01): 'BUILD_TIME_MONTH', |
| 85 | + self.bindesc_gen_tag(self.TYPE_UINT, 0xa02): 'BUILD_TIME_DAY', |
| 86 | + self.bindesc_gen_tag(self.TYPE_UINT, 0xa03): 'BUILD_TIME_HOUR', |
| 87 | + self.bindesc_gen_tag(self.TYPE_UINT, 0xa04): 'BUILD_TIME_MINUTE', |
| 88 | + self.bindesc_gen_tag(self.TYPE_UINT, 0xa05): 'BUILD_TIME_SECOND', |
| 89 | + self.bindesc_gen_tag(self.TYPE_UINT, 0xa06): 'BUILD_TIME_UNIX', |
| 90 | + self.bindesc_gen_tag(self.TYPE_STR, 0xa07): 'BUILD_DATE_TIME_STRING', |
| 91 | + self.bindesc_gen_tag(self.TYPE_STR, 0xa08): 'BUILD_DATE_STRING', |
| 92 | + self.bindesc_gen_tag(self.TYPE_STR, 0xa09): 'BUILD_TIME_STRING', |
| 93 | + self.bindesc_gen_tag(self.TYPE_STR, 0xb00): 'HOST_NAME', |
| 94 | + self.bindesc_gen_tag(self.TYPE_STR, 0xb01): 'C_COMPILER_NAME', |
| 95 | + self.bindesc_gen_tag(self.TYPE_STR, 0xb02): 'C_COMPILER_VERSION', |
| 96 | + self.bindesc_gen_tag(self.TYPE_STR, 0xb03): 'CXX_COMPILER_NAME', |
| 97 | + self.bindesc_gen_tag(self.TYPE_STR, 0xb04): 'CXX_COMPILER_VERSION', |
| 98 | + } |
| 99 | + self.NAME_TO_TAG = {v: k for k, v in self.TAG_TO_NAME.items()} |
| 100 | + |
| 101 | + super().__init__( |
| 102 | + 'bindesc', |
| 103 | + 'work with Binary Descriptors', |
| 104 | + dedent(''' |
| 105 | + Work with Binary Descriptors - constant data objects |
| 106 | + describing a binary image |
| 107 | + ''')) |
| 108 | + |
| 109 | + def do_add_parser(self, parser_adder): |
| 110 | + parser = parser_adder.add_parser(self.name, |
| 111 | + help=self.help, |
| 112 | + description=self.description) |
| 113 | + |
| 114 | + subparsers = parser.add_subparsers(help='sub-command to run') |
| 115 | + |
| 116 | + dump_parser = subparsers.add_parser('dump', help='Dump all binary descriptors in the image') |
| 117 | + dump_parser.add_argument('file', type=str, help='Executable file') |
| 118 | + dump_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, help='File type') |
| 119 | + dump_parser.add_argument('-b', '--big-endian', action='store_true', |
| 120 | + help='Target CPU is big endian') |
| 121 | + dump_parser.set_defaults(subcmd='dump', big_endian=False) |
| 122 | + |
| 123 | + search_parser = subparsers.add_parser('search', help='Search for a specific descriptor') |
| 124 | + search_parser.add_argument('descriptor', type=str, help='Descriptor name') |
| 125 | + search_parser.add_argument('file', type=str, help='Executable file') |
| 126 | + search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, help='File type') |
| 127 | + search_parser.add_argument('-b', '--big-endian', action='store_true', |
| 128 | + help='Target CPU is big endian') |
| 129 | + search_parser.set_defaults(subcmd='search', big_endian=False) |
| 130 | + |
| 131 | + custom_search_parser = subparsers.add_parser('custom_search', |
| 132 | + help='Search for a custom descriptor') |
| 133 | + custom_search_parser.add_argument('type', type=str, choices=['UINT', 'STR', 'BYTES'], |
| 134 | + help='Descriptor type') |
| 135 | + custom_search_parser.add_argument('id', type=str, help='Descriptor ID in hex') |
| 136 | + custom_search_parser.add_argument('file', type=str, help='Executable file') |
| 137 | + custom_search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, |
| 138 | + help='File type') |
| 139 | + custom_search_parser.add_argument('-b', '--big-endian', action='store_true', |
| 140 | + help='Target CPU is big endian') |
| 141 | + custom_search_parser.set_defaults(subcmd='custom_search', big_endian=False) |
| 142 | + |
| 143 | + list_parser = subparsers.add_parser('list', help='List all known descriptors') |
| 144 | + list_parser.set_defaults(subcmd='list', big_endian=False) |
| 145 | + |
| 146 | + return parser |
| 147 | + |
| 148 | + def dump(self, args): |
| 149 | + image = self.get_image_data(args.file) |
| 150 | + |
| 151 | + descriptors = self.parse_descriptors(image) |
| 152 | + for tag, value in descriptors.items(): |
| 153 | + if tag in self.TAG_TO_NAME: |
| 154 | + tag = self.TAG_TO_NAME[tag] |
| 155 | + log.inf(f'{tag}', self.bindesc_repr(value)) |
| 156 | + |
| 157 | + def list(self, args): |
| 158 | + for tag in self.TAG_TO_NAME.values(): |
| 159 | + log.inf(f'{tag}') |
| 160 | + |
| 161 | + def common_search(self, args, search_term): |
| 162 | + image = self.get_image_data(args.file) |
| 163 | + |
| 164 | + descriptors = self.parse_descriptors(image) |
| 165 | + |
| 166 | + if search_term in descriptors: |
| 167 | + value = descriptors[search_term] |
| 168 | + log.inf(self.bindesc_repr(value)) |
| 169 | + else: |
| 170 | + log.die('Descriptor not found') |
| 171 | + |
| 172 | + def search(self, args): |
| 173 | + try: |
| 174 | + search_term = self.NAME_TO_TAG[args.descriptor] |
| 175 | + except KeyError: |
| 176 | + log.die(f'Descriptor {args.descriptor} is invalid') |
| 177 | + |
| 178 | + self.common_search(args, search_term) |
| 179 | + |
| 180 | + def custom_search(self, args): |
| 181 | + custom_type = { |
| 182 | + 'STR': self.TYPE_STR, |
| 183 | + 'UINT': self.TYPE_UINT, |
| 184 | + 'BYTES': self.TYPE_BYTES |
| 185 | + }[args.type] |
| 186 | + custom_tag = self.bindesc_gen_tag(custom_type, int(args.id, 16)) |
| 187 | + self.common_search(args, custom_tag) |
| 188 | + |
| 189 | + def do_run(self, args, _): |
| 190 | + if MISSING_REQUIREMENTS: |
| 191 | + raise RuntimeError('one or more Python dependencies were missing; ' |
| 192 | + 'see the getting started guide for details on ' |
| 193 | + 'how to fix') |
| 194 | + self.is_big_endian = args.big_endian |
| 195 | + self.file_type = self.guess_file_type(args) |
| 196 | + subcmd = getattr(self, args.subcmd) |
| 197 | + subcmd(args) |
| 198 | + |
| 199 | + def get_image_data(self, file_name): |
| 200 | + if self.file_type == 'bin': |
| 201 | + with open(file_name, 'rb') as bin_file: |
| 202 | + return bin_file.read() |
| 203 | + |
| 204 | + if self.file_type == 'hex': |
| 205 | + return IntelHex(file_name).tobinstr() |
| 206 | + |
| 207 | + if self.file_type == 'uf2': |
| 208 | + with open(file_name, 'rb') as uf2_file: |
| 209 | + return convert_from_uf2(uf2_file.read()) |
| 210 | + |
| 211 | + if self.file_type == 'elf': |
| 212 | + with open(file_name, 'rb') as f: |
| 213 | + elffile = ELFFile(f) |
| 214 | + |
| 215 | + section = elffile.get_section_by_name('rom_start') |
| 216 | + if section: |
| 217 | + return section.data() |
| 218 | + |
| 219 | + section = elffile.get_section_by_name('text') |
| 220 | + if section: |
| 221 | + return section.data() |
| 222 | + |
| 223 | + log.die('No "rom_start" or "text" section found') |
| 224 | + |
| 225 | + log.die('Unknown file type') |
| 226 | + |
| 227 | + def parse_descriptors(self, image): |
| 228 | + magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC) |
| 229 | + index = image.find(magic) |
| 230 | + if index == -1: |
| 231 | + log.die('Could not find binary descriptor magic') |
| 232 | + |
| 233 | + descriptors = {} |
| 234 | + |
| 235 | + index += len(magic) # index points to first descriptor |
| 236 | + current_tag = self.bytes_to_short(image[index:index+2]) |
| 237 | + while current_tag != self.DESCRIPTORS_END: |
| 238 | + index += 2 # index points to length |
| 239 | + length = self.bytes_to_short(image[index:index+2]) |
| 240 | + index += 2 # index points to data |
| 241 | + data = image[index:index+length] |
| 242 | + |
| 243 | + tag_type = self.bindesc_get_type(current_tag) |
| 244 | + if tag_type == self.TYPE_STR: |
| 245 | + decoded_data = data[:-1].decode('ascii') |
| 246 | + elif tag_type == self.TYPE_UINT: |
| 247 | + decoded_data = self.bytes_to_uint(data) |
| 248 | + elif tag_type == self.TYPE_BYTES: |
| 249 | + decoded_data = data |
| 250 | + else: |
| 251 | + log.die(f'Unknown type for tag 0x{current_tag:04x}') |
| 252 | + |
| 253 | + key = f'0x{current_tag:04x}' |
| 254 | + descriptors[key] = decoded_data |
| 255 | + index += length |
| 256 | + index = self.align(index, 4) |
| 257 | + current_tag = self.bytes_to_short(image[index:index+2]) |
| 258 | + |
| 259 | + return descriptors |
| 260 | + |
| 261 | + def guess_file_type(self, args): |
| 262 | + if "file" not in args: |
| 263 | + return None |
| 264 | + |
| 265 | + # If file type is explicitly given, use it |
| 266 | + if args.file_type is not None: |
| 267 | + return args.file_type |
| 268 | + |
| 269 | + # If the file has a known extension, use it |
| 270 | + for extension in self.EXTENSIONS: |
| 271 | + if args.file.endswith(f'.{extension}'): |
| 272 | + return extension |
| 273 | + |
| 274 | + with open(args.file, 'rb') as f: |
| 275 | + header = f.read(1024) |
| 276 | + |
| 277 | + # Try the elf magic |
| 278 | + if header.startswith(b'\x7fELF'): |
| 279 | + return 'elf' |
| 280 | + |
| 281 | + # Try the uf2 magic |
| 282 | + if header.startswith(b'UF2\n'): |
| 283 | + return 'uf2' |
| 284 | + |
| 285 | + try: |
| 286 | + # if the file is textual it's probably hex |
| 287 | + header.decode('ascii') |
| 288 | + return 'hex' |
| 289 | + except UnicodeDecodeError: |
| 290 | + # Default to bin |
| 291 | + return 'bin' |
| 292 | + |
| 293 | + def bytes_to_uint(self, b): |
| 294 | + return struct.unpack('>I' if self.is_big_endian else 'I', b)[0] |
| 295 | + |
| 296 | + def bytes_to_short(self, b): |
| 297 | + return struct.unpack('>H' if self.is_big_endian else 'H', b)[0] |
| 298 | + |
| 299 | + @staticmethod |
| 300 | + def bindesc_gen_tag(_type, _id): |
| 301 | + return f'0x{(_type << 12 | _id):04x}' |
| 302 | + |
| 303 | + @staticmethod |
| 304 | + def bindesc_get_type(tag): |
| 305 | + return tag >> 12 |
| 306 | + |
| 307 | + @staticmethod |
| 308 | + def align(x, alignment): |
| 309 | + return (x + alignment - 1) & (~(alignment - 1)) |
| 310 | + |
| 311 | + @staticmethod |
| 312 | + def bindesc_repr(value): |
| 313 | + if isinstance(value, str): |
| 314 | + return f'"{value}"' |
| 315 | + if isinstance(value, (int, bytes)): |
| 316 | + return f'{value}' |
0 commit comments