Skip to content

Commit c42a7df

Browse files
yonschnashif
authored andcommitted
west: commands: Add bindesc command
Added the bindesc command to west, for working with binary descriptors. Currently it supports dump, list and search subcommands, for bin, hex, elf and uf2 file types. Signed-off-by: Yonatan Schachter <[email protected]>
1 parent 5508b17 commit c42a7df

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

scripts/west-commands.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,8 @@ west-commands:
5656
- name: blobs
5757
class: Blobs
5858
help: work with binary blobs
59+
- file: scripts/west_commands/bindesc.py
60+
commands:
61+
- name: bindesc
62+
class: Bindesc
63+
help: work with Binary Descriptors

scripts/west_commands/bindesc.py

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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

Comments
 (0)