Skip to content

Instantly share code, notes, and snippets.

@apua
Last active May 30, 2025 16:05
Show Gist options
  • Save apua/dd68b7eecc6da87fb9720f0da1228f03 to your computer and use it in GitHub Desktop.
Save apua/dd68b7eecc6da87fb9720f0da1228f03 to your computer and use it in GitHub Desktop.
# Ref: `Devicetree Specification - 5. Flattened Devicetree (DTB) Format
# <https://devicetree-specification.readthedocs.io/en/stable/flattened-format.html>`_
import collections, io, logging, os, sys
def bytes2int(b): return int.from_bytes(b, 'big')
def int2bytes(i): return int.to_bytes(i, 4, 'big')
logging.basicConfig(level=getattr(logging, os.getenv('LOG', 'info').upper()), format='%(levelname)s:%(funcName)s:%(message)s')
logger = logging.getLogger()
fd = open(sys.argv[1], 'rb') if sys.stdin.isatty() else io.BytesIO(sys.stdin.buffer.read())
def read_bytes(n):
if len(buffer := fd.read(n)) < n: raise EOFError
else: return buffer
# 5.2. Header
# ===========
magic = read_bytes(4)
totalsize = read_bytes(4)
off_dt_struct = read_bytes(4)
off_dt_strings = read_bytes(4)
off_mem_rsvmap = read_bytes(4)
version = read_bytes(4)
last_comp_version = read_bytes(4)
boot_cpuid_phys = read_bytes(4)
size_dt_strings = read_bytes(4)
size_dt_struct = read_bytes(4)
logger.info('%s magic %d', magic.hex(), bytes2int(magic))
logger.info('%s totalsize %d', totalsize.hex(), bytes2int(totalsize))
logger.info('%s off_dt_struct %d', off_dt_struct.hex(), bytes2int(off_dt_struct))
logger.info('%s off_dt_strings %d', off_dt_strings.hex(), bytes2int(off_dt_strings))
logger.info('%s off_mem_rsvmap %d', off_mem_rsvmap.hex(), bytes2int(off_mem_rsvmap))
logger.info('%s version %d', version.hex(), bytes2int(version))
logger.info('%s last_comp_version %d', last_comp_version.hex(), bytes2int(last_comp_version))
logger.info('%s boot_cpuid_phys %d', boot_cpuid_phys.hex(), bytes2int(boot_cpuid_phys))
logger.info('%s size_dt_strings %d', size_dt_strings.hex(), bytes2int(size_dt_strings))
logger.info('%s size_dt_struct %d', size_dt_struct.hex(), bytes2int(size_dt_struct))
if not (magic.hex() == 'd00dfeed'): raise ValueError('FATAL ERROR: Blob has incorrect magic number')
if not (bytes2int(off_mem_rsvmap) == (size_header := 4 * 10)): raise ValueError
if not (bytes2int(off_dt_strings) == bytes2int(off_dt_struct) + bytes2int(size_dt_struct)): raise ValueError
if not (bytes2int(totalsize) == bytes2int(off_dt_strings) + bytes2int(size_dt_strings)): raise ValueError
logger.info('version: %d', bytes2int(version))
logger.info('last compatible version: %d', bytes2int(last_comp_version))
logger.info('boot CPU physical ID: %d', bytes2int(boot_cpuid_phys))
# 5.3. Memory Reservation Block
# =============================
fd.seek(bytes2int(off_mem_rsvmap))
fdt_reserve_entry = collections.namedtuple('fdt_reserve_entry', 'address, size')
# > The list of reserved blocks shall be terminated with an entry where both address and size are equal to 0.
mem_rsvmap = []
while (entry := fdt_reserve_entry(read_bytes(8), read_bytes(8))) and not (bytes2int(entry.address) == 0 and bytes2int(entry.size) == 0):
mem_rsvmap.append(entry)
for m in mem_rsvmap: logger.info('/memreserve/ 0x%s 0x%s;', m.address.hex(), m.size.hex())
# 5.4. Structure Block
# ====================
fd.seek(bytes2int(off_dt_struct))
FDT_BEGIN_NODE = int2bytes(0x00000001)
FDT_END_NODE = int2bytes(0x00000002)
FDT_PROP = int2bytes(0x00000003)
FDT_NOP = int2bytes(0x00000004)
FDT_END = int2bytes(0x00000009)
Node = collections.namedtuple('Node', 'name, properties, children')
Property = collections.namedtuple('Property', 'name, value')
def get_null_terminated_string(n):
buffer = b''
while s := read_bytes(n):
buffer += s
if s[-1] == 0:
return buffer
def get_property():
length, nameoff = read_bytes(4), read_bytes(4)
logger.debug('get_property length=%s nameoff=%s', length, nameoff)
# > This value is followed by zeroed padding bytes (if necessary) to align to the next 32-bit boundary
_length = bytes2int(length)
padding = (4 - m) if (m := _length % 4) else 0
value = read_bytes(_length + padding)
position = fd.tell()
fd.seek(bytes2int(off_dt_strings) + bytes2int(nameoff))
# > The strings block has no alignment constraints
name = get_null_terminated_string(1)
fd.seek(position)
logger.debug('get_property name=%s value=%s', name, value)
return Property(name, value)
def get_node():
name = get_null_terminated_string(4)
logger.debug('get_node name=%s', name)
properties = []
children = []
while token := read_bytes(4):
logger.debug('get_node token=%s', token)
if token == FDT_NOP: continue
elif token == FDT_BEGIN_NODE: children.append(get_node())
elif token == FDT_PROP: properties.append(get_property())
elif token == FDT_END_NODE: break
else: raise ValueError(token)
return Node(name, properties, children)
while (token := read_bytes(4)) and token == FDT_NOP: pass
if not (token == FDT_BEGIN_NODE): raise ValueError
root = get_node()
while (token := read_bytes(4)) and token == FDT_NOP: pass
if not (token == FDT_END): raise ValueError
# 6.4. File layout
# ================
indent = ' ' * 4
def property2expr(prop, nested):
name = prop.name.decode().rstrip('\x00')
try:
value = prop.value.decode().rstrip('\x00')
if not value.isprintable(): raise UnicodeError
except UnicodeError:
assert len(prop.value) % 4 == 0
vs = (bytes2int(prop.value[i:i+4]) for i in range(0, len(prop.value), 4))
value_repr = '<' + ' '.join(f'0x{i:02x}' for i in vs) + '>'
else:
value_repr = f'"{value}"'
return indent * nested + f'{name} = {value_repr};'
def node2expr(node, nested=0):
name = node.name.decode().rstrip('\x00') or '/'
yield indent * nested + f'{name} {{'
for prop in node.properties:
yield property2expr(prop, nested+1)
for child in node.children:
yield from node2expr(child, nested+1)
yield indent * nested + '};'
dts_lines = ['/dts-v1/;']
dts_lines.extend(f'/memreserve/ 0x{entry.address.hex()} 0x{entry.size.hex()};' for entry in mem_rsvmap)
dts_lines.extend(node2expr(root))
print(*dts_lines, sep='\n')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment