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