From ce599be70d8654e8f11858a6f0c4e8a92e27064b Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Thu, 9 Mar 2023 15:52:46 -0600 Subject: [PATCH] Added scripts/dbgbtree.py for debugging B-trees, tweaked dbgrbyd.py An example: $ ./scripts/dbgbtree.py -B4096 disk 0xaa -t -i btree 0xaa.1000, rev 35, weight 278 block ids name tag data (truncated) 00aa.1000: +-+ 0-16 branch id16 3 7e d4 10 ~.. 007e.0854: | |-> 0 inlined id0 1 73 s | |-> 1 inlined id1 1 74 t | |-> 2 inlined id2 1 75 u | |-> 3 inlined id3 1 76 v | |-> 4 inlined id4 1 77 w | |-> 5 inlined id5 1 78 x | |-> 6 inlined id6 1 79 y | |-> 7 inlined id7 1 7a z | |-> 8 inlined id8 1 61 a | |-> 9 inlined id9 1 62 b ... This added the idea of block+limit addresses such as 0xaa.1000. Added this as an option to dbgrbyd.py along with a couple other tweaks: - Added block+limit support (0x.). - Fixed in-device representation indentation when trees are present. - Changed fromtag to implicitly fixup ids/weights off-by-one-ness, this is consistent with lfs.c. --- scripts/dbgbtree.py | 607 ++++++++++++++++++++++++++++++++++++++++++++ scripts/dbgrbyd.py | 111 +++++--- 2 files changed, 679 insertions(+), 39 deletions(-) create mode 100755 scripts/dbgbtree.py diff --git a/scripts/dbgbtree.py b/scripts/dbgbtree.py new file mode 100755 index 00000000..31de3bb2 --- /dev/null +++ b/scripts/dbgbtree.py @@ -0,0 +1,607 @@ +#!/usr/bin/env python3 + +import bisect +import itertools as it +import math as m +import os +import struct + + +def blocklim(s): + if '.' in s: + s = s.strip() + b = 10 + if s.startswith('0x') or s.startswith('0X'): + s = s[2:] + b = 16 + elif s.startswith('0o') or s.startswith('0O'): + s = s[2:] + b = 8 + elif s.startswith('0b') or s.startswith('0B'): + s = s[2:] + b = 2 + + s0, s1 = s.split('.', 1) + return int(s0, b), int(s1, b) + else: + return int(s, 0) + +def crc32c(data, crc=0): + crc ^= 0xffffffff + for b in data: + crc ^= b + for j in range(8): + crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78) + return 0xffffffff ^ crc + +def fromleb128(data): + word = 0 + for i, b in enumerate(data): + word |= ((b & 0x7f) << 7*i) + word &= 0xffffffff + if not b & 0x80: + return word, i+1 + return word, len(data) + +def fromtag(data): + tag, delta = fromleb128(data) + id, delta_ = fromleb128(data[delta:]) + size, delta__ = fromleb128(data[delta+delta_:]) + return tag&1, tag&~1, id if tag&0x8 else id-1, size, delta+delta_+delta__ + +def popc(x): + return bin(x).count('1') + +def xxd(data, width=16, crc=False): + for i in range(0, len(data), width): + yield '%-*s %-*s' % ( + 3*width, + ' '.join('%02x' % b for b in data[i:i+width]), + width, + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, data[i:i+width]))) + +def tagrepr(tag, id, size, off=None): + if (tag & ~0x3f0) == 0x0400: + return 'mk%s id%d %d' % ( + 'branch' if ((tag & 0x3f0) >> 4) == 0x00 + else 'reg' if ((tag & 0x3f0) >> 4) == 0x01 + else 'dir' if ((tag & 0x3f0) >> 4) == 0x02 + else ' 0x%02x' % ((tag & 0x3f0) >> 4), + id, + size) + elif tag == 0x0800: + return 'inlined id%d %d' % (id, size) + elif tag == 0x0810: + return 'block id%d %d' % (id, size) + elif tag == 0x0820: + return 'btree id%d %d' % (id, size) + elif tag == 0x0830: + return 'branch id%d %d' % (id, size) + elif (tag & ~0xff2) == 0x2000: + return '%suattr 0x%02x%s%s' % ( + 'rm' if tag & 0x2 else '', + (tag & 0xff0) >> 4, + ' id%d' % id if id != -1 else '', + ' %d' % size if not tag & 0x2 or size else '') + elif tag == 0x0006: + return 'grow id%d w%d' % ( + id, + size) + elif tag == 0x0016: + return 'shrink id%d w%d' % ( + id, + size) + elif (tag & ~0x10) == 0x0004: + return 'crc%x%s %d' % ( + 1 if tag & 0x10 else 0, + ' 0x%02x' % id if id != -1 else '', + size) + elif tag == 0x0024: + return 'fcrc%s %d' % ( + ' 0x%02x' % id if id != -1 else '', + size) + elif tag & 0x8: + return 'alt%s%s 0x%x w%d %s' % ( + 'r' if tag & 0x2 else 'b', + 'gt' if tag & 0x4 else 'le', + tag & 0x3ff0, + id, + '0x%x' % (0xffffffff & (off-size)) + if off is not None + else '-%d' % off) + else: + return '0x%04x id%d %d' % (tag, id, size) + + +class Rbyd: + def __init__(self, block, limit, data, rev, off, trunk, weight): + self.block = block + self.limit = limit + self.data = data + self.rev = rev + self.off = off + self.trunk = trunk + self.weight = weight + + @classmethod + def fetch(cls, f, block_size, block, limit): + # seek to the block + f.seek(block * block_size) + data = f.read(limit) + + # fetch the rbyd + rev, = struct.unpack(' (upper-weight_-1, alt & ~0xf) + if alt & 0x4 + else ((id, tag & ~0xf) <= (lower+weight_, alt & ~0xf))): + lower += upper-lower-1-weight_ if alt & 0x4 else 0 + upper -= upper-lower-1-weight_ if not alt & 0x4 else 0 + j = j - jump + # stay on path + else: + lower += weight_ if not alt & 0x4 else 0 + upper -= weight_ if alt & 0x4 else 0 + j = j + delta + # found tag + else: + tag_ = alt + id_ = upper-1 + w_ = id_-lower + + done = (id_, tag_) < (id, tag) or tag_ & 2 + + return (done, tag_, id_, w_, + j, delta, self.data[j+delta:j+delta+jump]) + + def __bool__(self): + return self.trunk is not None + + def __eq__(self, other): + return self.block == other.block and self.limit == other.limit + + def __ne__(self, other): + return not self.__eq__(other) + + def __iter__(self): + tag = 0 + id = 0 + + while True: + done, tag, id, w, j, d, data = self.lookup(tag+0x10, id) + if done: + break + + yield tag, id, w, j, d, data + + +def main(disk, block_size=None, trunk=0, limit=None, *, + color='auto', + **args): + # figure out what color should be + if color == 'auto': + color = sys.stdout.isatty() + elif color == 'always': + color = True + else: + color = False + + # trunk may include a limit + if isinstance(trunk, tuple): + if limit is None: + limit = trunk[1] + trunk = trunk[0] + + # we seek around a bunch, so just keep the disk open + with open(disk, 'rb') as f: + # if block_size is omitted, assume the block device is one big block + if block_size is None: + f.seek(0, os.SEEK_END) + block_size = f.tell() + + # default limit to the block_size + if limit is None: + limit = block_size + + # fetch the trunk + trunk = Rbyd.fetch(f, block_size, trunk, limit) + print('btree 0x%x.%x, rev %d, weight %d' % ( + trunk.block, trunk.limit, trunk.rev, trunk.weight)) + + # look up an id, while keeping track of the search path + def lookup(id, depth=None): + rbyd = trunk + rid = id + depth_ = 1 + path = [] + + # corrupted? return a corrupted block once + if not rbyd: + return (id > 0, id, 0, rbyd, -1, 0, + 0, 0, b'', + 0, 0, b'', + path) + + while True: + # first lookup id/name + (done, _, rid_, w, + name_j, name_d, name) = rbyd.lookup(0x400, rid) + if done: + return True, id, 0, rbyd, -1, 0, 0, 0, b'', 0, 0, b'', path + + # then lookup struct + (done, tag, _, _, + struct_j, struct_d, struct_) = rbyd.lookup(0x800, rid_) + if done: + return True, id, 0, rbyd, -1, 0, 0, 0, b'', 0, 0, b'', path + + path.append((id + (rid_-rid), w, rbyd, rid_, tag, + name_j, name_d, name, + struct_j, struct_d, struct_)) + + # is it another branch? continue down tree + if tag == 0x830 and (depth is None or depth_ < depth): + block, delta = fromleb128(struct_) + limit, _ = fromleb128(struct_[delta:]) + rbyd = Rbyd.fetch(f, block_size, block, limit) + + # corrupted? bail here so we can keep traversing the tree + if not rbyd: + return (False, id + (rid_-rid), w, rbyd, -1, 0, + 0, 0, b'', + 0, 0, b'', + path) + + rid -= (rid_-(w-1)) + depth_ += 1 + else: + return (False, id + (rid_-rid), w, rbyd, rid_, tag, + name_j, name_d, name, + struct_j, struct_d, struct_, + path) + + # if we're printing the tree, first find the max depth so we know how + # much space to reserve + t_width = 0 + if args.get('tree'): + t_depth = 0 + id = -1 + while True: + (done, id, w, rbyd, rid, tag, + name_j, name_d, name, + struct_j, struct_d, struct_, + path) = (lookup(id+1, depth=args.get('depth'))) + if done: + break + + t_depth = max(t_depth, len(path)) + + t_width = 2*t_depth+2 if t_depth > 0 else 0 + t_branches = [(0, trunk.weight)] + + def t_repr(id, w, d=None): + branches_ = [] + for i in range(len(t_branches)): + if d is not None and d == i-1: + branches_.append('+') + elif i+1 < len(t_branches): + if (id-(w-1) == t_branches[i+1][0] + and t_branches[i+1][0] == t_branches[i][0] + and (not args.get('inner') + or (i == 0 and d == 0))): + branches_.append('+-') + elif (id-(w-1) == t_branches[i+1][0] + and t_branches[i+1][1]-1 == t_branches[i][1]-1 + and (not args.get('inner') or d == i)): + branches_.append('\'-') + elif (id-(w-1) == t_branches[i+1][0] + and (not args.get('inner') or d == i)): + branches_.append('|-') + elif (id-(w-1) >= t_branches[i][0] + and id-(w-1) < t_branches[i][1] + and t_branches[i+1][1]-1 != t_branches[i][1]-1): + branches_.append('| ') + else: + branches_.append(' ') + else: + if (id-(w-1) == t_branches[i][0] + and (not args.get('inner') or i == 0)): + branches_.append('+-%s> ' % ('-'*2*(t_depth-i-1))) + elif id-(w-1) == t_branches[i][1]-1: + branches_.append('\'-%s> ' % ('-'*2*(t_depth-i-1))) + elif (id-(w-1) >= t_branches[i][0] + and id-(w-1) < t_branches[i][1]): + branches_.append('|-%s> ' % ('-'*2*(t_depth-i-1))) + + return '%s%-*s%s' % ( + '\x1b[90m' if color else '', + t_width, ''.join(branches_), + '\x1b[m' if color else '') + + # print header + w_width = 2*m.ceil(m.log10(max(1, trunk.weight)+1))+1 + print('%-9s %*s%-*s %-8s %-22s %s' % ( + 'block', + t_width, '', + w_width, 'ids', + 'name', + 'tag', + 'data (truncated)' + if not args.get('no_truncate') else '')) + + # traverse and print entries + id = -1 + prbyd = None + ppath = [] + corrupted = False + while True: + (done, id, w, rbyd, rid, tag, + name_j, name_d, name, + struct_j, struct_d, struct_, + path) = (lookup(id+1, depth=args.get('depth'))) + if done: + break + + if args.get('inner') or args.get('tree'): + t_branches = [(0, trunk.weight)] + changed = False + for i, (x, px) in enumerate( + it.zip_longest(path[:-1], ppath[:-1])): + if x is None: + break + + (id_, w_, rbyd_, rid_, tag_, + name_j_, name_d_, name_, + struct_j_, struct_d_, struct__) = x + t_branches.append((id_-(w_-1), id_+1)) + + if args.get('inner'): + if not (changed or px is None or x != px): + continue + changed = True + + # show human-readable representation + print('%10s %s%*s %-8s %-22s %s' % ( + '%04x.%04x:' % (rbyd_.block, rbyd_.limit) + if prbyd is None or rbyd_ != prbyd + else '', + t_repr(id_, w_, i) if args.get('tree') else '', + w_width, '%d-%d' % (id_-(w_-1), id_) + if w_ > 1 else id_ + if w_ > 0 else '', + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, name_)), + tagrepr(tag_, rid_, len(struct__), None), + next(xxd(struct__, 8), '') + if not args.get('no_truncate') else '')) + + # show in-device representation + if args.get('device'): + print('%9s %*s%*s %8s %-22s%s' % ( + '', + t_width, '', + w_width, '', + '', + '%04x %08x %07x' % ( + tag_, 0xffffffff & rid_, len(struct__)), + ' %s' % ' '.join( + '%08x' % struct.unpack(' 1 else id + if w > 0 else '', + ''.join( + b if b >= ' ' and b <= '~' else '.' + for b in map(chr, name)), + tagrepr(tag, rid, len(struct_), None), + next(xxd(struct_, 8), '') + if not args.get('no_truncate') else '')) + + # show in-device representation + if args.get('device'): + print('%9s %*s%*s %8s %-22s%s' % ( + '', + t_width, '', + w_width, '', + '', + '%04x %08x %07x' % ( + tag, 0xffffffff & rid, len(struct_)), + ' %s' % ' '.join( + '%08x' % struct.unpack('= ' ' and b <= '~' else '.' for b in map(chr, data[i:i+width]))) -def tagrepr(tag, id, w, size, off=None): +def tagrepr(tag, id, size, off=None): if (tag & ~0x3f0) == 0x0400: - return 'mk%s id%d%s %d' % ( + return 'mk%s id%d %d' % ( 'branch' if ((tag & 0x3f0) >> 4) == 0x00 else 'reg' if ((tag & 0x3f0) >> 4) == 0x01 else 'dir' if ((tag & 0x3f0) >> 4) == 0x02 else ' 0x%02x' % ((tag & 0x3f0) >> 4), id, - ' w%d' % w if w is not None else '', size) elif tag == 0x0800: return 'inlined id%d %d' % (id, size) @@ -98,7 +116,7 @@ def tagrepr(tag, id, w, size, off=None): 'r' if tag & 0x2 else 'b', 'gt' if tag & 0x4 else 'le', tag & 0x3ff0, - id+1, + id, '0x%x' % (0xffffffff & (off-size)) if off is not None else '-%d' % off) @@ -300,7 +318,7 @@ def show_log(block_size, data, rev, off, *, lifetimerepr(j) if args.get('lifetimes') else '', '\x1b[90m' if color and j >= off else '', '%-22s%s' % ( - tagrepr(tag, id, None, size, j), + tagrepr(tag, id, size, j), ' %s' % next(xxd( data[j+delta:j+delta+min(size, 8)], 8), '') if not args.get('no_truncate') @@ -334,7 +352,8 @@ def show_log(block_size, data, rev, off, *, data[j+delta+i*4:j+delta+min(i*4+4,size)] .ljust(4, b'\0')) for i in range(min(m.ceil(size/4), 3)))[:23] - if (tag & 0xe) <= 0x4 else ''), + if not args.get('no_truncate') + and (tag & 0xe) <= 0x4 else ''), crc, popc(crc) & 1, '\x1b[m' if color and j >= off else '')) @@ -370,7 +389,6 @@ def show_tree(block_size, data, rev, trunk, weight, *, # found an alt? if alt & 0x8: - weight_ += 1 # follow? if ((id, tag & ~0xf) > (upper-weight_-1, alt & ~0xf) if alt & 0x4 @@ -510,9 +528,11 @@ def show_tree(block_size, data, rev, trunk, weight, *, return ' %s' % ''.join(path) # print header - print('%-8s %*s%-22s %s' % ( + w_width = 2*m.ceil(m.log10(max(1, weight)+1))+1 + print('%-8s %*s%-*s %-22s %s' % ( 'off', 2*depth+1 if args.get('tree') and depth > 0 else 0, '', + w_width, 'ids', 'tag', 'data (truncated)' if not args.get('no_truncate') else '')) @@ -525,29 +545,25 @@ def show_tree(block_size, data, rev, trunk, weight, *, break # show human-readable tag representation - print('%08x:%s %-57s' % ( + print('%08x:%s %*s %-57s' % ( j, treerepr(j) if args.get('tree') else '', + w_width, '%d-%d' % (id-(w-1), id) + if w > 1 else id + if w > 0 else '', '%-22s%s' % ( - tagrepr(tag, id, w, size, j), + tagrepr(tag, id, size, j), ' %s' % next(xxd( data[j+delta:j+delta+min(size, 8)], 8), '') if not args.get('no_truncate') and (tag & 0xe) <= 0x4 else ''))) - if args.get('raw'): - # show on-disk encoding of tags - for o, line in enumerate(xxd(data[j:j+delta])): - print('%8s: %s' % ( - '%04x' % (j + o*16), - line)) - - # show in-device representation, including some extra - # crc/parity info + # show in-device representation if args.get('device'): - print('%8s %s%-47s' % ( + print('%8s %*s%*s %s' % ( '', - lifetimerepr(0) if args.get('lifetimes') else '', + 2*depth+1 if args.get('tree') and depth > 0 else 0, '', + w_width, '', '%-22s%s' % ( '%04x %08x %07x' % (tag, 0xffffffff & id, size), ' %s' % ' '.join( @@ -555,7 +571,15 @@ def show_tree(block_size, data, rev, trunk, weight, *, data[j+delta+i*4:j+delta+min(i*4+4,size)] .ljust(4, b'\0')) for i in range(min(m.ceil(size/4), 3)))[:23] - if (tag & 0xe) <= 0x4 else ''))) + if not args.get('no_truncate') + and (tag & 0xe) <= 0x4 else ''))) + + if args.get('raw'): + # show on-disk encoding of tags + for o, line in enumerate(xxd(data[j:j+delta])): + print('%8s: %s' % ( + '%04x' % (j + o*16), + line)) if (tag & 0xe) <= 0x4: # show on-disk encoding of data @@ -585,16 +609,23 @@ def main(disk, block_size=None, block1=0, block2=None, *, f.seek(0, os.SEEK_END) block_size = f.tell() - # default limit to the block_size - if limit is None: - limit = block_size + # blocks may also encode limits + blocks = [ + block[0] if isinstance(block, tuple) else block + for block in [block1, block2] + if block is not None] + limits = [ + limit if limit is not None + else block[1] if isinstance(block, tuple) + else None + for block in [block1, block2] + if block is not None] # read each block - blocks = [block for block in [block1, block2] if block is not None] datas = [] - for block in blocks: + for block, limit in zip(blocks, limits): f.seek(block * block_size) - datas.append(f.read(block_size)) + datas.append(f.read(limit if limit is not None else block_size)) # first figure out which block as the most recent revision def fetch(data): @@ -607,7 +638,7 @@ def main(disk, block_size=None, block1=0, block2=None, *, weight = 0 weight_ = 0 wastrunk = False - while j_ < limit: + while j_ < len(data): v, tag, id, size, delta = fromtag(data[j_:]) if v != (popc(crc) & 1): break @@ -644,7 +675,7 @@ def main(disk, block_size=None, block1=0, block2=None, *, revs, offs, trunks, weights = [], [], [], [] i = 0 - for block, data in zip(blocks, datas): + for data in datas: rev, off, trunk_, weight = fetch(data) revs.append(rev) offs.append(off) @@ -656,15 +687,17 @@ def main(disk, block_size=None, block1=0, block2=None, *, i = len(revs)-1 # print contents of the winning metadata block - block, data, rev, off, trunk, weight = ( - blocks[i], datas[i], revs[i], offs[i], + block, limit, data, rev, off, trunk, weight = ( + blocks[i], limits[i], datas[i], revs[i], offs[i], trunk if trunk is not None else trunks[i], weights[i]) - print('mdir 0x%x, rev %d, size %d, weight %d%s' % ( - block, rev, off, weight, - ' (was 0x%x, %d, %d, %d)' % ( - blocks[~i], revs[~i], offs[~i], weights[~i]) + print('rbyd 0x%x%s, rev %d, size %d, weight %d%s' % ( + block, '.%x' % limit if limit is not None else '', + rev, off, weight, + ' (was 0x%x%s, %d, %d, %d)' % ( + blocks[~i], '.%x' % limits[~i] if limits[~i] is not None else '', + revs[~i], offs[~i], weights[~i]) if len(blocks) > 1 else '')) if args.get('log'): @@ -692,12 +725,12 @@ if __name__ == "__main__": parser.add_argument( 'block1', nargs='?', - type=lambda x: int(x, 0), + type=blocklim, help="Block address of the first metadata block.") parser.add_argument( 'block2', nargs='?', - type=lambda x: int(x, 0), + type=blocklim, help="Block address of the second metadata block.") parser.add_argument( '-B', '--block-size',