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 9from west import log 10 11 12try: 13 from elftools.elf.elffile import ELFFile 14 from intelhex import IntelHex 15 MISSING_REQUIREMENTS = False 16except ImportError: 17 MISSING_REQUIREMENTS = True 18 19 20# Based on scripts/build/uf2conv.py 21def 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 58class 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', 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 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}' 317