1# Copyright (c) 2023 Yonatan Schachter 2# 3# SPDX-License-Identifier: Apache-2.0 4 5import struct 6from textwrap import dedent 7 8from west.commands import WestCommand 9 10try: 11 from elftools.elf.elffile import ELFFile 12 from intelhex import IntelHex 13 MISSING_REQUIREMENTS = False 14except ImportError: 15 MISSING_REQUIREMENTS = True 16 17 18# Based on scripts/build/uf2conv.py 19def convert_from_uf2(cmd, buf): 20 UF2_MAGIC_START0 = 0x0A324655 # First magic number ('UF2\n') 21 UF2_MAGIC_START1 = 0x9E5D5157 # Second magic number 22 numblocks = len(buf) // 512 23 curraddr = None 24 outp = [] 25 for blockno in range(numblocks): 26 ptr = blockno * 512 27 block = buf[ptr:ptr + 512] 28 hd = struct.unpack(b'<IIIIIIII', block[0:32]) 29 if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1: 30 cmd.inf(f'Skipping block at {ptr}; bad magic') 31 continue 32 if hd[2] & 1: 33 # NO-flash flag set; skip block 34 continue 35 datalen = hd[4] 36 if datalen > 476: 37 cmd.die(f'Invalid UF2 data size at {ptr}') 38 newaddr = hd[3] 39 if curraddr is None: 40 curraddr = newaddr 41 padding = newaddr - curraddr 42 if padding < 0: 43 cmd.die(f'Block out of order at {ptr}') 44 if padding > 10*1024*1024: 45 cmd.die(f'More than 10M of padding needed at {ptr}') 46 if padding % 4 != 0: 47 cmd.die(f'Non-word padding size at {ptr}') 48 while padding > 0: 49 padding -= 4 50 outp += b'\x00\x00\x00\x00' 51 outp.append(block[32 : 32 + datalen]) 52 curraddr = newaddr + datalen 53 return b''.join(outp) 54 55 56class Bindesc(WestCommand): 57 EXTENSIONS = ['bin', 'hex', 'elf', 'uf2'] 58 59 # Corresponds to the definitions in include/zephyr/bindesc.h. 60 # Do not change without syncing the definitions in both files! 61 TYPE_UINT = 0 62 TYPE_STR = 1 63 TYPE_BYTES = 2 64 MAGIC = 0xb9863e5a7ea46046 65 DESCRIPTORS_END = 0xffff 66 67 def __init__(self): 68 self.TAG_TO_NAME = { 69 # Corresponds to the definitions in include/zephyr/bindesc.h. 70 # Do not change without syncing the definitions in both files! 71 self.bindesc_gen_tag(self.TYPE_STR, 0x800): 'APP_VERSION_STRING', 72 self.bindesc_gen_tag(self.TYPE_UINT, 0x801): 'APP_VERSION_MAJOR', 73 self.bindesc_gen_tag(self.TYPE_UINT, 0x802): 'APP_VERSION_MINOR', 74 self.bindesc_gen_tag(self.TYPE_UINT, 0x803): 'APP_VERSION_PATCHLEVEL', 75 self.bindesc_gen_tag(self.TYPE_UINT, 0x804): 'APP_VERSION_NUMBER', 76 self.bindesc_gen_tag(self.TYPE_STR, 0x805): 'APP_BUILD_VERSION', 77 self.bindesc_gen_tag(self.TYPE_STR, 0x900): 'KERNEL_VERSION_STRING', 78 self.bindesc_gen_tag(self.TYPE_UINT, 0x901): 'KERNEL_VERSION_MAJOR', 79 self.bindesc_gen_tag(self.TYPE_UINT, 0x902): 'KERNEL_VERSION_MINOR', 80 self.bindesc_gen_tag(self.TYPE_UINT, 0x903): 'KERNEL_VERSION_PATCHLEVEL', 81 self.bindesc_gen_tag(self.TYPE_UINT, 0x904): 'KERNEL_VERSION_NUMBER', 82 self.bindesc_gen_tag(self.TYPE_STR, 0x905): 'KERNEL_BUILD_VERSION', 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', required=True) 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 extract_parser = subparsers.add_parser('extract', 124 help='Extract the binary descriptor blob to a file') 125 extract_parser.add_argument('file', type=str, help='Executable file') 126 extract_parser.add_argument('out_file', type=str, help='Bindesc binary dump file') 127 extract_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, 128 help='Input file type') 129 extract_parser.add_argument('-b', '--big-endian', action='store_true', 130 help='Target CPU is big endian') 131 extract_parser.set_defaults(subcmd='extract', big_endian=False) 132 133 search_parser = subparsers.add_parser('search', help='Search for a specific descriptor') 134 search_parser.add_argument('descriptor', type=str, help='Descriptor name') 135 search_parser.add_argument('file', type=str, help='Executable file') 136 search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, 137 help='File type') 138 search_parser.add_argument('-b', '--big-endian', action='store_true', 139 help='Target CPU is big endian') 140 search_parser.set_defaults(subcmd='search', big_endian=False) 141 142 custom_search_parser = subparsers.add_parser('custom_search', 143 help='Search for a custom descriptor') 144 custom_search_parser.add_argument('type', type=str, choices=['UINT', 'STR', 'BYTES'], 145 help='Descriptor type') 146 custom_search_parser.add_argument('id', type=str, help='Descriptor ID in hex') 147 custom_search_parser.add_argument('file', type=str, help='Executable file') 148 custom_search_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, 149 help='File type') 150 custom_search_parser.add_argument('-b', '--big-endian', action='store_true', 151 help='Target CPU is big endian') 152 custom_search_parser.set_defaults(subcmd='custom_search', big_endian=False) 153 154 list_parser = subparsers.add_parser('list', help='List all known descriptors') 155 list_parser.set_defaults(subcmd='list', big_endian=False) 156 157 get_offset_parser = subparsers.add_parser('get_offset', 158 help='Get the offset of the descriptors') 159 get_offset_parser.add_argument('file', type=str, help='Executable file') 160 get_offset_parser.add_argument('--file-type', type=str, choices=self.EXTENSIONS, 161 help='File type') 162 get_offset_parser.add_argument('-b', '--big-endian', action='store_true', 163 help='Target CPU is big endian') 164 get_offset_parser.set_defaults(subcmd='get_offset', big_endian=False) 165 return parser 166 167 def dump(self, args): 168 image = self.get_image_data(args.file) 169 170 descriptors = self.parse_descriptors(image) 171 for tag, value in descriptors.items(): 172 if tag in self.TAG_TO_NAME: 173 tag = self.TAG_TO_NAME[tag] 174 self.inf(f'{tag}', self.bindesc_repr(value)) 175 176 def list(self, args): 177 for tag in self.TAG_TO_NAME.values(): 178 self.inf(f'{tag}') 179 180 def common_search(self, args, search_term): 181 image = self.get_image_data(args.file) 182 183 descriptors = self.parse_descriptors(image) 184 185 if search_term in descriptors: 186 value = descriptors[search_term] 187 self.inf(self.bindesc_repr(value)) 188 else: 189 self.die('Descriptor not found') 190 191 def search(self, args): 192 try: 193 search_term = self.NAME_TO_TAG[args.descriptor] 194 except KeyError: 195 self.die(f'Descriptor {args.descriptor} is invalid') 196 197 self.common_search(args, search_term) 198 199 def custom_search(self, args): 200 custom_type = { 201 'STR': self.TYPE_STR, 202 'UINT': self.TYPE_UINT, 203 'BYTES': self.TYPE_BYTES 204 }[args.type] 205 custom_tag = self.bindesc_gen_tag(custom_type, int(args.id, 16)) 206 self.common_search(args, custom_tag) 207 208 def get_offset(self, args): 209 image = self.get_image_data(args.file) 210 211 magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC) 212 index = image.find(magic) 213 if index == -1: 214 self.die('Could not find binary descriptor magic') 215 self.inf(f'{index} {hex(index)}') 216 217 def extract(self, args): 218 image = self.get_image_data(args.file) 219 220 magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC) 221 index = image.find(magic) 222 if index == -1: 223 self.die('Could not find binary descriptor magic') 224 225 index += len(magic) # index points to first descriptor 226 block_start = index 227 current_tag = self.bytes_to_short(image[index:index+2]) 228 while current_tag != self.DESCRIPTORS_END: 229 index += 2 # index points to length 230 length = self.bytes_to_short(image[index:index+2]) 231 # go to next tag 232 index = self.align(index + 2 + length, 4) 233 current_tag = self.bytes_to_short(image[index:index+2]) 234 block_len = index - block_start 235 236 with open(args.out_file, 'wb') as out_file: 237 out_file.write(image[block_start:index]) 238 self.inf(f'{block_start}+{block_len} {hex(block_start)}+{hex(block_len)}') 239 240 def do_run(self, args, _): 241 if MISSING_REQUIREMENTS: 242 raise RuntimeError('one or more Python dependencies were missing; ' 243 'see the getting started guide for details on ' 244 'how to fix') 245 self.is_big_endian = args.big_endian 246 self.file_type = self.guess_file_type(args) 247 subcmd = getattr(self, args.subcmd) 248 subcmd(args) 249 250 def get_image_data(self, file_name): 251 if self.file_type == 'bin': 252 with open(file_name, 'rb') as bin_file: 253 return bin_file.read() 254 255 if self.file_type == 'hex': 256 return IntelHex(file_name).tobinstr() 257 258 if self.file_type == 'uf2': 259 with open(file_name, 'rb') as uf2_file: 260 return convert_from_uf2(self, uf2_file.read()) 261 262 if self.file_type == 'elf': 263 with open(file_name, 'rb') as f: 264 elffile = ELFFile(f) 265 266 section = elffile.get_section_by_name('rom_start') 267 if section: 268 return section.data() 269 270 section = elffile.get_section_by_name('text') 271 if section: 272 return section.data() 273 274 self.die('No "rom_start" or "text" section found') 275 276 self.die('Unknown file type') 277 278 def parse_descriptors(self, image): 279 magic = struct.pack('>Q' if self.is_big_endian else 'Q', self.MAGIC) 280 index = image.find(magic) 281 if index == -1: 282 self.die('Could not find binary descriptor magic') 283 284 descriptors = {} 285 286 index += len(magic) # index points to first descriptor 287 current_tag = self.bytes_to_short(image[index:index+2]) 288 while current_tag != self.DESCRIPTORS_END: 289 index += 2 # index points to length 290 length = self.bytes_to_short(image[index:index+2]) 291 index += 2 # index points to data 292 data = image[index:index+length] 293 294 tag_type = self.bindesc_get_type(current_tag) 295 if tag_type == self.TYPE_STR: 296 decoded_data = data[:-1].decode('ascii') 297 elif tag_type == self.TYPE_UINT: 298 decoded_data = self.bytes_to_uint(data) 299 elif tag_type == self.TYPE_BYTES: 300 decoded_data = data 301 else: 302 self.die(f'Unknown type for tag 0x{current_tag:04x}') 303 304 key = f'0x{current_tag:04x}' 305 descriptors[key] = decoded_data 306 index += length 307 index = self.align(index, 4) 308 current_tag = self.bytes_to_short(image[index:index+2]) 309 310 return descriptors 311 312 def guess_file_type(self, args): 313 if "file" not in args: 314 return None 315 316 # If file type is explicitly given, use it 317 if args.file_type is not None: 318 return args.file_type 319 320 # If the file has a known extension, use it 321 for extension in self.EXTENSIONS: 322 if args.file.endswith(f'.{extension}'): 323 return extension 324 325 with open(args.file, 'rb') as f: 326 header = f.read(1024) 327 328 # Try the elf magic 329 if header.startswith(b'\x7fELF'): 330 return 'elf' 331 332 # Try the uf2 magic 333 if header.startswith(b'UF2\n'): 334 return 'uf2' 335 336 try: 337 # if the file is textual it's probably hex 338 header.decode('ascii') 339 return 'hex' 340 except UnicodeDecodeError: 341 # Default to bin 342 return 'bin' 343 344 def bytes_to_uint(self, b): 345 return struct.unpack('>I' if self.is_big_endian else 'I', b)[0] 346 347 def bytes_to_short(self, b): 348 return struct.unpack('>H' if self.is_big_endian else 'H', b)[0] 349 350 @staticmethod 351 def bindesc_gen_tag(_type, _id): 352 return f'0x{(_type << 12 | _id):04x}' 353 354 @staticmethod 355 def bindesc_get_type(tag): 356 return tag >> 12 357 358 @staticmethod 359 def align(x, alignment): 360 return (x + alignment - 1) & (~(alignment - 1)) 361 362 @staticmethod 363 def bindesc_repr(value): 364 if isinstance(value, str): 365 return f'"{value}"' 366 if isinstance(value, int | bytes): 367 return f'{value}' 368