* [Tarantool-patches] [PATCH luajit v3 0/2] debug: generalized extension @ 2023-11-10 20:16 Maksim Kokryashkin via Tarantool-patches 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 1/2] " Maksim Kokryashkin via Tarantool-patches 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 2/2] test: add tests for debugging extensions Maksim Kokryashkin via Tarantool-patches 0 siblings, 2 replies; 7+ messages in thread From: Maksim Kokryashkin via Tarantool-patches @ 2023-11-10 20:16 UTC (permalink / raw) To: tarantool-patches, sergeyb, skaplun, m.kokryashkin, imun Cc: Maksim Kokryashkin Changes in v3: - Fixed comments as per review by Sergey Bronnikov - Tests don't require prove now and depend on python's unittest instead Branch: https://github.com/tarantool/luajit/tree/fckxorg/generalized-debugger Maksim Kokryashkin (1): test: add tests for debugging extensions Maxim Kokryashkin (1): debug: generalized extension .flake8rc | 4 + src/luajit-gdb.py | 885 ------------------ src/{luajit_lldb.py => luajit_dbg.py} | 617 ++++++++---- test/CMakeLists.txt | 3 + .../CMakeLists.txt | 78 ++ .../debug-extension-tests.py | 250 +++++ 6 files changed, 752 insertions(+), 1085 deletions(-) delete mode 100644 src/luajit-gdb.py rename src/{luajit_lldb.py => luajit_dbg.py} (63%) create mode 100644 test/LuaJIT-debug-extensions-tests/CMakeLists.txt create mode 100644 test/LuaJIT-debug-extensions-tests/debug-extension-tests.py -- 2.39.3 (Apple Git-145) ^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH luajit v3 1/2] debug: generalized extension 2023-11-10 20:16 [Tarantool-patches] [PATCH luajit v3 0/2] debug: generalized extension Maksim Kokryashkin via Tarantool-patches @ 2023-11-10 20:16 ` Maksim Kokryashkin via Tarantool-patches 2023-11-22 14:46 ` Sergey Bronnikov via Tarantool-patches 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 2/2] test: add tests for debugging extensions Maksim Kokryashkin via Tarantool-patches 1 sibling, 1 reply; 7+ messages in thread From: Maksim Kokryashkin via Tarantool-patches @ 2023-11-10 20:16 UTC (permalink / raw) To: tarantool-patches, sergeyb, skaplun, m.kokryashkin, imun From: Maxim Kokryashkin <m.kokryashkin@tarantool.org> This patch joins the LLDB and GDB LuaJIT debugging extensions into one, so now the extension logic can be debugger-agnostic. To do that, an adapter class is introduced, and all of the debugger-specific behavior is encapsulated there. The extension auto-detects the debugger it was loaded into and selects the correct low-level logic implementation. --- src/luajit-gdb.py | 885 -------------------------- src/{luajit_lldb.py => luajit_dbg.py} | 617 ++++++++++++------ 2 files changed, 417 insertions(+), 1085 deletions(-) delete mode 100644 src/luajit-gdb.py rename src/{luajit_lldb.py => luajit_dbg.py} (63%) diff --git a/src/luajit-gdb.py b/src/luajit-gdb.py deleted file mode 100644 index 5eaf250f..00000000 --- a/src/luajit-gdb.py +++ /dev/null @@ -1,885 +0,0 @@ -# GDB extension for LuaJIT post-mortem analysis. -# To use, just put 'source <path-to-repo>/src/luajit-gdb.py' in gdb. - -import re -import gdb -import sys - -# make script compatible with the ancient Python {{{ - - -LEGACY = re.match(r'^2\.', sys.version) - -if LEGACY: - CONNECTED = False - int = long - range = xrange - - -# }}} - - -gtype_cache = {} - - -def gtype(typestr): - global gtype_cache - if typestr in gtype_cache: - return gtype_cache[typestr] - - m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr) - - gtype = gdb.lookup_type(typestr) if m is None \ - else gdb.lookup_type(m.group(1)).pointer() - - gtype_cache[typestr] = gtype - return gtype - - -def cast(typestr, val): - return gdb.Value(val).cast(gtype(typestr)) - - -def lookup(symbol): - variable, _ = gdb.lookup_symbol(symbol) - return variable.value() if variable else None - - -def parse_arg(arg): - if not arg: - return None - - ret = gdb.parse_and_eval(arg) - - if not ret: - raise gdb.GdbError('table argument empty') - - return ret - - -def tou64(val): - return cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF - - -def tou32(val): - return cast('uint32_t', val) & 0xFFFFFFFF - - -def i2notu32(val): - return ~int(val) & 0xFFFFFFFF - - -def strx64(val): - return re.sub('L?$', '', - hex(int(cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF))) - - -# Types {{{ - - -LJ_T = { - 'NIL': i2notu32(0), - 'FALSE': i2notu32(1), - 'TRUE': i2notu32(2), - 'LIGHTUD': i2notu32(3), - 'STR': i2notu32(4), - 'UPVAL': i2notu32(5), - 'THREAD': i2notu32(6), - 'PROTO': i2notu32(7), - 'FUNC': i2notu32(8), - 'TRACE': i2notu32(9), - 'CDATA': i2notu32(10), - 'TAB': i2notu32(11), - 'UDATA': i2notu32(12), - 'NUMX': i2notu32(13), -} - - -def typenames(value): - return { - LJ_T[k]: 'LJ_T' + k for k in LJ_T.keys() - }.get(int(value), 'LJ_TINVALID') - - -# }}} - -# Frames {{{ - - -FRAME_TYPE = 0x3 -FRAME_P = 0x4 -FRAME_TYPEP = FRAME_TYPE | FRAME_P - -FRAME = { - 'LUA': 0x0, - 'C': 0x1, - 'CONT': 0x2, - 'VARG': 0x3, - 'LUAP': 0x4, - 'CP': 0x5, - 'PCALL': 0x6, - 'PCALLH': 0x7, -} - - -def frametypes(ft): - return { - FRAME['LUA']: 'L', - FRAME['C']: 'C', - FRAME['CONT']: 'M', - FRAME['VARG']: 'V', - }.get(ft, '?') - - -def bc_a(ins): - return (ins >> 8) & 0xff - - -def frame_ftsz(framelink): - return cast('ptrdiff_t', framelink['ftsz'] if LJ_FR2 - else framelink['fr']['tp']['ftsz']) - - -def frame_pc(framelink): - return cast('BCIns *', frame_ftsz(framelink)) if LJ_FR2 \ - else mref('BCIns *', framelink['fr']['tp']['pcr']) - - -def frame_prevl(framelink): - return framelink - (1 + LJ_FR2 + bc_a(frame_pc(framelink)[-1])) - - -def frame_ispcall(framelink): - return (frame_ftsz(framelink) & FRAME['PCALL']) == FRAME['PCALL'] - - -def frame_sized(framelink): - return (frame_ftsz(framelink) & ~FRAME_TYPEP) - - -def frame_prevd(framelink): - return cast('TValue *', cast('char *', framelink) - frame_sized(framelink)) - - -def frame_type(framelink): - return frame_ftsz(framelink) & FRAME_TYPE - - -def frame_typep(framelink): - return frame_ftsz(framelink) & FRAME_TYPEP - - -def frame_islua(framelink): - return frametypes(int(frame_type(framelink))) == 'L' \ - and int(frame_ftsz(framelink)) > 0 - - -def frame_prev(framelink): - return frame_prevl(framelink) if frame_islua(framelink) \ - else frame_prevd(framelink) - - -def frame_sentinel(L): - return mref('TValue *', L['stack']) + LJ_FR2 - - -# }}} - -# Const {{{ - - -LJ_64 = None -LJ_GC64 = None -LJ_FR2 = None -LJ_DUALNUM = None - -LJ_GCVMASK = ((1 << 47) - 1) -LJ_TISNUM = None -PADDING = None - -# These constants are meaningful only for 'LJ_64' mode. -LJ_LIGHTUD_BITS_SEG = 8 -LJ_LIGHTUD_BITS_LO = 47 - LJ_LIGHTUD_BITS_SEG -LIGHTUD_SEG_MASK = (1 << LJ_LIGHTUD_BITS_SEG) - 1 -LIGHTUD_LO_MASK = (1 << LJ_LIGHTUD_BITS_LO) - 1 - - -# }}} - - -def itype(o): - return cast('uint32_t', o['it64'] >> 47) if LJ_GC64 else o['it'] - - -def mref(typename, obj): - return cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32']) - - -def gcref(obj): - return cast('GCobj *', obj['gcptr64'] if LJ_GC64 - else cast('uintptr_t', obj['gcptr32'])) - - -def gcval(obj): - return cast('GCobj *', obj['gcptr64'] & LJ_GCVMASK if LJ_GC64 - else cast('uintptr_t', obj['gcptr32'])) - - -def gcnext(obj): - return gcref(obj)['gch']['nextgc'] - - -def L(L=None): - # lookup a symbol for the main coroutine considering the host app - # XXX Fragile: though the loop initialization looks like a crap but it - # respects both Python 2 and Python 3. - for lstate in [L] + list(map(lambda main: lookup(main), ( - # LuaJIT main coro (see luajit/src/luajit.c) - 'globalL', - # Tarantool main coro (see tarantool/src/lua/init.h) - 'tarantool_L', - # TODO: Add more - ))): - if lstate: - return cast('lua_State *', lstate) - - -def G(L): - return mref('global_State *', L['glref']) - - -def J(g): - typeGG = gtype('GG_State') - - return cast('jit_State *', int(cast('char *', g)) - - int(typeGG['g'].bitpos / 8) - + int(typeGG['J'].bitpos / 8)) - - -def vm_state(g): - return { - i2notu32(0): 'INTERP', - i2notu32(1): 'LFUNC', - i2notu32(2): 'FFUNC', - i2notu32(3): 'CFUNC', - i2notu32(4): 'GC', - i2notu32(5): 'EXIT', - i2notu32(6): 'RECORD', - i2notu32(7): 'OPT', - i2notu32(8): 'ASM', - }.get(int(tou32(g['vmstate'])), 'TRACE') - - -def gc_state(g): - return { - 0: 'PAUSE', - 1: 'PROPAGATE', - 2: 'ATOMIC', - 3: 'SWEEPSTRING', - 4: 'SWEEP', - 5: 'FINALIZE', - 6: 'LAST', - }.get(int(g['gc']['state']), 'INVALID') - - -def jit_state(g): - return { - 0: 'IDLE', - 0x10: 'ACTIVE', - 0x11: 'RECORD', - 0x12: 'START', - 0x13: 'END', - 0x14: 'ASM', - 0x15: 'ERR', - }.get(int(J(g)['state']), 'INVALID') - - -def tvisint(o): - return LJ_DUALNUM and itype(o) == LJ_TISNUM - - -def tvisnumber(o): - return itype(o) <= LJ_TISNUM - - -def tvislightud(o): - if LJ_64 and not LJ_GC64: - return (cast('int32_t', itype(o)) >> 15) == -2 - else: - return itype(o) == LJ_T['LIGHTUD'] - - -def strdata(obj): - # String is printed with pointer to it, thanks to gdb. Just strip it. - try: - return str(cast('char *', cast('GCstr *', obj) + 1))[len(PADDING):] - except UnicodeEncodeError: - return "<luajit-gdb: error occured while rendering non-ascii slot>" - - -def itypemap(o): - if LJ_64 and not LJ_GC64: - return LJ_T['NUMX'] if tvisnumber(o) \ - else LJ_T['LIGHTUD'] if tvislightud(o) \ - else itype(o) - else: - return LJ_T['NUMX'] if tvisnumber(o) else itype(o) - - -def funcproto(func): - assert func['ffid'] == 0 - - return cast('GCproto *', - mref('char *', func['pc']) - gdb.lookup_type('GCproto').sizeof) - - -def gclistlen(root, end=0x0): - count = 0 - while (gcref(root) != end): - count += 1 - root = gcnext(root) - return count - - -def gcringlen(root): - if not gcref(root): - return 0 - elif gcref(root) == gcref(gcnext(root)): - return 1 - else: - return 1 + gclistlen(gcnext(root), gcref(root)) - - -gclen = { - 'root': gclistlen, - 'gray': gclistlen, - 'grayagain': gclistlen, - 'weak': gclistlen, - # XXX: gc.mmudata is a ring-list. - 'mmudata': gcringlen, -} - - -# The generator that implements frame iterator. -# Every frame is represented as a tuple of framelink and frametop. -def frames(L): - frametop = L['top'] - framelink = L['base'] - 1 - framelink_sentinel = frame_sentinel(L) - while True: - yield framelink, frametop - frametop = framelink - (1 + LJ_FR2) - if framelink <= framelink_sentinel: - break - framelink = frame_prev(framelink) - - -def lightudV(tv): - if LJ_64: - u = int(tv['u64']) - # lightudseg macro expanded. - seg = (u >> LJ_LIGHTUD_BITS_LO) & LIGHTUD_SEG_MASK - segmap = mref('uint32_t *', G(L(None))['gc']['lightudseg']) - # lightudlo macro expanded. - return (int(segmap[seg]) << 32) | (u & LIGHTUD_LO_MASK) - else: - return gcval(tv['gcr']) - - -# Dumpers {{{ - - -def dump_lj_tnil(tv): - return 'nil' - - -def dump_lj_tfalse(tv): - return 'false' - - -def dump_lj_ttrue(tv): - return 'true' - - -def dump_lj_tlightud(tv): - return 'light userdata @ {}'.format(strx64(lightudV(tv))) - - -def dump_lj_tstr(tv): - return 'string {body} @ {address}'.format( - body=strdata(gcval(tv['gcr'])), - address=strx64(gcval(tv['gcr'])) - ) - - -def dump_lj_tupval(tv): - return 'upvalue @ {}'.format(strx64(gcval(tv['gcr']))) - - -def dump_lj_tthread(tv): - return 'thread @ {}'.format(strx64(gcval(tv['gcr']))) - - -def dump_lj_tproto(tv): - return 'proto @ {}'.format(strx64(gcval(tv['gcr']))) - - -def dump_lj_tfunc(tv): - func = cast('struct GCfuncC *', gcval(tv['gcr'])) - ffid = func['ffid'] - - if ffid == 0: - pt = funcproto(func) - return 'Lua function @ {addr}, {nups} upvalues, {chunk}:{line}'.format( - addr=strx64(func), - nups=int(func['nupvalues']), - chunk=strdata(cast('GCstr *', gcval(pt['chunkname']))), - line=pt['firstline'] - ) - elif ffid == 1: - return 'C function @ {}'.format(strx64(func['f'])) - else: - return 'fast function #{}'.format(int(ffid)) - - -def dump_lj_ttrace(tv): - trace = cast('struct GCtrace *', gcval(tv['gcr'])) - return 'trace {traceno} @ {addr}'.format( - traceno=strx64(trace['traceno']), - addr=strx64(trace) - ) - - -def dump_lj_tcdata(tv): - return 'cdata @ {}'.format(strx64(gcval(tv['gcr']))) - - -def dump_lj_ttab(tv): - table = cast('GCtab *', gcval(tv['gcr'])) - return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format( - gcr=strx64(table), - asize=table['asize'], - hmask=strx64(table['hmask']), - ) - - -def dump_lj_tudata(tv): - return 'userdata @ {}'.format(strx64(gcval(tv['gcr']))) - - -def dump_lj_tnumx(tv): - if tvisint(tv): - return 'integer {}'.format(cast('int32_t', tv['i'])) - else: - return 'number {}'.format(cast('double', tv['n'])) - - -def dump_lj_invalid(tv): - return 'not valid type @ {}'.format(strx64(gcval(tv['gcr']))) - - -# }}} - - -dumpers = { - 'LJ_TNIL': dump_lj_tnil, - 'LJ_TFALSE': dump_lj_tfalse, - 'LJ_TTRUE': dump_lj_ttrue, - 'LJ_TLIGHTUD': dump_lj_tlightud, - 'LJ_TSTR': dump_lj_tstr, - 'LJ_TUPVAL': dump_lj_tupval, - 'LJ_TTHREAD': dump_lj_tthread, - 'LJ_TPROTO': dump_lj_tproto, - 'LJ_TFUNC': dump_lj_tfunc, - 'LJ_TTRACE': dump_lj_ttrace, - 'LJ_TCDATA': dump_lj_tcdata, - 'LJ_TTAB': dump_lj_ttab, - 'LJ_TUDATA': dump_lj_tudata, - 'LJ_TNUMX': dump_lj_tnumx, -} - - -def dump_tvalue(tvalue): - return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue) - - -def dump_framelink_slot_address(fr): - return '{}:{}'.format(fr - 1, fr) if LJ_FR2 \ - else '{}'.format(fr) + PADDING - - -def dump_framelink(L, fr): - if fr == frame_sentinel(L): - return '{addr} [S ] FRAME: dummy L'.format( - addr=dump_framelink_slot_address(fr), - ) - return '{addr} [ ] FRAME: [{pp}] delta={d}, {f}'.format( - addr=dump_framelink_slot_address(fr), - pp='PP' if frame_ispcall(fr) else '{frname}{p}'.format( - frname=frametypes(int(frame_type(fr))), - p='P' if frame_typep(fr) & FRAME_P else '' - ), - d=cast('TValue *', fr) - cast('TValue *', frame_prev(fr)), - f=dump_lj_tfunc(fr - LJ_FR2), - ) - - -def dump_stack_slot(L, slot, base=None, top=None): - base = base or L['base'] - top = top or L['top'] - - return '{addr}{padding} [ {B}{T}{M}] VALUE: {value}'.format( - addr=strx64(slot), - padding=PADDING, - B='B' if slot == base else ' ', - T='T' if slot == top else ' ', - M='M' if slot == mref('TValue *', L['maxstack']) else ' ', - value=dump_tvalue(slot), - ) - - -def dump_stack(L, base=None, top=None): - base = base or L['base'] - top = top or L['top'] - stack = mref('TValue *', L['stack']) - maxstack = mref('TValue *', L['maxstack']) - red = 5 + 2 * LJ_FR2 - - dump = [ - '{padding} Red zone: {nredslots: >2} slots {padding}'.format( - padding='-' * len(PADDING), - nredslots=red, - ), - ] - dump.extend([ - dump_stack_slot(L, maxstack + offset, base, top) - for offset in range(red, 0, -1) # noqa: E131 - ]) - dump.extend([ - '{padding} Stack: {nstackslots: >5} slots {padding}'.format( - padding='-' * len(PADDING), - nstackslots=int((tou64(maxstack) - tou64(stack)) >> 3), - ), - dump_stack_slot(L, maxstack, base, top), - '{start}:{end} [ ] {nfreeslots} slots: Free stack slots'.format( - start=strx64(top + 1), - end=strx64(maxstack - 1), - nfreeslots=int((tou64(maxstack) - tou64(top) - 8) >> 3), - ), - ]) - - for framelink, frametop in frames(L): - # Dump all data slots in the (framelink, top) interval. - dump.extend([ - dump_stack_slot(L, framelink + offset, base, top) - for offset in range(frametop - framelink, 0, -1) # noqa: E131 - ]) - # Dump frame slot (2 slots in case of GC64). - dump.append(dump_framelink(L, framelink)) - - return '\n'.join(dump) - - -def dump_gc(g): - gc = g['gc'] - stats = ['{key}: {value}'.format(key=f, value=gc[f]) for f in ( - 'total', 'threshold', 'debt', 'estimate', 'stepmul', 'pause' - )] - - stats += ['sweepstr: {sweepstr}/{strmask}'.format( - sweepstr=gc['sweepstr'], - # String hash mask (size of hash table - 1). - strmask=g['strmask'] + 1, - )] - - stats += ['{key}: {number} objects'.format( - key=stat, - number=handler(gc[stat]) - ) for stat, handler in gclen.items()] - - return '\n'.join(map(lambda s: '\t' + s, stats)) - - -class LJBase(gdb.Command): - - def __init__(self, name): - # XXX Fragile: though the command initialization looks like a crap but - # it respects both Python 2 and Python 3. - gdb.Command.__init__(self, name, gdb.COMMAND_DATA) - gdb.write('{} command initialized\n'.format(name)) - - -class LJDumpArch(LJBase): - ''' -lj-arch - -The command requires no args and dumps values of LJ_64 and LJ_GC64 -compile-time flags. These values define the sizes of host and GC -pointers respectively. - ''' - - def invoke(self, arg, from_tty): - gdb.write( - 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}\n' - .format( - LJ_64=LJ_64, - LJ_GC64=LJ_GC64, - LJ_DUALNUM=LJ_DUALNUM - ) - ) - - -class LJDumpTValue(LJBase): - ''' -lj-tv <TValue *> - -The command receives a pointer to <tv> (TValue address) and dumps -the type and some info related to it. - -* LJ_TNIL: nil -* LJ_TFALSE: false -* LJ_TTRUE: true -* LJ_TLIGHTUD: light userdata @ <gcr> -* LJ_TSTR: string <string payload> @ <gcr> -* LJ_TUPVAL: upvalue @ <gcr> -* LJ_TTHREAD: thread @ <gcr> -* LJ_TPROTO: proto @ <gcr> -* LJ_TFUNC: <LFUNC|CFUNC|FFUNC> - <LFUNC>: Lua function @ <gcr>, <nupvals> upvalues, <chunk:line> - <CFUNC>: C function <mcode address> - <FFUNC>: fast function #<ffid> -* LJ_TTRACE: trace <traceno> @ <gcr> -* LJ_TCDATA: cdata @ <gcr> -* LJ_TTAB: table @ <gcr> (asize: <asize>, hmask: <hmask>) -* LJ_TUDATA: userdata @ <gcr> -* LJ_TNUMX: number <numeric payload> - -Whether the type of the given address differs from the listed above, then -error message occurs. - ''' - - def invoke(self, arg, from_tty): - tv = cast('TValue *', parse_arg(arg)) - gdb.write('{}\n'.format(dump_tvalue(tv))) - - -class LJDumpString(LJBase): - ''' -lj-str <GCstr *> - -The command receives a <gcr> of the corresponding GCstr object and dumps -the payload, size in bytes and hash. - -*Caveat*: Since Python 2 provides no native Unicode support, the payload -is replaced with the corresponding error when decoding fails. - ''' - - def invoke(self, arg, from_tty): - string = cast('GCstr *', parse_arg(arg)) - gdb.write("String: {body} [{len} bytes] with hash {hash}\n".format( - body=strdata(string), - hash=strx64(string['hash']), - len=string['len'], - )) - - -class LJDumpTable(LJBase): - ''' -lj-tab <GCtab *> - -The command receives a GCtab adress and dumps the table contents: -* Metatable address whether the one is set -* Array part <asize> slots: - <aslot ptr>: [<index>]: <tv> -* Hash part <hsize> nodes: - <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr> - ''' - - def invoke(self, arg, from_tty): - t = cast('GCtab *', parse_arg(arg)) - array = mref('TValue *', t['array']) - nodes = mref('struct Node *', t['node']) - mt = gcval(t['metatable']) - capacity = { - 'apart': int(t['asize']), - 'hpart': int(t['hmask'] + 1) if t['hmask'] > 0 else 0 - } - - if mt != 0: - gdb.write('Metatable detected: {}\n'.format(strx64(mt))) - - gdb.write('Array part: {} slots\n'.format(capacity['apart'])) - for i in range(capacity['apart']): - slot = array + i - gdb.write('{ptr}: [{index}]: {value}\n'.format( - ptr=slot, - index=i, - value=dump_tvalue(slot) - )) - - gdb.write('Hash part: {} nodes\n'.format(capacity['hpart'])) - # See hmask comment in lj_obj.h - for i in range(capacity['hpart']): - node = nodes + i - gdb.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}\n'.format( - ptr=node, - key=dump_tvalue(node['key']), - val=dump_tvalue(node['val']), - n=mref('struct Node *', node['next']) - )) - - -class LJDumpStack(LJBase): - ''' -lj-stack [<lua_State *>] - -The command receives a lua_State address and dumps the given Lua -coroutine guest stack: - -<slot ptr> [<slot attributes>] <VALUE|FRAME> - -* <slot ptr>: guest stack slot address -* <slot attributes>: - - S: Bottom of the stack (the slot L->stack points to) - - B: Base of the current guest frame (the slot L->base points to) - - T: Top of the current guest frame (the slot L->top points to) - - M: Last slot of the stack (the slot L->maxstack points to) -* <VALUE>: see help lj-tv for more info -* <FRAME>: framelink slot differs from the value slot: it contains info - related to the function being executed within this guest frame, its - type and link to the parent guest frame - [<frame type>] delta=<slots in frame>, <lj-tv for LJ_TFUNC slot> - - <frame type>: - + L: VM performs a call as a result of bytecode execution - + C: VM performs a call as a result of lj_vm_call - + M: VM performs a call to a metamethod as a result of bytecode - execution - + V: Variable-length frame for storing arguments of a variadic - function - + CP: Protected C frame - + PP: VM performs a call as a result of executinig pcall or xpcall - -If L is ommited the main coroutine is used. - ''' - - def invoke(self, arg, from_tty): - gdb.write('{}\n'.format(dump_stack(L(parse_arg(arg))))) - - -class LJState(LJBase): - ''' -lj-state -The command requires no args and dumps current VM and GC states -* VM state: <INTERP|C|GC|EXIT|RECORD|OPT|ASM|TRACE> -* GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST> -* JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR> - ''' - - def invoke(self, arg, from_tty): - g = G(L(None)) - gdb.write('{}\n'.format('\n'.join( - map(lambda t: '{} state: {}'.format(*t), { - 'VM': vm_state(g), - 'GC': gc_state(g), - 'JIT': jit_state(g), - }.items()) - ))) - - -class LJGC(LJBase): - ''' -lj-gc - -The command requires no args and dumps current GC stats: -* total: <total number of allocated bytes in GC area> -* threshold: <limit when gc step is triggered> -* debt: <how much GC is behind schedule> -* estimate: <estimate of memory actually in use> -* stepmul: <incremental GC step granularity> -* pause: <pause between successive GC cycles> -* sweepstr: <sweep position in string table> -* root: <number of all collectable objects> -* gray: <number of gray objects> -* grayagain: <number of objects for atomic traversal> -* weak: <number of weak tables (to be cleared)> -* mmudata: <number of udata|cdata to be finalized> - ''' - - def invoke(self, arg, from_tty): - g = G(L(None)) - gdb.write('GC stats: {state}\n{stats}\n'.format( - state=gc_state(g), - stats=dump_gc(g) - )) - - -def init(commands): - global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, LJ_TISNUM, PADDING - - # XXX Fragile: though connecting the callback looks like a crap but it - # respects both Python 2 and Python 3 (see #4828). - def connect(callback): - if LEGACY: - global CONNECTED - CONNECTED = True - gdb.events.new_objfile.connect(callback) - - # XXX Fragile: though disconnecting the callback looks like a crap but it - # respects both Python 2 and Python 3 (see #4828). - def disconnect(callback): - if LEGACY: - global CONNECTED - if not CONNECTED: - return - CONNECTED = False - gdb.events.new_objfile.disconnect(callback) - - try: - # Try to remove the callback at first to not append duplicates to - # gdb.events.new_objfile internal list. - disconnect(load) - except Exception: - # Callback is not connected. - pass - - try: - # Detect whether libluajit objfile is loaded. - gdb.parse_and_eval('luaJIT_setmode') - except Exception: - gdb.write('luajit-gdb.py initialization is postponed ' - 'until libluajit objfile is loaded\n') - # Add a callback to be executed when the next objfile is loaded. - connect(load) - return - - try: - LJ_64 = str(gdb.parse_and_eval('IRT_PTR')) == 'IRT_P64' - LJ_FR2 = LJ_GC64 = str(gdb.parse_and_eval('IRT_PGC')) == 'IRT_P64' - LJ_DUALNUM = gdb.lookup_global_symbol('lj_lib_checknumber') is not None - except Exception: - gdb.write('luajit-gdb.py failed to load: ' - 'no debugging symbols found for libluajit\n') - return - - for name, command in commands.items(): - command(name) - - PADDING = ' ' * len(':' + hex((1 << (47 if LJ_GC64 else 32)) - 1)) - LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] - - gdb.write('luajit-gdb.py is successfully loaded\n') - - -def load(event=None): - init({ - 'lj-arch': LJDumpArch, - 'lj-tv': LJDumpTValue, - 'lj-str': LJDumpString, - 'lj-tab': LJDumpTable, - 'lj-stack': LJDumpStack, - 'lj-state': LJState, - 'lj-gc': LJGC, - }) - - -load(None) diff --git a/src/luajit_lldb.py b/src/luajit_dbg.py similarity index 63% rename from src/luajit_lldb.py rename to src/luajit_dbg.py index ef0986cc..cb46b473 100644 --- a/src/luajit_lldb.py +++ b/src/luajit_dbg.py @@ -1,10 +1,231 @@ -# LLDB extension for LuaJIT post-mortem analysis. -# To use, just put 'command script import <path-to-repo>/src/luajit_lldb.py' -# in lldb. +# Debug extension for LuaJIT post-mortem analysis. +# To use in LLDB: 'command script import <path-to-repo>/src/luajit_dbg.py' +# To use in GDB: 'source <path-to-repo>/src/luajit_dbg.py' import abc import re -import lldb +import sys +import types + +from importlib import import_module + +# make script compatible with the ancient Python {{{ + + +LEGACY = re.match(r'^2\.', sys.version) + +if LEGACY: + CONNECTED = False + int = long + range = xrange + + +def is_integer_type(val): + return isinstance(val, int) or (LEGACY and isinstance(val, types.IntType)) + + +# }}} + + +class Debugger(object): + def __init__(self): + self.GDB = False + self.LLDB = False + + debuggers = { + 'gdb': lambda lib: True, + 'lldb': lambda lib: lib.debugger is not None, + } + for name, healthcheck in debuggers.items(): + lib = None + try: + lib = import_module(name) + except ImportError: + continue + + if healthcheck(lib): + setattr(self, name.upper(), True) + globals()[name] = lib + self.name = name + + assert self.LLDB != self.GDB + + def setup_target(self, debugger): + global target + if self.LLDB: + target = debugger.GetSelectedTarget() + + def write(self, msg): + if self.LLDB: + print(msg) + else: + gdb.write(msg + '\n') + + def cmd_init(self, cmd_cls, debugger=None): + if self.LLDB: + debugger.HandleCommand( + 'command script add --overwrite --class ' + 'luajit_dbg.{cls} {cmd}' + .format( + cls=cmd_cls.__name__, + cmd=cmd_cls.command, + ) + ) + else: + cmd_cls() + + def event_connect(self, callback): + if not self.LLDB: + # XXX Fragile: though connecting the callback looks like a crap but + # it respects both Python 2 and Python 3 (see #4828). + if LEGACY: + global CONNECTED + CONNECTED = True + gdb.events.new_objfile.connect(callback) + + def event_disconnect(self, callback): + if not self.LLDB: + # XXX Fragile: though disconnecting the callback looks like a crap + # but it respects both Python 2 and Python 3 (see #4828). + if LEGACY: + global CONNECTED + if not CONNECTED: + return + CONNECTED = False + gdb.events.new_objfile.disconnect(callback) + + def lookup_variable(self, name): + if self.LLDB: + return target.FindFirstGlobalVariable(name) + else: + variable, _ = gdb.lookup_symbol(name) + return variable.value() if variable else None + + def lookup_symbol(self, sym): + if self.LLDB: + return target.modules[0].FindSymbol(sym) + else: + return gdb.lookup_global_symbol(sym) + + def to_unsigned(self, val): + return val.unsigned if self.LLDB else int(val) + + def to_signed(self, val): + return val.signed if self.LLDB else int(val) + + def to_str(self, val): + return val.value if self.LLDB else str(val) + + def find_type(self, typename): + if self.LLDB: + return target.FindFirstType(typename) + else: + return gdb.lookup_type(typename) + + def type_to_pointer_type(self, tp): + if self.LLDB: + return tp.GetPointerType() + else: + return tp.pointer() + + def cast_impl(self, value, t, pointer_type): + if self.LLDB: + if is_integer_type(value): + # Integer casts require some black magic + # for lldb to behave properly. + if pointer_type: + return target.CreateValueFromAddress( + 'value', + lldb.SBAddress(value, target), + t.GetPointeeType(), + ).address_of + else: + return target.CreateValueFromData( + name='value', + data=lldb.SBData.CreateDataFromInt(value, size=8), + type=t, + ) + else: + return value.Cast(t) + else: + return gdb.Value(value).cast(t) + + def dereference(self, val): + if self.LLDB: + return val.Dereference() + else: + return val.dereference() + + def eval(self, expression): + if self.LLDB: + process = target.GetProcess() + thread = process.GetSelectedThread() + frame = thread.GetSelectedFrame() + + if not expression: + return None + + return frame.EvaluateExpression(expression) + else: + return gdb.parse_and_eval(expression) + + def type_sizeof_impl(self, tp): + if self.LLDB: + return tp.GetByteSize() + else: + return tp.sizeof + + def summary(self, val): + if self.LLDB: + return val.summary + else: + return str(val)[len(PADDING):].strip() + + def type_member(self, type_obj, name): + if self.LLDB: + return next((x for x in type_obj.members if x.name == name), None) + else: + return type_obj[name] + + def type_member_offset(self, member): + if self.LLDB: + return member.GetOffsetInBytes() + else: + return member.bitpos / 8 + + def get_member(self, value, member_name): + if self.LLDB: + return value.GetChildMemberWithName(member_name) + else: + return value[member_name] + + def address_of(self, value): + if self.LLDB: + return value.address_of + else: + return value.address + + def arch_init(self): + global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, PADDING, LJ_TISNUM, target + if self.LLDB: + irtype_enum = dbg.find_type('IRType').enum_members + for member in irtype_enum: + if member.name == 'IRT_PTR': + LJ_64 = dbg.to_unsigned(member) & 0x1f == IRT_P64 + if member.name == 'IRT_PGC': + LJ_GC64 = dbg.to_unsigned(member) & 0x1f == IRT_P64 + else: + LJ_64 = str(dbg.eval('IRT_PTR')) == 'IRT_P64' + LJ_GC64 = str(dbg.eval('IRT_PGC')) == 'IRT_P64' + + LJ_FR2 = LJ_GC64 + LJ_DUALNUM = dbg.lookup_symbol('lj_lib_checknumber') is not None + # Two extra characters are required to fit in the `0x` part. + PADDING = ' ' * len(strx64(L())) + LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] + + +dbg = Debugger() LJ_64 = None LJ_GC64 = None @@ -17,68 +238,73 @@ IRT_P64 = 9 LJ_GCVMASK = ((1 << 47) - 1) LJ_TISNUM = None -# Debugger specific {{{ - - # Global target = None -class Ptr: +class Ptr(object): def __init__(self, value, normal_type): self.value = value self.normal_type = normal_type @property def __deref(self): - return self.normal_type(self.value.Dereference()) + return self.normal_type(dbg.dereference(self.value)) def __add__(self, other): - assert isinstance(other, int) + assert is_integer_type(other) return self.__class__( cast( self.normal_type.__name__ + ' *', cast( 'uintptr_t', - self.value.unsigned + other * self.value.deref.size, + dbg.to_unsigned(self.value) + other * sizeof( + self.normal_type.__name__ + ), ), ), ) def __sub__(self, other): - assert isinstance(other, int) or isinstance(other, Ptr) - if isinstance(other, int): + assert is_integer_type(other) or isinstance(other, Ptr) + if is_integer_type(other): return self.__add__(-other) else: - return int((self.value.unsigned - other.value.unsigned) - / sizeof(self.normal_type.__name__)) + return int( + ( + dbg.to_unsigned(self.value) - dbg.to_unsigned(other.value) + ) / sizeof(self.normal_type.__name__) + ) def __eq__(self, other): - assert isinstance(other, Ptr) or isinstance(other, int) and other >= 0 + assert isinstance(other, Ptr) or is_integer_type(other) if isinstance(other, Ptr): - return self.value.unsigned == other.value.unsigned + return dbg.to_unsigned(self.value) == dbg.to_unsigned(other.value) else: - return self.value.unsigned == other + return dbg.to_unsigned(self.value) == other def __ne__(self, other): return not self == other def __gt__(self, other): assert isinstance(other, Ptr) - return self.value.unsigned > other.value.unsigned + return dbg.to_unsigned(self.value) > dbg.to_unsigned(other.value) def __ge__(self, other): assert isinstance(other, Ptr) - return self.value.unsigned >= other.value.unsigned + return dbg.to_unsigned(self.value) >= dbg.to_unsigned(other.value) def __bool__(self): - return self.value.unsigned != 0 + return dbg.to_unsigned(self.value) != 0 def __int__(self): - return self.value.unsigned + return dbg.to_unsigned(self.value) + + def __long__(self): + return dbg.to_unsigned(self.value) def __str__(self): - return self.value.value + return dbg.to_str(self.value) def __getattr__(self, name): if name != '__deref': @@ -86,53 +312,26 @@ class Ptr: return self.__deref -class MetaStruct(type): - def __init__(cls, name, bases, nmspc): - super(MetaStruct, cls).__init__(name, bases, nmspc) - - def make_general(field, tp): - builtin = { - 'uint': 'unsigned', - 'int': 'signed', - 'string': 'value', - } - if tp in builtin.keys(): - return lambda self: getattr(self[field], builtin[tp]) - else: - return lambda self: globals()[tp](self[field]) - - if hasattr(cls, 'metainfo'): - for field in cls.metainfo: - if not isinstance(field[0], str): - setattr(cls, field[1], field[0]) - else: - setattr( - cls, - field[1], - property(make_general(field[1], field[0])), - ) - - -class Struct(metaclass=MetaStruct): +class Struct(object): def __init__(self, value): self.value = value def __getitem__(self, name): - return self.value.GetChildMemberWithName(name) + return dbg.get_member(self.value, name) @property def addr(self): - return self.value.address_of + return dbg.address_of(self.value) c_structs = { 'MRef': [ - (property(lambda self: self['ptr64'].unsigned if LJ_GC64 - else self['ptr32'].unsigned), 'ptr') + (property(lambda self: dbg.to_unsigned(self['ptr64']) if LJ_GC64 + else dbg.to_unsigned(self['ptr32'])), 'ptr') ], 'GCRef': [ - (property(lambda self: self['gcptr64'].unsigned if LJ_GC64 - else self['gcptr32'].unsigned), 'gcptr') + (property(lambda self: dbg.to_unsigned(self['gcptr64']) if LJ_GC64 + else dbg.to_unsigned(self['gcptr32'])), 'gcptr') ], 'TValue': [ ('GCRef', 'gcr'), @@ -141,8 +340,12 @@ c_structs = { ('int', 'it64'), ('string', 'n'), (property(lambda self: FR(self['fr']) if not LJ_GC64 else None), 'fr'), - (property(lambda self: self['ftsz'].signed if LJ_GC64 else None), - 'ftsz') + ( + property( + lambda self: dbg.to_signed(self['ftsz']) if LJ_GC64 else None + ), + 'ftsz' + ) ], 'GCState': [ ('GCRef', 'root'), @@ -216,26 +419,51 @@ c_structs = { ('TValue', 'val'), ('MRef', 'next') ], - 'BCIns': [] + 'BCIns': [], } -for cls in c_structs.keys(): - globals()[cls] = type(cls, (Struct, ), {'metainfo': c_structs[cls]}) +def make_property_from_metadata(field, tp): + builtin = { + 'uint': dbg.to_unsigned, + 'int': dbg.to_signed, + 'string': dbg.to_str, + } + if tp in builtin.keys(): + return lambda self: builtin[tp](self[field]) + else: + return lambda self: globals()[tp](self[field]) + + +for cls, metainfo in c_structs.items(): + cls_dict = {} + for field in metainfo: + if not isinstance(field[0], str): + cls_dict[field[1]] = field[0] + else: + cls_dict[field[1]] = property( + make_property_from_metadata(field[1], field[0]) + ) + globals()[cls] = type(cls, (Struct, ), cls_dict) for cls in Struct.__subclasses__(): ptr_name = cls.__name__ + 'Ptr' + def make_init(cls): + return lambda self, value: super(type(self), self).__init__(value, cls) + globals()[ptr_name] = type(ptr_name, (Ptr,), { - '__init__': - lambda self, value: super(type(self), self).__init__(value, cls) + '__init__': make_init(cls) }) -class Command(object): - def __init__(self, debugger, unused): - pass +class Command(object if dbg.LLDB else gdb.Command): + def __init__(self, debugger=None, unused=None): + if dbg.GDB: + # XXX Fragile: though initialization looks like a crap but it + # respects both Python 2 and Python 3 (see #4828). + gdb.Command.__init__(self, self.command, gdb.COMMAND_DATA) def get_short_help(self): return self.__doc__.splitlines()[0] @@ -245,21 +473,15 @@ class Command(object): def __call__(self, debugger, command, exe_ctx, result): try: - self.execute(debugger, command, result) + self.execute(command) except Exception as e: msg = 'Failed to execute command `{}`: {}'.format(self.command, e) result.SetError(msg) def parse(self, command): - process = target.GetProcess() - thread = process.GetSelectedThread() - frame = thread.GetSelectedFrame() - if not command: return None - - ret = frame.EvaluateExpression(command) - return ret + return dbg.to_unsigned(dbg.eval(command)) @abc.abstractproperty def command(self): @@ -270,7 +492,7 @@ class Command(object): """ @abc.abstractmethod - def execute(self, debugger, args, result): + def execute(self, args): """Implementation of the command. Subclasses override this method to implement the logic of a given command, e.g. printing a stacktrace. The command output should be @@ -278,6 +500,11 @@ class Command(object): properly routed to LLDB frontend. Any unhandled exception will be automatically transformed into proper errors. """ + def invoke(self, arg, from_tty): + try: + self.execute(arg) + except Exception as e: + dbg.write(e) def cast(typename, value): @@ -299,75 +526,38 @@ def cast(typename, value): name = name[:-1].strip() pointer_type = True - # Get the lldb type representation. - t = target.FindFirstType(name) + # Get the inferior type representation. + t = dbg.find_type(name) if pointer_type: - t = t.GetPointerType() - - if isinstance(value, int): - # Integer casts require some black magic for lldb to behave properly. - if pointer_type: - casted = target.CreateValueFromAddress( - 'value', - lldb.SBAddress(value, target), - t.GetPointeeType(), - ).address_of - else: - casted = target.CreateValueFromData( - name='value', - data=lldb.SBData.CreateDataFromInt(value, size=8), - type=t, - ) - else: - casted = value.Cast(t) + t = dbg.type_to_pointer_type(t) + + casted = dbg.cast_impl(value, t, pointer_type) if isinstance(typename, type): - # Wrap lldb object, if possible + # Wrap inferior object, if possible return typename(casted) else: return casted -def lookup_global(name): - return target.FindFirstGlobalVariable(name) - - -def type_member(type_obj, name): - return next((x for x in type_obj.members if x.name == name), None) - - -def find_type(typename): - return target.FindFirstType(typename) - - def offsetof(typename, membername): - type_obj = find_type(typename) - member = type_member(type_obj, membername) + type_obj = dbg.find_type(typename) + member = dbg.type_member(type_obj, membername) assert member is not None - return member.GetOffsetInBytes() + return dbg.type_member_offset(member) def sizeof(typename): - type_obj = find_type(typename) - return type_obj.GetByteSize() + type_obj = dbg.find_type(typename) + return dbg.type_sizeof_impl(type_obj) def vtou64(value): - return value.unsigned & 0xFFFFFFFFFFFFFFFF + return dbg.to_unsigned(value) & 0xFFFFFFFFFFFFFFFF def vtoi(value): - return value.signed - - -def dbg_eval(expr): - process = target.GetProcess() - thread = process.GetSelectedThread() - frame = thread.GetSelectedFrame() - return frame.EvaluateExpression(expr) - - -# }}} Debugger specific + return dbg.to_signed(value) def gcval(obj): @@ -393,7 +583,7 @@ def gclistlen(root, end=0x0): def gcringlen(root): - if not gcref(root): + if gcref(root) == 0: return 0 elif gcref(root) == gcref(gcnext(root)): return 1 @@ -439,7 +629,7 @@ def J(g): J_offset = offsetof('GG_State', 'J') return cast( jit_StatePtr, - vtou64(cast('char *', g)) - g_offset + J_offset, + int(vtou64(cast('char *', g)) - g_offset + J_offset), ) @@ -451,7 +641,7 @@ def L(L=None): # lookup a symbol for the main coroutine considering the host app # XXX Fragile: though the loop initialization looks like a crap but it # respects both Python 2 and Python 3. - for lstate in [L] + list(map(lambda main: lookup_global(main), ( + for lstate in [L] + list(map(lambda main: dbg.lookup_variable(main), ( # LuaJIT main coro (see luajit/src/luajit.c) 'globalL', # Tarantool main coro (see tarantool/src/lua/init.h) @@ -459,7 +649,7 @@ def L(L=None): # TODO: Add more ))): if lstate: - return lua_State(lstate) + return lua_StatePtr(lstate) def tou32(val): @@ -523,9 +713,9 @@ def funcproto(func): def strdata(obj): try: ptr = cast('char *', obj + 1) - return ptr.summary + return dbg.summary(ptr) except UnicodeEncodeError: - return "<luajit-lldb: error occured while rendering non-ascii slot>" + return "<luajit_dbg: error occured while rendering non-ascii slot>" def itype(o): @@ -730,12 +920,12 @@ def frame_pc(framelink): def frame_prevl(framelink): - # We are evaluating the `frame_pc(framelink)[-1])` with lldb's + # We are evaluating the `frame_pc(framelink)[-1])` with # REPL, because the lldb API is faulty and it's not possible to cast # a struct member of 32-bit type to 64-bit type without getting onto # the next property bits, despite the fact that it's an actual value, not # a pointer to it. - bcins = vtou64(dbg_eval('((BCIns *)' + str(frame_pc(framelink)) + ')[-1]')) + bcins = vtou64(dbg.eval('((BCIns *)' + str(frame_pc(framelink)) + ')[-1]')) return framelink - (1 + LJ_FR2 + bc_a(bcins)) @@ -789,12 +979,12 @@ def frames(L): def dump_framelink_slot_address(fr): return '{start:{padding}}:{end:{padding}}'.format( - start=hex(int(fr - 1)), - end=hex(int(fr)), + start=strx64(fr - 1), + end=strx64(fr), padding=len(PADDING), ) if LJ_FR2 else '{addr:{padding}}'.format( - addr=hex(int(fr)), - padding=len(PADDING), + addr=strx64(fr), + padding=2 * len(PADDING) + 1, ) @@ -863,7 +1053,6 @@ def dump_stack(L, base=None, top=None): nfreeslots=int((maxstack - top - 8) >> 3), ), ]) - for framelink, frametop in frames(L): # Dump all data slots in the (framelink, top) interval. dump.extend([ @@ -904,9 +1093,11 @@ the type and some info related to it. Whether the type of the given address differs from the listed above, then error message occurs. ''' - def execute(self, debugger, args, result): + command = 'lj-tv' + + def execute(self, args): tvptr = TValuePtr(cast('TValue *', self.parse(args))) - print('{}'.format(dump_tvalue(tvptr))) + dbg.write('{}'.format(dump_tvalue(tvptr))) class LJState(Command): @@ -917,9 +1108,11 @@ The command requires no args and dumps current VM and GC states * GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST> * JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR> ''' - def execute(self, debugger, args, result): + command = 'lj-state' + + def execute(self, args): g = G(L(None)) - print('{}'.format('\n'.join( + dbg.write('{}'.format('\n'.join( map(lambda t: '{} state: {}'.format(*t), { 'VM': vm_state(g), 'GC': gc_state(g), @@ -936,8 +1129,10 @@ The command requires no args and dumps values of LJ_64 and LJ_GC64 compile-time flags. These values define the sizes of host and GC pointers respectively. ''' - def execute(self, debugger, args, result): - print( + command = 'lj-arch' + + def execute(self, args): + dbg.write( 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}' .format( LJ_64=LJ_64, @@ -965,9 +1160,11 @@ The command requires no args and dumps current GC stats: * weak: <number of weak tables (to be cleared)> * mmudata: <number of udata|cdata to be finalized> ''' - def execute(self, debugger, args, result): + command = 'lj-gc' + + def execute(self, args): g = G(L(None)) - print('GC stats: {state}\n{stats}'.format( + dbg.write('GC stats: {state}\n{stats}'.format( state=gc_state(g), stats=dump_gc(g) )) @@ -983,9 +1180,11 @@ the payload, size in bytes and hash. *Caveat*: Since Python 2 provides no native Unicode support, the payload is replaced with the corresponding error when decoding fails. ''' - def execute(self, debugger, args, result): + command = 'lj-str' + + def execute(self, args): string_ptr = GCstrPtr(cast('GCstr *', self.parse(args))) - print("String: {body} [{len} bytes] with hash {hash}".format( + dbg.write("String: {body} [{len} bytes] with hash {hash}".format( body=strdata(string_ptr), hash=strx64(string_ptr.hash), len=string_ptr.len, @@ -1003,7 +1202,9 @@ The command receives a GCtab adress and dumps the table contents: * Hash part <hsize> nodes: <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr> ''' - def execute(self, debugger, args, result): + command = 'lj-tab' + + def execute(self, args): t = GCtabPtr(cast('GCtab *', self.parse(args))) array = mref(TValuePtr, t.array) nodes = mref(NodePtr, t.node) @@ -1014,22 +1215,22 @@ The command receives a GCtab adress and dumps the table contents: } if mt: - print('Metatable detected: {}'.format(strx64(mt))) + dbg.write('Metatable detected: {}'.format(strx64(mt))) - print('Array part: {} slots'.format(capacity['apart'])) + dbg.write('Array part: {} slots'.format(capacity['apart'])) for i in range(capacity['apart']): slot = array + i - print('{ptr}: [{index}]: {value}'.format( + dbg.write('{ptr}: [{index}]: {value}'.format( ptr=strx64(slot), index=i, value=dump_tvalue(slot) )) - print('Hash part: {} nodes'.format(capacity['hpart'])) + dbg.write('Hash part: {} nodes'.format(capacity['hpart'])) # See hmask comment in lj_obj.h for i in range(capacity['hpart']): node = nodes + i - print('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format( + dbg.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format( ptr=strx64(node), key=dump_tvalue(TValuePtr(node.key.addr)), val=dump_tvalue(TValuePtr(node.val.addr)), @@ -1069,56 +1270,72 @@ coroutine guest stack: If L is ommited the main coroutine is used. ''' - def execute(self, debugger, args, result): + command = 'lj-stack' + + def execute(self, args): lstate = self.parse(args) - lstate_ptr = cast('lua_State *', lstate) if coro is not None else None - print('{}'.format(dump_stack(L(lstate_ptr)))) + lstate_ptr = cast('lua_State *', lstate) if lstate else None + dbg.write('{}'.format(dump_stack(L(lstate_ptr)))) -def register_commands(debugger, commands): - for command, cls in commands.items(): - cls.command = command - debugger.HandleCommand( - 'command script add --overwrite --class luajit_lldb.{cls} {cmd}' - .format( - cls=cls.__name__, - cmd=cls.command, - ) - ) - print('{cmd} command intialized'.format(cmd=cls.command)) +LJ_COMMANDS = [ + LJDumpTValue, + LJState, + LJDumpArch, + LJGC, + LJDumpString, + LJDumpTable, + LJDumpStack, +] + +def register_commands(commands, debugger=None): + for cls in commands: + dbg.cmd_init(cls, debugger) + dbg.write('{cmd} command intialized'.format(cmd=cls.command)) -def configure(debugger): - global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, PADDING, LJ_TISNUM, target - target = debugger.GetSelectedTarget() - module = target.modules[0] - LJ_DUALNUM = module.FindSymbol('lj_lib_checknumber') is not None +def configure(debugger=None): + global PADDING, LJ_TISNUM, LJ_DUALNUM + dbg.setup_target(debugger) try: - irtype_enum = target.FindFirstType('IRType').enum_members - for member in irtype_enum: - if member.name == 'IRT_PTR': - LJ_64 = member.unsigned & 0x1f == IRT_P64 - if member.name == 'IRT_PGC': - LJ_FR2 = LJ_GC64 = member.unsigned & 0x1f == IRT_P64 + # Try to remove the callback at first to not append duplicates to + # gdb.events.new_objfile internal list. + dbg.event_disconnect(load) except Exception: - print('luajit_lldb.py failed to load: ' - 'no debugging symbols found for libluajit') - return - - PADDING = ' ' * len(strx64((TValuePtr(L().addr)))) - LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] - - -def __lldb_init_module(debugger, internal_dict): - configure(debugger) - register_commands(debugger, { - 'lj-tv': LJDumpTValue, - 'lj-state': LJState, - 'lj-arch': LJDumpArch, - 'lj-gc': LJGC, - 'lj-str': LJDumpString, - 'lj-tab': LJDumpTable, - 'lj-stack': LJDumpStack, - }) - print('luajit_lldb.py is successfully loaded') + # Callback is not connected. + pass + + try: + # Detect whether libluajit objfile is loaded. + dbg.eval('luaJIT_setmode') + except Exception: + dbg.write('luajit_dbg.py initialization is postponed ' + 'until libluajit objfile is loaded\n') + # Add a callback to be executed when the next objfile is loaded. + dbg.event_connect(load) + return False + + try: + dbg.arch_init() + except Exception: + dbg.write('LuaJIT debug extension failed to load: ' + 'no debugging symbols found for libluajit') + return False + return True + + +# XXX: The dummy parameter is needed for this function to +# work as a gdb callback. +def load(_=None, debugger=None): + if configure(debugger): + register_commands(LJ_COMMANDS, debugger) + dbg.write('LuaJIT debug extension is successfully loaded') + + +def __lldb_init_module(debugger, _=None): + load(None, debugger) + + +if dbg.GDB: + load() -- 2.39.3 (Apple Git-145) ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [Tarantool-patches] [PATCH luajit v3 1/2] debug: generalized extension 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 1/2] " Maksim Kokryashkin via Tarantool-patches @ 2023-11-22 14:46 ` Sergey Bronnikov via Tarantool-patches 2023-12-06 13:58 ` Maxim Kokryashkin via Tarantool-patches 0 siblings, 1 reply; 7+ messages in thread From: Sergey Bronnikov via Tarantool-patches @ 2023-11-22 14:46 UTC (permalink / raw) To: Maksim Kokryashkin, tarantool-patches, skaplun, m.kokryashkin, imun Hello, Max, thanks for the patch! extension cannot be load in GDB: (gdb) source src/luajit_dbg.py Traceback (most recent call last): File "src/luajit_dbg.py", line 228, in <module> dbg = Debugger() File "src/luajit_dbg.py", line 46, in __init__ if healthcheck(lib): File "src/luajit_dbg.py", line 37, in <lambda> 'lldb': lambda lib: lib.debugger is not None, AttributeError: module 'lldb' has no attribute 'debugger' (gdb) [0] ~/sources/MRG/tarantool/third_party/luajit$ gdb --version GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1 On 11/10/23 23:16, Maksim Kokryashkin wrote: > From: Maxim Kokryashkin <m.kokryashkin@tarantool.org> > > This patch joins the LLDB and GDB LuaJIT debugging extensions > into one, so now the extension logic can be debugger-agnostic. > To do that, an adapter class is introduced, and all of the > debugger-specific behavior is encapsulated there. The extension > auto-detects the debugger it was loaded into and selects the > correct low-level logic implementation. > --- > src/luajit-gdb.py | 885 -------------------------- > src/{luajit_lldb.py => luajit_dbg.py} | 617 ++++++++++++------ > 2 files changed, 417 insertions(+), 1085 deletions(-) > delete mode 100644 src/luajit-gdb.py > rename src/{luajit_lldb.py => luajit_dbg.py} (63%) > > diff --git a/src/luajit-gdb.py b/src/luajit-gdb.py > deleted file mode 100644 > index 5eaf250f..00000000 > --- a/src/luajit-gdb.py > +++ /dev/null > @@ -1,885 +0,0 @@ > -# GDB extension for LuaJIT post-mortem analysis. > -# To use, just put 'source <path-to-repo>/src/luajit-gdb.py' in gdb. > - > -import re > -import gdb > -import sys > - > -# make script compatible with the ancient Python {{{ > - > - > -LEGACY = re.match(r'^2\.', sys.version) > - > -if LEGACY: > - CONNECTED = False > - int = long > - range = xrange > - > - > -# }}} > - > - > -gtype_cache = {} > - > - > -def gtype(typestr): > - global gtype_cache > - if typestr in gtype_cache: > - return gtype_cache[typestr] > - > - m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr) > - > - gtype = gdb.lookup_type(typestr) if m is None \ > - else gdb.lookup_type(m.group(1)).pointer() > - > - gtype_cache[typestr] = gtype > - return gtype > - > - > -def cast(typestr, val): > - return gdb.Value(val).cast(gtype(typestr)) > - > - > -def lookup(symbol): > - variable, _ = gdb.lookup_symbol(symbol) > - return variable.value() if variable else None > - > - > -def parse_arg(arg): > - if not arg: > - return None > - > - ret = gdb.parse_and_eval(arg) > - > - if not ret: > - raise gdb.GdbError('table argument empty') > - > - return ret > - > - > -def tou64(val): > - return cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF > - > - > -def tou32(val): > - return cast('uint32_t', val) & 0xFFFFFFFF > - > - > -def i2notu32(val): > - return ~int(val) & 0xFFFFFFFF > - > - > -def strx64(val): > - return re.sub('L?$', '', > - hex(int(cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF))) > - > - > -# Types {{{ > - > - > -LJ_T = { > - 'NIL': i2notu32(0), > - 'FALSE': i2notu32(1), > - 'TRUE': i2notu32(2), > - 'LIGHTUD': i2notu32(3), > - 'STR': i2notu32(4), > - 'UPVAL': i2notu32(5), > - 'THREAD': i2notu32(6), > - 'PROTO': i2notu32(7), > - 'FUNC': i2notu32(8), > - 'TRACE': i2notu32(9), > - 'CDATA': i2notu32(10), > - 'TAB': i2notu32(11), > - 'UDATA': i2notu32(12), > - 'NUMX': i2notu32(13), > -} > - > - > -def typenames(value): > - return { > - LJ_T[k]: 'LJ_T' + k for k in LJ_T.keys() > - }.get(int(value), 'LJ_TINVALID') > - > - > -# }}} > - > -# Frames {{{ > - > - > -FRAME_TYPE = 0x3 > -FRAME_P = 0x4 > -FRAME_TYPEP = FRAME_TYPE | FRAME_P > - > -FRAME = { > - 'LUA': 0x0, > - 'C': 0x1, > - 'CONT': 0x2, > - 'VARG': 0x3, > - 'LUAP': 0x4, > - 'CP': 0x5, > - 'PCALL': 0x6, > - 'PCALLH': 0x7, > -} > - > - > -def frametypes(ft): > - return { > - FRAME['LUA']: 'L', > - FRAME['C']: 'C', > - FRAME['CONT']: 'M', > - FRAME['VARG']: 'V', > - }.get(ft, '?') > - > - > -def bc_a(ins): > - return (ins >> 8) & 0xff > - > - > -def frame_ftsz(framelink): > - return cast('ptrdiff_t', framelink['ftsz'] if LJ_FR2 > - else framelink['fr']['tp']['ftsz']) > - > - > -def frame_pc(framelink): > - return cast('BCIns *', frame_ftsz(framelink)) if LJ_FR2 \ > - else mref('BCIns *', framelink['fr']['tp']['pcr']) > - > - > -def frame_prevl(framelink): > - return framelink - (1 + LJ_FR2 + bc_a(frame_pc(framelink)[-1])) > - > - > -def frame_ispcall(framelink): > - return (frame_ftsz(framelink) & FRAME['PCALL']) == FRAME['PCALL'] > - > - > -def frame_sized(framelink): > - return (frame_ftsz(framelink) & ~FRAME_TYPEP) > - > - > -def frame_prevd(framelink): > - return cast('TValue *', cast('char *', framelink) - frame_sized(framelink)) > - > - > -def frame_type(framelink): > - return frame_ftsz(framelink) & FRAME_TYPE > - > - > -def frame_typep(framelink): > - return frame_ftsz(framelink) & FRAME_TYPEP > - > - > -def frame_islua(framelink): > - return frametypes(int(frame_type(framelink))) == 'L' \ > - and int(frame_ftsz(framelink)) > 0 > - > - > -def frame_prev(framelink): > - return frame_prevl(framelink) if frame_islua(framelink) \ > - else frame_prevd(framelink) > - > - > -def frame_sentinel(L): > - return mref('TValue *', L['stack']) + LJ_FR2 > - > - > -# }}} > - > -# Const {{{ > - > - > -LJ_64 = None > -LJ_GC64 = None > -LJ_FR2 = None > -LJ_DUALNUM = None > - > -LJ_GCVMASK = ((1 << 47) - 1) > -LJ_TISNUM = None > -PADDING = None > - > -# These constants are meaningful only for 'LJ_64' mode. > -LJ_LIGHTUD_BITS_SEG = 8 > -LJ_LIGHTUD_BITS_LO = 47 - LJ_LIGHTUD_BITS_SEG > -LIGHTUD_SEG_MASK = (1 << LJ_LIGHTUD_BITS_SEG) - 1 > -LIGHTUD_LO_MASK = (1 << LJ_LIGHTUD_BITS_LO) - 1 > - > - > -# }}} > - > - > -def itype(o): > - return cast('uint32_t', o['it64'] >> 47) if LJ_GC64 else o['it'] > - > - > -def mref(typename, obj): > - return cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32']) > - > - > -def gcref(obj): > - return cast('GCobj *', obj['gcptr64'] if LJ_GC64 > - else cast('uintptr_t', obj['gcptr32'])) > - > - > -def gcval(obj): > - return cast('GCobj *', obj['gcptr64'] & LJ_GCVMASK if LJ_GC64 > - else cast('uintptr_t', obj['gcptr32'])) > - > - > -def gcnext(obj): > - return gcref(obj)['gch']['nextgc'] > - > - > -def L(L=None): > - # lookup a symbol for the main coroutine considering the host app > - # XXX Fragile: though the loop initialization looks like a crap but it > - # respects both Python 2 and Python 3. > - for lstate in [L] + list(map(lambda main: lookup(main), ( > - # LuaJIT main coro (see luajit/src/luajit.c) > - 'globalL', > - # Tarantool main coro (see tarantool/src/lua/init.h) > - 'tarantool_L', > - # TODO: Add more > - ))): > - if lstate: > - return cast('lua_State *', lstate) > - > - > -def G(L): > - return mref('global_State *', L['glref']) > - > - > -def J(g): > - typeGG = gtype('GG_State') > - > - return cast('jit_State *', int(cast('char *', g)) > - - int(typeGG['g'].bitpos / 8) > - + int(typeGG['J'].bitpos / 8)) > - > - > -def vm_state(g): > - return { > - i2notu32(0): 'INTERP', > - i2notu32(1): 'LFUNC', > - i2notu32(2): 'FFUNC', > - i2notu32(3): 'CFUNC', > - i2notu32(4): 'GC', > - i2notu32(5): 'EXIT', > - i2notu32(6): 'RECORD', > - i2notu32(7): 'OPT', > - i2notu32(8): 'ASM', > - }.get(int(tou32(g['vmstate'])), 'TRACE') > - > - > -def gc_state(g): > - return { > - 0: 'PAUSE', > - 1: 'PROPAGATE', > - 2: 'ATOMIC', > - 3: 'SWEEPSTRING', > - 4: 'SWEEP', > - 5: 'FINALIZE', > - 6: 'LAST', > - }.get(int(g['gc']['state']), 'INVALID') > - > - > -def jit_state(g): > - return { > - 0: 'IDLE', > - 0x10: 'ACTIVE', > - 0x11: 'RECORD', > - 0x12: 'START', > - 0x13: 'END', > - 0x14: 'ASM', > - 0x15: 'ERR', > - }.get(int(J(g)['state']), 'INVALID') > - > - > -def tvisint(o): > - return LJ_DUALNUM and itype(o) == LJ_TISNUM > - > - > -def tvisnumber(o): > - return itype(o) <= LJ_TISNUM > - > - > -def tvislightud(o): > - if LJ_64 and not LJ_GC64: > - return (cast('int32_t', itype(o)) >> 15) == -2 > - else: > - return itype(o) == LJ_T['LIGHTUD'] > - > - > -def strdata(obj): > - # String is printed with pointer to it, thanks to gdb. Just strip it. > - try: > - return str(cast('char *', cast('GCstr *', obj) + 1))[len(PADDING):] > - except UnicodeEncodeError: > - return "<luajit-gdb: error occured while rendering non-ascii slot>" > - > - > -def itypemap(o): > - if LJ_64 and not LJ_GC64: > - return LJ_T['NUMX'] if tvisnumber(o) \ > - else LJ_T['LIGHTUD'] if tvislightud(o) \ > - else itype(o) > - else: > - return LJ_T['NUMX'] if tvisnumber(o) else itype(o) > - > - > -def funcproto(func): > - assert func['ffid'] == 0 > - > - return cast('GCproto *', > - mref('char *', func['pc']) - gdb.lookup_type('GCproto').sizeof) > - > - > -def gclistlen(root, end=0x0): > - count = 0 > - while (gcref(root) != end): > - count += 1 > - root = gcnext(root) > - return count > - > - > -def gcringlen(root): > - if not gcref(root): > - return 0 > - elif gcref(root) == gcref(gcnext(root)): > - return 1 > - else: > - return 1 + gclistlen(gcnext(root), gcref(root)) > - > - > -gclen = { > - 'root': gclistlen, > - 'gray': gclistlen, > - 'grayagain': gclistlen, > - 'weak': gclistlen, > - # XXX: gc.mmudata is a ring-list. > - 'mmudata': gcringlen, > -} > - > - > -# The generator that implements frame iterator. > -# Every frame is represented as a tuple of framelink and frametop. > -def frames(L): > - frametop = L['top'] > - framelink = L['base'] - 1 > - framelink_sentinel = frame_sentinel(L) > - while True: > - yield framelink, frametop > - frametop = framelink - (1 + LJ_FR2) > - if framelink <= framelink_sentinel: > - break > - framelink = frame_prev(framelink) > - > - > -def lightudV(tv): > - if LJ_64: > - u = int(tv['u64']) > - # lightudseg macro expanded. > - seg = (u >> LJ_LIGHTUD_BITS_LO) & LIGHTUD_SEG_MASK > - segmap = mref('uint32_t *', G(L(None))['gc']['lightudseg']) > - # lightudlo macro expanded. > - return (int(segmap[seg]) << 32) | (u & LIGHTUD_LO_MASK) > - else: > - return gcval(tv['gcr']) > - > - > -# Dumpers {{{ > - > - > -def dump_lj_tnil(tv): > - return 'nil' > - > - > -def dump_lj_tfalse(tv): > - return 'false' > - > - > -def dump_lj_ttrue(tv): > - return 'true' > - > - > -def dump_lj_tlightud(tv): > - return 'light userdata @ {}'.format(strx64(lightudV(tv))) > - > - > -def dump_lj_tstr(tv): > - return 'string {body} @ {address}'.format( > - body=strdata(gcval(tv['gcr'])), > - address=strx64(gcval(tv['gcr'])) > - ) > - > - > -def dump_lj_tupval(tv): > - return 'upvalue @ {}'.format(strx64(gcval(tv['gcr']))) > - > - > -def dump_lj_tthread(tv): > - return 'thread @ {}'.format(strx64(gcval(tv['gcr']))) > - > - > -def dump_lj_tproto(tv): > - return 'proto @ {}'.format(strx64(gcval(tv['gcr']))) > - > - > -def dump_lj_tfunc(tv): > - func = cast('struct GCfuncC *', gcval(tv['gcr'])) > - ffid = func['ffid'] > - > - if ffid == 0: > - pt = funcproto(func) > - return 'Lua function @ {addr}, {nups} upvalues, {chunk}:{line}'.format( > - addr=strx64(func), > - nups=int(func['nupvalues']), > - chunk=strdata(cast('GCstr *', gcval(pt['chunkname']))), > - line=pt['firstline'] > - ) > - elif ffid == 1: > - return 'C function @ {}'.format(strx64(func['f'])) > - else: > - return 'fast function #{}'.format(int(ffid)) > - > - > -def dump_lj_ttrace(tv): > - trace = cast('struct GCtrace *', gcval(tv['gcr'])) > - return 'trace {traceno} @ {addr}'.format( > - traceno=strx64(trace['traceno']), > - addr=strx64(trace) > - ) > - > - > -def dump_lj_tcdata(tv): > - return 'cdata @ {}'.format(strx64(gcval(tv['gcr']))) > - > - > -def dump_lj_ttab(tv): > - table = cast('GCtab *', gcval(tv['gcr'])) > - return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format( > - gcr=strx64(table), > - asize=table['asize'], > - hmask=strx64(table['hmask']), > - ) > - > - > -def dump_lj_tudata(tv): > - return 'userdata @ {}'.format(strx64(gcval(tv['gcr']))) > - > - > -def dump_lj_tnumx(tv): > - if tvisint(tv): > - return 'integer {}'.format(cast('int32_t', tv['i'])) > - else: > - return 'number {}'.format(cast('double', tv['n'])) > - > - > -def dump_lj_invalid(tv): > - return 'not valid type @ {}'.format(strx64(gcval(tv['gcr']))) > - > - > -# }}} > - > - > -dumpers = { > - 'LJ_TNIL': dump_lj_tnil, > - 'LJ_TFALSE': dump_lj_tfalse, > - 'LJ_TTRUE': dump_lj_ttrue, > - 'LJ_TLIGHTUD': dump_lj_tlightud, > - 'LJ_TSTR': dump_lj_tstr, > - 'LJ_TUPVAL': dump_lj_tupval, > - 'LJ_TTHREAD': dump_lj_tthread, > - 'LJ_TPROTO': dump_lj_tproto, > - 'LJ_TFUNC': dump_lj_tfunc, > - 'LJ_TTRACE': dump_lj_ttrace, > - 'LJ_TCDATA': dump_lj_tcdata, > - 'LJ_TTAB': dump_lj_ttab, > - 'LJ_TUDATA': dump_lj_tudata, > - 'LJ_TNUMX': dump_lj_tnumx, > -} > - > - > -def dump_tvalue(tvalue): > - return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue) > - > - > -def dump_framelink_slot_address(fr): > - return '{}:{}'.format(fr - 1, fr) if LJ_FR2 \ > - else '{}'.format(fr) + PADDING > - > - > -def dump_framelink(L, fr): > - if fr == frame_sentinel(L): > - return '{addr} [S ] FRAME: dummy L'.format( > - addr=dump_framelink_slot_address(fr), > - ) > - return '{addr} [ ] FRAME: [{pp}] delta={d}, {f}'.format( > - addr=dump_framelink_slot_address(fr), > - pp='PP' if frame_ispcall(fr) else '{frname}{p}'.format( > - frname=frametypes(int(frame_type(fr))), > - p='P' if frame_typep(fr) & FRAME_P else '' > - ), > - d=cast('TValue *', fr) - cast('TValue *', frame_prev(fr)), > - f=dump_lj_tfunc(fr - LJ_FR2), > - ) > - > - > -def dump_stack_slot(L, slot, base=None, top=None): > - base = base or L['base'] > - top = top or L['top'] > - > - return '{addr}{padding} [ {B}{T}{M}] VALUE: {value}'.format( > - addr=strx64(slot), > - padding=PADDING, > - B='B' if slot == base else ' ', > - T='T' if slot == top else ' ', > - M='M' if slot == mref('TValue *', L['maxstack']) else ' ', > - value=dump_tvalue(slot), > - ) > - > - > -def dump_stack(L, base=None, top=None): > - base = base or L['base'] > - top = top or L['top'] > - stack = mref('TValue *', L['stack']) > - maxstack = mref('TValue *', L['maxstack']) > - red = 5 + 2 * LJ_FR2 > - > - dump = [ > - '{padding} Red zone: {nredslots: >2} slots {padding}'.format( > - padding='-' * len(PADDING), > - nredslots=red, > - ), > - ] > - dump.extend([ > - dump_stack_slot(L, maxstack + offset, base, top) > - for offset in range(red, 0, -1) # noqa: E131 > - ]) > - dump.extend([ > - '{padding} Stack: {nstackslots: >5} slots {padding}'.format( > - padding='-' * len(PADDING), > - nstackslots=int((tou64(maxstack) - tou64(stack)) >> 3), > - ), > - dump_stack_slot(L, maxstack, base, top), > - '{start}:{end} [ ] {nfreeslots} slots: Free stack slots'.format( > - start=strx64(top + 1), > - end=strx64(maxstack - 1), > - nfreeslots=int((tou64(maxstack) - tou64(top) - 8) >> 3), > - ), > - ]) > - > - for framelink, frametop in frames(L): > - # Dump all data slots in the (framelink, top) interval. > - dump.extend([ > - dump_stack_slot(L, framelink + offset, base, top) > - for offset in range(frametop - framelink, 0, -1) # noqa: E131 > - ]) > - # Dump frame slot (2 slots in case of GC64). > - dump.append(dump_framelink(L, framelink)) > - > - return '\n'.join(dump) > - > - > -def dump_gc(g): > - gc = g['gc'] > - stats = ['{key}: {value}'.format(key=f, value=gc[f]) for f in ( > - 'total', 'threshold', 'debt', 'estimate', 'stepmul', 'pause' > - )] > - > - stats += ['sweepstr: {sweepstr}/{strmask}'.format( > - sweepstr=gc['sweepstr'], > - # String hash mask (size of hash table - 1). > - strmask=g['strmask'] + 1, > - )] > - > - stats += ['{key}: {number} objects'.format( > - key=stat, > - number=handler(gc[stat]) > - ) for stat, handler in gclen.items()] > - > - return '\n'.join(map(lambda s: '\t' + s, stats)) > - > - > -class LJBase(gdb.Command): > - > - def __init__(self, name): > - # XXX Fragile: though the command initialization looks like a crap but > - # it respects both Python 2 and Python 3. > - gdb.Command.__init__(self, name, gdb.COMMAND_DATA) > - gdb.write('{} command initialized\n'.format(name)) > - > - > -class LJDumpArch(LJBase): > - ''' > -lj-arch > - > -The command requires no args and dumps values of LJ_64 and LJ_GC64 > -compile-time flags. These values define the sizes of host and GC > -pointers respectively. > - ''' > - > - def invoke(self, arg, from_tty): > - gdb.write( > - 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}\n' > - .format( > - LJ_64=LJ_64, > - LJ_GC64=LJ_GC64, > - LJ_DUALNUM=LJ_DUALNUM > - ) > - ) > - > - > -class LJDumpTValue(LJBase): > - ''' > -lj-tv <TValue *> > - > -The command receives a pointer to <tv> (TValue address) and dumps > -the type and some info related to it. > - > -* LJ_TNIL: nil > -* LJ_TFALSE: false > -* LJ_TTRUE: true > -* LJ_TLIGHTUD: light userdata @ <gcr> > -* LJ_TSTR: string <string payload> @ <gcr> > -* LJ_TUPVAL: upvalue @ <gcr> > -* LJ_TTHREAD: thread @ <gcr> > -* LJ_TPROTO: proto @ <gcr> > -* LJ_TFUNC: <LFUNC|CFUNC|FFUNC> > - <LFUNC>: Lua function @ <gcr>, <nupvals> upvalues, <chunk:line> > - <CFUNC>: C function <mcode address> > - <FFUNC>: fast function #<ffid> > -* LJ_TTRACE: trace <traceno> @ <gcr> > -* LJ_TCDATA: cdata @ <gcr> > -* LJ_TTAB: table @ <gcr> (asize: <asize>, hmask: <hmask>) > -* LJ_TUDATA: userdata @ <gcr> > -* LJ_TNUMX: number <numeric payload> > - > -Whether the type of the given address differs from the listed above, then > -error message occurs. > - ''' > - > - def invoke(self, arg, from_tty): > - tv = cast('TValue *', parse_arg(arg)) > - gdb.write('{}\n'.format(dump_tvalue(tv))) > - > - > -class LJDumpString(LJBase): > - ''' > -lj-str <GCstr *> > - > -The command receives a <gcr> of the corresponding GCstr object and dumps > -the payload, size in bytes and hash. > - > -*Caveat*: Since Python 2 provides no native Unicode support, the payload > -is replaced with the corresponding error when decoding fails. > - ''' > - > - def invoke(self, arg, from_tty): > - string = cast('GCstr *', parse_arg(arg)) > - gdb.write("String: {body} [{len} bytes] with hash {hash}\n".format( > - body=strdata(string), > - hash=strx64(string['hash']), > - len=string['len'], > - )) > - > - > -class LJDumpTable(LJBase): > - ''' > -lj-tab <GCtab *> > - > -The command receives a GCtab adress and dumps the table contents: > -* Metatable address whether the one is set > -* Array part <asize> slots: > - <aslot ptr>: [<index>]: <tv> > -* Hash part <hsize> nodes: > - <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr> > - ''' > - > - def invoke(self, arg, from_tty): > - t = cast('GCtab *', parse_arg(arg)) > - array = mref('TValue *', t['array']) > - nodes = mref('struct Node *', t['node']) > - mt = gcval(t['metatable']) > - capacity = { > - 'apart': int(t['asize']), > - 'hpart': int(t['hmask'] + 1) if t['hmask'] > 0 else 0 > - } > - > - if mt != 0: > - gdb.write('Metatable detected: {}\n'.format(strx64(mt))) > - > - gdb.write('Array part: {} slots\n'.format(capacity['apart'])) > - for i in range(capacity['apart']): > - slot = array + i > - gdb.write('{ptr}: [{index}]: {value}\n'.format( > - ptr=slot, > - index=i, > - value=dump_tvalue(slot) > - )) > - > - gdb.write('Hash part: {} nodes\n'.format(capacity['hpart'])) > - # See hmask comment in lj_obj.h > - for i in range(capacity['hpart']): > - node = nodes + i > - gdb.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}\n'.format( > - ptr=node, > - key=dump_tvalue(node['key']), > - val=dump_tvalue(node['val']), > - n=mref('struct Node *', node['next']) > - )) > - > - > -class LJDumpStack(LJBase): > - ''' > -lj-stack [<lua_State *>] > - > -The command receives a lua_State address and dumps the given Lua > -coroutine guest stack: > - > -<slot ptr> [<slot attributes>] <VALUE|FRAME> > - > -* <slot ptr>: guest stack slot address > -* <slot attributes>: > - - S: Bottom of the stack (the slot L->stack points to) > - - B: Base of the current guest frame (the slot L->base points to) > - - T: Top of the current guest frame (the slot L->top points to) > - - M: Last slot of the stack (the slot L->maxstack points to) > -* <VALUE>: see help lj-tv for more info > -* <FRAME>: framelink slot differs from the value slot: it contains info > - related to the function being executed within this guest frame, its > - type and link to the parent guest frame > - [<frame type>] delta=<slots in frame>, <lj-tv for LJ_TFUNC slot> > - - <frame type>: > - + L: VM performs a call as a result of bytecode execution > - + C: VM performs a call as a result of lj_vm_call > - + M: VM performs a call to a metamethod as a result of bytecode > - execution > - + V: Variable-length frame for storing arguments of a variadic > - function > - + CP: Protected C frame > - + PP: VM performs a call as a result of executinig pcall or xpcall > - > -If L is ommited the main coroutine is used. > - ''' > - > - def invoke(self, arg, from_tty): > - gdb.write('{}\n'.format(dump_stack(L(parse_arg(arg))))) > - > - > -class LJState(LJBase): > - ''' > -lj-state > -The command requires no args and dumps current VM and GC states > -* VM state: <INTERP|C|GC|EXIT|RECORD|OPT|ASM|TRACE> > -* GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST> > -* JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR> > - ''' > - > - def invoke(self, arg, from_tty): > - g = G(L(None)) > - gdb.write('{}\n'.format('\n'.join( > - map(lambda t: '{} state: {}'.format(*t), { > - 'VM': vm_state(g), > - 'GC': gc_state(g), > - 'JIT': jit_state(g), > - }.items()) > - ))) > - > - > -class LJGC(LJBase): > - ''' > -lj-gc > - > -The command requires no args and dumps current GC stats: > -* total: <total number of allocated bytes in GC area> > -* threshold: <limit when gc step is triggered> > -* debt: <how much GC is behind schedule> > -* estimate: <estimate of memory actually in use> > -* stepmul: <incremental GC step granularity> > -* pause: <pause between successive GC cycles> > -* sweepstr: <sweep position in string table> > -* root: <number of all collectable objects> > -* gray: <number of gray objects> > -* grayagain: <number of objects for atomic traversal> > -* weak: <number of weak tables (to be cleared)> > -* mmudata: <number of udata|cdata to be finalized> > - ''' > - > - def invoke(self, arg, from_tty): > - g = G(L(None)) > - gdb.write('GC stats: {state}\n{stats}\n'.format( > - state=gc_state(g), > - stats=dump_gc(g) > - )) > - > - > -def init(commands): > - global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, LJ_TISNUM, PADDING > - > - # XXX Fragile: though connecting the callback looks like a crap but it > - # respects both Python 2 and Python 3 (see #4828). > - def connect(callback): > - if LEGACY: > - global CONNECTED > - CONNECTED = True > - gdb.events.new_objfile.connect(callback) > - > - # XXX Fragile: though disconnecting the callback looks like a crap but it > - # respects both Python 2 and Python 3 (see #4828). > - def disconnect(callback): > - if LEGACY: > - global CONNECTED > - if not CONNECTED: > - return > - CONNECTED = False > - gdb.events.new_objfile.disconnect(callback) > - > - try: > - # Try to remove the callback at first to not append duplicates to > - # gdb.events.new_objfile internal list. > - disconnect(load) > - except Exception: > - # Callback is not connected. > - pass > - > - try: > - # Detect whether libluajit objfile is loaded. > - gdb.parse_and_eval('luaJIT_setmode') > - except Exception: > - gdb.write('luajit-gdb.py initialization is postponed ' > - 'until libluajit objfile is loaded\n') > - # Add a callback to be executed when the next objfile is loaded. > - connect(load) > - return > - > - try: > - LJ_64 = str(gdb.parse_and_eval('IRT_PTR')) == 'IRT_P64' > - LJ_FR2 = LJ_GC64 = str(gdb.parse_and_eval('IRT_PGC')) == 'IRT_P64' > - LJ_DUALNUM = gdb.lookup_global_symbol('lj_lib_checknumber') is not None > - except Exception: > - gdb.write('luajit-gdb.py failed to load: ' > - 'no debugging symbols found for libluajit\n') > - return > - > - for name, command in commands.items(): > - command(name) > - > - PADDING = ' ' * len(':' + hex((1 << (47 if LJ_GC64 else 32)) - 1)) > - LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] > - > - gdb.write('luajit-gdb.py is successfully loaded\n') > - > - > -def load(event=None): > - init({ > - 'lj-arch': LJDumpArch, > - 'lj-tv': LJDumpTValue, > - 'lj-str': LJDumpString, > - 'lj-tab': LJDumpTable, > - 'lj-stack': LJDumpStack, > - 'lj-state': LJState, > - 'lj-gc': LJGC, > - }) > - > - > -load(None) > diff --git a/src/luajit_lldb.py b/src/luajit_dbg.py > similarity index 63% > rename from src/luajit_lldb.py > rename to src/luajit_dbg.py > index ef0986cc..cb46b473 100644 > --- a/src/luajit_lldb.py > +++ b/src/luajit_dbg.py > @@ -1,10 +1,231 @@ > -# LLDB extension for LuaJIT post-mortem analysis. > -# To use, just put 'command script import <path-to-repo>/src/luajit_lldb.py' > -# in lldb. > +# Debug extension for LuaJIT post-mortem analysis. > +# To use in LLDB: 'command script import <path-to-repo>/src/luajit_dbg.py' > +# To use in GDB: 'source <path-to-repo>/src/luajit_dbg.py' > > import abc > import re > -import lldb > +import sys > +import types > + > +from importlib import import_module > + > +# make script compatible with the ancient Python {{{ > + > + > +LEGACY = re.match(r'^2\.', sys.version) > + > +if LEGACY: > + CONNECTED = False > + int = long > + range = xrange > + > + > +def is_integer_type(val): > + return isinstance(val, int) or (LEGACY and isinstance(val, types.IntType)) > + > + > +# }}} > + > + > +class Debugger(object): > + def __init__(self): > + self.GDB = False > + self.LLDB = False > + > + debuggers = { > + 'gdb': lambda lib: True, > + 'lldb': lambda lib: lib.debugger is not None, > + } > + for name, healthcheck in debuggers.items(): > + lib = None > + try: > + lib = import_module(name) > + except ImportError: > + continue > + > + if healthcheck(lib): > + setattr(self, name.upper(), True) > + globals()[name] = lib > + self.name = name > + > + assert self.LLDB != self.GDB > + > + def setup_target(self, debugger): > + global target > + if self.LLDB: > + target = debugger.GetSelectedTarget() > + > + def write(self, msg): > + if self.LLDB: > + print(msg) > + else: > + gdb.write(msg + '\n') > + > + def cmd_init(self, cmd_cls, debugger=None): > + if self.LLDB: > + debugger.HandleCommand( > + 'command script add --overwrite --class ' > + 'luajit_dbg.{cls} {cmd}' > + .format( > + cls=cmd_cls.__name__, > + cmd=cmd_cls.command, > + ) > + ) > + else: > + cmd_cls() > + > + def event_connect(self, callback): > + if not self.LLDB: > + # XXX Fragile: though connecting the callback looks like a crap but > + # it respects both Python 2 and Python 3 (see #4828). > + if LEGACY: > + global CONNECTED > + CONNECTED = True > + gdb.events.new_objfile.connect(callback) > + > + def event_disconnect(self, callback): > + if not self.LLDB: > + # XXX Fragile: though disconnecting the callback looks like a crap > + # but it respects both Python 2 and Python 3 (see #4828). > + if LEGACY: > + global CONNECTED > + if not CONNECTED: > + return > + CONNECTED = False > + gdb.events.new_objfile.disconnect(callback) > + > + def lookup_variable(self, name): > + if self.LLDB: > + return target.FindFirstGlobalVariable(name) > + else: > + variable, _ = gdb.lookup_symbol(name) > + return variable.value() if variable else None > + > + def lookup_symbol(self, sym): > + if self.LLDB: > + return target.modules[0].FindSymbol(sym) > + else: > + return gdb.lookup_global_symbol(sym) > + > + def to_unsigned(self, val): > + return val.unsigned if self.LLDB else int(val) > + > + def to_signed(self, val): > + return val.signed if self.LLDB else int(val) > + > + def to_str(self, val): > + return val.value if self.LLDB else str(val) > + > + def find_type(self, typename): > + if self.LLDB: > + return target.FindFirstType(typename) > + else: > + return gdb.lookup_type(typename) > + > + def type_to_pointer_type(self, tp): > + if self.LLDB: > + return tp.GetPointerType() > + else: > + return tp.pointer() > + > + def cast_impl(self, value, t, pointer_type): > + if self.LLDB: > + if is_integer_type(value): > + # Integer casts require some black magic > + # for lldb to behave properly. > + if pointer_type: > + return target.CreateValueFromAddress( > + 'value', > + lldb.SBAddress(value, target), > + t.GetPointeeType(), > + ).address_of > + else: > + return target.CreateValueFromData( > + name='value', > + data=lldb.SBData.CreateDataFromInt(value, size=8), > + type=t, > + ) > + else: > + return value.Cast(t) > + else: > + return gdb.Value(value).cast(t) > + > + def dereference(self, val): > + if self.LLDB: > + return val.Dereference() > + else: > + return val.dereference() > + > + def eval(self, expression): > + if self.LLDB: > + process = target.GetProcess() > + thread = process.GetSelectedThread() > + frame = thread.GetSelectedFrame() > + > + if not expression: > + return None > + > + return frame.EvaluateExpression(expression) > + else: > + return gdb.parse_and_eval(expression) > + > + def type_sizeof_impl(self, tp): > + if self.LLDB: > + return tp.GetByteSize() > + else: > + return tp.sizeof > + > + def summary(self, val): > + if self.LLDB: > + return val.summary > + else: > + return str(val)[len(PADDING):].strip() > + > + def type_member(self, type_obj, name): > + if self.LLDB: > + return next((x for x in type_obj.members if x.name == name), None) > + else: > + return type_obj[name] > + > + def type_member_offset(self, member): > + if self.LLDB: > + return member.GetOffsetInBytes() > + else: > + return member.bitpos / 8 > + > + def get_member(self, value, member_name): > + if self.LLDB: > + return value.GetChildMemberWithName(member_name) > + else: > + return value[member_name] > + > + def address_of(self, value): > + if self.LLDB: > + return value.address_of > + else: > + return value.address > + > + def arch_init(self): > + global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, PADDING, LJ_TISNUM, target > + if self.LLDB: > + irtype_enum = dbg.find_type('IRType').enum_members > + for member in irtype_enum: > + if member.name == 'IRT_PTR': > + LJ_64 = dbg.to_unsigned(member) & 0x1f == IRT_P64 > + if member.name == 'IRT_PGC': > + LJ_GC64 = dbg.to_unsigned(member) & 0x1f == IRT_P64 > + else: > + LJ_64 = str(dbg.eval('IRT_PTR')) == 'IRT_P64' > + LJ_GC64 = str(dbg.eval('IRT_PGC')) == 'IRT_P64' > + > + LJ_FR2 = LJ_GC64 > + LJ_DUALNUM = dbg.lookup_symbol('lj_lib_checknumber') is not None > + # Two extra characters are required to fit in the `0x` part. > + PADDING = ' ' * len(strx64(L())) > + LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] > + > + > +dbg = Debugger() > > LJ_64 = None > LJ_GC64 = None > @@ -17,68 +238,73 @@ IRT_P64 = 9 > LJ_GCVMASK = ((1 << 47) - 1) > LJ_TISNUM = None > > -# Debugger specific {{{ > - > - > # Global > target = None > > > -class Ptr: > +class Ptr(object): > def __init__(self, value, normal_type): > self.value = value > self.normal_type = normal_type > > @property > def __deref(self): > - return self.normal_type(self.value.Dereference()) > + return self.normal_type(dbg.dereference(self.value)) > > def __add__(self, other): > - assert isinstance(other, int) > + assert is_integer_type(other) > return self.__class__( > cast( > self.normal_type.__name__ + ' *', > cast( > 'uintptr_t', > - self.value.unsigned + other * self.value.deref.size, > + dbg.to_unsigned(self.value) + other * sizeof( > + self.normal_type.__name__ > + ), > ), > ), > ) > > def __sub__(self, other): > - assert isinstance(other, int) or isinstance(other, Ptr) > - if isinstance(other, int): > + assert is_integer_type(other) or isinstance(other, Ptr) > + if is_integer_type(other): > return self.__add__(-other) > else: > - return int((self.value.unsigned - other.value.unsigned) > - / sizeof(self.normal_type.__name__)) > + return int( > + ( > + dbg.to_unsigned(self.value) - dbg.to_unsigned(other.value) > + ) / sizeof(self.normal_type.__name__) > + ) > > def __eq__(self, other): > - assert isinstance(other, Ptr) or isinstance(other, int) and other >= 0 > + assert isinstance(other, Ptr) or is_integer_type(other) > if isinstance(other, Ptr): > - return self.value.unsigned == other.value.unsigned > + return dbg.to_unsigned(self.value) == dbg.to_unsigned(other.value) > else: > - return self.value.unsigned == other > + return dbg.to_unsigned(self.value) == other > > def __ne__(self, other): > return not self == other > > def __gt__(self, other): > assert isinstance(other, Ptr) > - return self.value.unsigned > other.value.unsigned > + return dbg.to_unsigned(self.value) > dbg.to_unsigned(other.value) > > def __ge__(self, other): > assert isinstance(other, Ptr) > - return self.value.unsigned >= other.value.unsigned > + return dbg.to_unsigned(self.value) >= dbg.to_unsigned(other.value) > > def __bool__(self): > - return self.value.unsigned != 0 > + return dbg.to_unsigned(self.value) != 0 > > def __int__(self): > - return self.value.unsigned > + return dbg.to_unsigned(self.value) > + > + def __long__(self): > + return dbg.to_unsigned(self.value) > > def __str__(self): > - return self.value.value > + return dbg.to_str(self.value) > > def __getattr__(self, name): > if name != '__deref': > @@ -86,53 +312,26 @@ class Ptr: > return self.__deref > > > -class MetaStruct(type): > - def __init__(cls, name, bases, nmspc): > - super(MetaStruct, cls).__init__(name, bases, nmspc) > - > - def make_general(field, tp): > - builtin = { > - 'uint': 'unsigned', > - 'int': 'signed', > - 'string': 'value', > - } > - if tp in builtin.keys(): > - return lambda self: getattr(self[field], builtin[tp]) > - else: > - return lambda self: globals()[tp](self[field]) > - > - if hasattr(cls, 'metainfo'): > - for field in cls.metainfo: > - if not isinstance(field[0], str): > - setattr(cls, field[1], field[0]) > - else: > - setattr( > - cls, > - field[1], > - property(make_general(field[1], field[0])), > - ) > - > - > -class Struct(metaclass=MetaStruct): > +class Struct(object): > def __init__(self, value): > self.value = value > > def __getitem__(self, name): > - return self.value.GetChildMemberWithName(name) > + return dbg.get_member(self.value, name) > > @property > def addr(self): > - return self.value.address_of > + return dbg.address_of(self.value) > > > c_structs = { > 'MRef': [ > - (property(lambda self: self['ptr64'].unsigned if LJ_GC64 > - else self['ptr32'].unsigned), 'ptr') > + (property(lambda self: dbg.to_unsigned(self['ptr64']) if LJ_GC64 > + else dbg.to_unsigned(self['ptr32'])), 'ptr') > ], > 'GCRef': [ > - (property(lambda self: self['gcptr64'].unsigned if LJ_GC64 > - else self['gcptr32'].unsigned), 'gcptr') > + (property(lambda self: dbg.to_unsigned(self['gcptr64']) if LJ_GC64 > + else dbg.to_unsigned(self['gcptr32'])), 'gcptr') > ], > 'TValue': [ > ('GCRef', 'gcr'), > @@ -141,8 +340,12 @@ c_structs = { > ('int', 'it64'), > ('string', 'n'), > (property(lambda self: FR(self['fr']) if not LJ_GC64 else None), 'fr'), > - (property(lambda self: self['ftsz'].signed if LJ_GC64 else None), > - 'ftsz') > + ( > + property( > + lambda self: dbg.to_signed(self['ftsz']) if LJ_GC64 else None > + ), > + 'ftsz' > + ) > ], > 'GCState': [ > ('GCRef', 'root'), > @@ -216,26 +419,51 @@ c_structs = { > ('TValue', 'val'), > ('MRef', 'next') > ], > - 'BCIns': [] > + 'BCIns': [], > } > > > -for cls in c_structs.keys(): > - globals()[cls] = type(cls, (Struct, ), {'metainfo': c_structs[cls]}) > +def make_property_from_metadata(field, tp): > + builtin = { > + 'uint': dbg.to_unsigned, > + 'int': dbg.to_signed, > + 'string': dbg.to_str, > + } > + if tp in builtin.keys(): > + return lambda self: builtin[tp](self[field]) > + else: > + return lambda self: globals()[tp](self[field]) > + > + > +for cls, metainfo in c_structs.items(): > + cls_dict = {} > + for field in metainfo: > + if not isinstance(field[0], str): > + cls_dict[field[1]] = field[0] > + else: > + cls_dict[field[1]] = property( > + make_property_from_metadata(field[1], field[0]) > + ) > + globals()[cls] = type(cls, (Struct, ), cls_dict) > > > for cls in Struct.__subclasses__(): > ptr_name = cls.__name__ + 'Ptr' > > + def make_init(cls): > + return lambda self, value: super(type(self), self).__init__(value, cls) > + > globals()[ptr_name] = type(ptr_name, (Ptr,), { > - '__init__': > - lambda self, value: super(type(self), self).__init__(value, cls) > + '__init__': make_init(cls) > }) > > > -class Command(object): > - def __init__(self, debugger, unused): > - pass > +class Command(object if dbg.LLDB else gdb.Command): > + def __init__(self, debugger=None, unused=None): > + if dbg.GDB: > + # XXX Fragile: though initialization looks like a crap but it > + # respects both Python 2 and Python 3 (see #4828). > + gdb.Command.__init__(self, self.command, gdb.COMMAND_DATA) > > def get_short_help(self): > return self.__doc__.splitlines()[0] > @@ -245,21 +473,15 @@ class Command(object): > > def __call__(self, debugger, command, exe_ctx, result): > try: > - self.execute(debugger, command, result) > + self.execute(command) > except Exception as e: > msg = 'Failed to execute command `{}`: {}'.format(self.command, e) > result.SetError(msg) > > def parse(self, command): > - process = target.GetProcess() > - thread = process.GetSelectedThread() > - frame = thread.GetSelectedFrame() > - > if not command: > return None > - > - ret = frame.EvaluateExpression(command) > - return ret > + return dbg.to_unsigned(dbg.eval(command)) > > @abc.abstractproperty > def command(self): > @@ -270,7 +492,7 @@ class Command(object): > """ > > @abc.abstractmethod > - def execute(self, debugger, args, result): > + def execute(self, args): > """Implementation of the command. > Subclasses override this method to implement the logic of a given > command, e.g. printing a stacktrace. The command output should be > @@ -278,6 +500,11 @@ class Command(object): > properly routed to LLDB frontend. Any unhandled exception will be > automatically transformed into proper errors. > """ > + def invoke(self, arg, from_tty): > + try: > + self.execute(arg) > + except Exception as e: > + dbg.write(e) > > > def cast(typename, value): > @@ -299,75 +526,38 @@ def cast(typename, value): > name = name[:-1].strip() > pointer_type = True > > - # Get the lldb type representation. > - t = target.FindFirstType(name) > + # Get the inferior type representation. > + t = dbg.find_type(name) > if pointer_type: > - t = t.GetPointerType() > - > - if isinstance(value, int): > - # Integer casts require some black magic for lldb to behave properly. > - if pointer_type: > - casted = target.CreateValueFromAddress( > - 'value', > - lldb.SBAddress(value, target), > - t.GetPointeeType(), > - ).address_of > - else: > - casted = target.CreateValueFromData( > - name='value', > - data=lldb.SBData.CreateDataFromInt(value, size=8), > - type=t, > - ) > - else: > - casted = value.Cast(t) > + t = dbg.type_to_pointer_type(t) > + > + casted = dbg.cast_impl(value, t, pointer_type) > > if isinstance(typename, type): > - # Wrap lldb object, if possible > + # Wrap inferior object, if possible > return typename(casted) > else: > return casted > > > -def lookup_global(name): > - return target.FindFirstGlobalVariable(name) > - > - > -def type_member(type_obj, name): > - return next((x for x in type_obj.members if x.name == name), None) > - > - > -def find_type(typename): > - return target.FindFirstType(typename) > - > - > def offsetof(typename, membername): > - type_obj = find_type(typename) > - member = type_member(type_obj, membername) > + type_obj = dbg.find_type(typename) > + member = dbg.type_member(type_obj, membername) > assert member is not None > - return member.GetOffsetInBytes() > + return dbg.type_member_offset(member) > > > def sizeof(typename): > - type_obj = find_type(typename) > - return type_obj.GetByteSize() > + type_obj = dbg.find_type(typename) > + return dbg.type_sizeof_impl(type_obj) > > > def vtou64(value): > - return value.unsigned & 0xFFFFFFFFFFFFFFFF > + return dbg.to_unsigned(value) & 0xFFFFFFFFFFFFFFFF > > > def vtoi(value): > - return value.signed > - > - > -def dbg_eval(expr): > - process = target.GetProcess() > - thread = process.GetSelectedThread() > - frame = thread.GetSelectedFrame() > - return frame.EvaluateExpression(expr) > - > - > -# }}} Debugger specific > + return dbg.to_signed(value) > > > def gcval(obj): > @@ -393,7 +583,7 @@ def gclistlen(root, end=0x0): > > > def gcringlen(root): > - if not gcref(root): > + if gcref(root) == 0: > return 0 > elif gcref(root) == gcref(gcnext(root)): > return 1 > @@ -439,7 +629,7 @@ def J(g): > J_offset = offsetof('GG_State', 'J') > return cast( > jit_StatePtr, > - vtou64(cast('char *', g)) - g_offset + J_offset, > + int(vtou64(cast('char *', g)) - g_offset + J_offset), > ) > > > @@ -451,7 +641,7 @@ def L(L=None): > # lookup a symbol for the main coroutine considering the host app > # XXX Fragile: though the loop initialization looks like a crap but it > # respects both Python 2 and Python 3. > - for lstate in [L] + list(map(lambda main: lookup_global(main), ( > + for lstate in [L] + list(map(lambda main: dbg.lookup_variable(main), ( > # LuaJIT main coro (see luajit/src/luajit.c) > 'globalL', > # Tarantool main coro (see tarantool/src/lua/init.h) > @@ -459,7 +649,7 @@ def L(L=None): > # TODO: Add more > ))): > if lstate: > - return lua_State(lstate) > + return lua_StatePtr(lstate) > > > def tou32(val): > @@ -523,9 +713,9 @@ def funcproto(func): > def strdata(obj): > try: > ptr = cast('char *', obj + 1) > - return ptr.summary > + return dbg.summary(ptr) > except UnicodeEncodeError: > - return "<luajit-lldb: error occured while rendering non-ascii slot>" > + return "<luajit_dbg: error occured while rendering non-ascii slot>" > > > def itype(o): > @@ -730,12 +920,12 @@ def frame_pc(framelink): > > > def frame_prevl(framelink): > - # We are evaluating the `frame_pc(framelink)[-1])` with lldb's > + # We are evaluating the `frame_pc(framelink)[-1])` with > # REPL, because the lldb API is faulty and it's not possible to cast > # a struct member of 32-bit type to 64-bit type without getting onto > # the next property bits, despite the fact that it's an actual value, not > # a pointer to it. > - bcins = vtou64(dbg_eval('((BCIns *)' + str(frame_pc(framelink)) + ')[-1]')) > + bcins = vtou64(dbg.eval('((BCIns *)' + str(frame_pc(framelink)) + ')[-1]')) > return framelink - (1 + LJ_FR2 + bc_a(bcins)) > > > @@ -789,12 +979,12 @@ def frames(L): > > def dump_framelink_slot_address(fr): > return '{start:{padding}}:{end:{padding}}'.format( > - start=hex(int(fr - 1)), > - end=hex(int(fr)), > + start=strx64(fr - 1), > + end=strx64(fr), > padding=len(PADDING), > ) if LJ_FR2 else '{addr:{padding}}'.format( > - addr=hex(int(fr)), > - padding=len(PADDING), > + addr=strx64(fr), > + padding=2 * len(PADDING) + 1, > ) > > > @@ -863,7 +1053,6 @@ def dump_stack(L, base=None, top=None): > nfreeslots=int((maxstack - top - 8) >> 3), > ), > ]) > - > for framelink, frametop in frames(L): > # Dump all data slots in the (framelink, top) interval. > dump.extend([ > @@ -904,9 +1093,11 @@ the type and some info related to it. > Whether the type of the given address differs from the listed above, then > error message occurs. > ''' > - def execute(self, debugger, args, result): > + command = 'lj-tv' > + > + def execute(self, args): > tvptr = TValuePtr(cast('TValue *', self.parse(args))) > - print('{}'.format(dump_tvalue(tvptr))) > + dbg.write('{}'.format(dump_tvalue(tvptr))) > > > class LJState(Command): > @@ -917,9 +1108,11 @@ The command requires no args and dumps current VM and GC states > * GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST> > * JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR> > ''' > - def execute(self, debugger, args, result): > + command = 'lj-state' > + > + def execute(self, args): > g = G(L(None)) > - print('{}'.format('\n'.join( > + dbg.write('{}'.format('\n'.join( > map(lambda t: '{} state: {}'.format(*t), { > 'VM': vm_state(g), > 'GC': gc_state(g), > @@ -936,8 +1129,10 @@ The command requires no args and dumps values of LJ_64 and LJ_GC64 > compile-time flags. These values define the sizes of host and GC > pointers respectively. > ''' > - def execute(self, debugger, args, result): > - print( > + command = 'lj-arch' > + > + def execute(self, args): > + dbg.write( > 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}' > .format( > LJ_64=LJ_64, > @@ -965,9 +1160,11 @@ The command requires no args and dumps current GC stats: > * weak: <number of weak tables (to be cleared)> > * mmudata: <number of udata|cdata to be finalized> > ''' > - def execute(self, debugger, args, result): > + command = 'lj-gc' > + > + def execute(self, args): > g = G(L(None)) > - print('GC stats: {state}\n{stats}'.format( > + dbg.write('GC stats: {state}\n{stats}'.format( > state=gc_state(g), > stats=dump_gc(g) > )) > @@ -983,9 +1180,11 @@ the payload, size in bytes and hash. > *Caveat*: Since Python 2 provides no native Unicode support, the payload > is replaced with the corresponding error when decoding fails. > ''' > - def execute(self, debugger, args, result): > + command = 'lj-str' > + > + def execute(self, args): > string_ptr = GCstrPtr(cast('GCstr *', self.parse(args))) > - print("String: {body} [{len} bytes] with hash {hash}".format( > + dbg.write("String: {body} [{len} bytes] with hash {hash}".format( > body=strdata(string_ptr), > hash=strx64(string_ptr.hash), > len=string_ptr.len, > @@ -1003,7 +1202,9 @@ The command receives a GCtab adress and dumps the table contents: > * Hash part <hsize> nodes: > <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr> > ''' > - def execute(self, debugger, args, result): > + command = 'lj-tab' > + > + def execute(self, args): > t = GCtabPtr(cast('GCtab *', self.parse(args))) > array = mref(TValuePtr, t.array) > nodes = mref(NodePtr, t.node) > @@ -1014,22 +1215,22 @@ The command receives a GCtab adress and dumps the table contents: > } > > if mt: > - print('Metatable detected: {}'.format(strx64(mt))) > + dbg.write('Metatable detected: {}'.format(strx64(mt))) > > - print('Array part: {} slots'.format(capacity['apart'])) > + dbg.write('Array part: {} slots'.format(capacity['apart'])) > for i in range(capacity['apart']): > slot = array + i > - print('{ptr}: [{index}]: {value}'.format( > + dbg.write('{ptr}: [{index}]: {value}'.format( > ptr=strx64(slot), > index=i, > value=dump_tvalue(slot) > )) > > - print('Hash part: {} nodes'.format(capacity['hpart'])) > + dbg.write('Hash part: {} nodes'.format(capacity['hpart'])) > # See hmask comment in lj_obj.h > for i in range(capacity['hpart']): > node = nodes + i > - print('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format( > + dbg.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format( > ptr=strx64(node), > key=dump_tvalue(TValuePtr(node.key.addr)), > val=dump_tvalue(TValuePtr(node.val.addr)), > @@ -1069,56 +1270,72 @@ coroutine guest stack: > > If L is ommited the main coroutine is used. > ''' > - def execute(self, debugger, args, result): > + command = 'lj-stack' > + > + def execute(self, args): > lstate = self.parse(args) > - lstate_ptr = cast('lua_State *', lstate) if coro is not None else None > - print('{}'.format(dump_stack(L(lstate_ptr)))) > + lstate_ptr = cast('lua_State *', lstate) if lstate else None > + dbg.write('{}'.format(dump_stack(L(lstate_ptr)))) > > > -def register_commands(debugger, commands): > - for command, cls in commands.items(): > - cls.command = command > - debugger.HandleCommand( > - 'command script add --overwrite --class luajit_lldb.{cls} {cmd}' > - .format( > - cls=cls.__name__, > - cmd=cls.command, > - ) > - ) > - print('{cmd} command intialized'.format(cmd=cls.command)) > +LJ_COMMANDS = [ > + LJDumpTValue, > + LJState, > + LJDumpArch, > + LJGC, > + LJDumpString, > + LJDumpTable, > + LJDumpStack, > +] > + > > +def register_commands(commands, debugger=None): > + for cls in commands: > + dbg.cmd_init(cls, debugger) > + dbg.write('{cmd} command intialized'.format(cmd=cls.command)) > > -def configure(debugger): > - global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, PADDING, LJ_TISNUM, target > - target = debugger.GetSelectedTarget() > - module = target.modules[0] > - LJ_DUALNUM = module.FindSymbol('lj_lib_checknumber') is not None > > +def configure(debugger=None): > + global PADDING, LJ_TISNUM, LJ_DUALNUM > + dbg.setup_target(debugger) > try: > - irtype_enum = target.FindFirstType('IRType').enum_members > - for member in irtype_enum: > - if member.name == 'IRT_PTR': > - LJ_64 = member.unsigned & 0x1f == IRT_P64 > - if member.name == 'IRT_PGC': > - LJ_FR2 = LJ_GC64 = member.unsigned & 0x1f == IRT_P64 > + # Try to remove the callback at first to not append duplicates to > + # gdb.events.new_objfile internal list. > + dbg.event_disconnect(load) > except Exception: > - print('luajit_lldb.py failed to load: ' > - 'no debugging symbols found for libluajit') > - return > - > - PADDING = ' ' * len(strx64((TValuePtr(L().addr)))) > - LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] > - > - > -def __lldb_init_module(debugger, internal_dict): > - configure(debugger) > - register_commands(debugger, { > - 'lj-tv': LJDumpTValue, > - 'lj-state': LJState, > - 'lj-arch': LJDumpArch, > - 'lj-gc': LJGC, > - 'lj-str': LJDumpString, > - 'lj-tab': LJDumpTable, > - 'lj-stack': LJDumpStack, > - }) > - print('luajit_lldb.py is successfully loaded') > + # Callback is not connected. > + pass > + > + try: > + # Detect whether libluajit objfile is loaded. > + dbg.eval('luaJIT_setmode') > + except Exception: > + dbg.write('luajit_dbg.py initialization is postponed ' > + 'until libluajit objfile is loaded\n') > + # Add a callback to be executed when the next objfile is loaded. > + dbg.event_connect(load) > + return False > + > + try: > + dbg.arch_init() > + except Exception: > + dbg.write('LuaJIT debug extension failed to load: ' > + 'no debugging symbols found for libluajit') > + return False > + return True > + > + > +# XXX: The dummy parameter is needed for this function to > +# work as a gdb callback. > +def load(_=None, debugger=None): > + if configure(debugger): > + register_commands(LJ_COMMANDS, debugger) > + dbg.write('LuaJIT debug extension is successfully loaded') > + > + > +def __lldb_init_module(debugger, _=None): > + load(None, debugger) > + > + > +if dbg.GDB: > + load() ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [Tarantool-patches] [PATCH luajit v3 1/2] debug: generalized extension 2023-11-22 14:46 ` Sergey Bronnikov via Tarantool-patches @ 2023-12-06 13:58 ` Maxim Kokryashkin via Tarantool-patches 0 siblings, 0 replies; 7+ messages in thread From: Maxim Kokryashkin via Tarantool-patches @ 2023-12-06 13:58 UTC (permalink / raw) To: Sergey Bronnikov; +Cc: Maksim Kokryashkin, tarantool-patches [-- Attachment #1: Type: text/plain, Size: 52046 bytes --] Hi! Thanks for the review! >Среда, 22 ноября 2023, 17:46 +03:00 от Sergey Bronnikov <sergeyb@tarantool.org>: > >Hello, Max, > >thanks for the patch! > > >extension cannot be load in GDB: Fixed, thanks! > >(gdb) source src/luajit_dbg.py >Traceback (most recent call last): > File "src/luajit_dbg.py", line 228, in <module> > dbg = Debugger() > File "src/luajit_dbg.py", line 46, in __init__ > if healthcheck(lib): > File "src/luajit_dbg.py", line 37, in <lambda> > 'lldb': lambda lib: lib.debugger is not None, >AttributeError: module 'lldb' has no attribute 'debugger' >(gdb) > >[0] ~/sources/MRG/tarantool/third_party/luajit$ gdb --version >GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1 > >On 11/10/23 23:16, Maksim Kokryashkin wrote: >> From: Maxim Kokryashkin < m.kokryashkin@tarantool.org > >> >> This patch joins the LLDB and GDB LuaJIT debugging extensions >> into one, so now the extension logic can be debugger-agnostic. >> To do that, an adapter class is introduced, and all of the >> debugger-specific behavior is encapsulated there. The extension >> auto-detects the debugger it was loaded into and selects the >> correct low-level logic implementation. >> --- >> src/luajit-gdb.py | 885 -------------------------- >> src/{luajit_lldb.py => luajit_dbg.py} | 617 ++++++++++++------ >> 2 files changed, 417 insertions(+), 1085 deletions(-) >> delete mode 100644 src/luajit-gdb.py >> rename src/{luajit_lldb.py => luajit_dbg.py} (63%) >> >> diff --git a/src/luajit-gdb.py b/src/luajit-gdb.py >> deleted file mode 100644 >> index 5eaf250f..00000000 >> --- a/src/luajit-gdb.py >> +++ /dev/null >> @@ -1,885 +0,0 @@ >> -# GDB extension for LuaJIT post-mortem analysis. >> -# To use, just put 'source <path-to-repo>/src/luajit-gdb.py' in gdb. >> - >> -import re >> -import gdb >> -import sys >> - >> -# make script compatible with the ancient Python {{{ >> - >> - >> -LEGACY = re.match(r'^2\.', sys.version) >> - >> -if LEGACY: >> - CONNECTED = False >> - int = long >> - range = xrange >> - >> - >> -# }}} >> - >> - >> -gtype_cache = {} >> - >> - >> -def gtype(typestr): >> - global gtype_cache >> - if typestr in gtype_cache: >> - return gtype_cache[typestr] >> - >> - m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr) >> - >> - gtype = gdb.lookup_type(typestr) if m is None \ >> - else gdb.lookup_type(m.group(1)).pointer() >> - >> - gtype_cache[typestr] = gtype >> - return gtype >> - >> - >> -def cast(typestr, val): >> - return gdb.Value(val).cast(gtype(typestr)) >> - >> - >> -def lookup(symbol): >> - variable, _ = gdb.lookup_symbol(symbol) >> - return variable.value() if variable else None >> - >> - >> -def parse_arg(arg): >> - if not arg: >> - return None >> - >> - ret = gdb.parse_and_eval(arg) >> - >> - if not ret: >> - raise gdb.GdbError('table argument empty') >> - >> - return ret >> - >> - >> -def tou64(val): >> - return cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF >> - >> - >> -def tou32(val): >> - return cast('uint32_t', val) & 0xFFFFFFFF >> - >> - >> -def i2notu32(val): >> - return ~int(val) & 0xFFFFFFFF >> - >> - >> -def strx64(val): >> - return re.sub('L?$', '', >> - hex(int(cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF))) >> - >> - >> -# Types {{{ >> - >> - >> -LJ_T = { >> - 'NIL': i2notu32(0), >> - 'FALSE': i2notu32(1), >> - 'TRUE': i2notu32(2), >> - 'LIGHTUD': i2notu32(3), >> - 'STR': i2notu32(4), >> - 'UPVAL': i2notu32(5), >> - 'THREAD': i2notu32(6), >> - 'PROTO': i2notu32(7), >> - 'FUNC': i2notu32(8), >> - 'TRACE': i2notu32(9), >> - 'CDATA': i2notu32(10), >> - 'TAB': i2notu32(11), >> - 'UDATA': i2notu32(12), >> - 'NUMX': i2notu32(13), >> -} >> - >> - >> -def typenames(value): >> - return { >> - LJ_T[k]: 'LJ_T' + k for k in LJ_T.keys() >> - }.get(int(value), 'LJ_TINVALID') >> - >> - >> -# }}} >> - >> -# Frames {{{ >> - >> - >> -FRAME_TYPE = 0x3 >> -FRAME_P = 0x4 >> -FRAME_TYPEP = FRAME_TYPE | FRAME_P >> - >> -FRAME = { >> - 'LUA': 0x0, >> - 'C': 0x1, >> - 'CONT': 0x2, >> - 'VARG': 0x3, >> - 'LUAP': 0x4, >> - 'CP': 0x5, >> - 'PCALL': 0x6, >> - 'PCALLH': 0x7, >> -} >> - >> - >> -def frametypes(ft): >> - return { >> - FRAME['LUA']: 'L', >> - FRAME['C']: 'C', >> - FRAME['CONT']: 'M', >> - FRAME['VARG']: 'V', >> - }.get(ft, '?') >> - >> - >> -def bc_a(ins): >> - return (ins >> 8) & 0xff >> - >> - >> -def frame_ftsz(framelink): >> - return cast('ptrdiff_t', framelink['ftsz'] if LJ_FR2 >> - else framelink['fr']['tp']['ftsz']) >> - >> - >> -def frame_pc(framelink): >> - return cast('BCIns *', frame_ftsz(framelink)) if LJ_FR2 \ >> - else mref('BCIns *', framelink['fr']['tp']['pcr']) >> - >> - >> -def frame_prevl(framelink): >> - return framelink - (1 + LJ_FR2 + bc_a(frame_pc(framelink)[-1])) >> - >> - >> -def frame_ispcall(framelink): >> - return (frame_ftsz(framelink) & FRAME['PCALL']) == FRAME['PCALL'] >> - >> - >> -def frame_sized(framelink): >> - return (frame_ftsz(framelink) & ~FRAME_TYPEP) >> - >> - >> -def frame_prevd(framelink): >> - return cast('TValue *', cast('char *', framelink) - frame_sized(framelink)) >> - >> - >> -def frame_type(framelink): >> - return frame_ftsz(framelink) & FRAME_TYPE >> - >> - >> -def frame_typep(framelink): >> - return frame_ftsz(framelink) & FRAME_TYPEP >> - >> - >> -def frame_islua(framelink): >> - return frametypes(int(frame_type(framelink))) == 'L' \ >> - and int(frame_ftsz(framelink)) > 0 >> - >> - >> -def frame_prev(framelink): >> - return frame_prevl(framelink) if frame_islua(framelink) \ >> - else frame_prevd(framelink) >> - >> - >> -def frame_sentinel(L): >> - return mref('TValue *', L['stack']) + LJ_FR2 >> - >> - >> -# }}} >> - >> -# Const {{{ >> - >> - >> -LJ_64 = None >> -LJ_GC64 = None >> -LJ_FR2 = None >> -LJ_DUALNUM = None >> - >> -LJ_GCVMASK = ((1 << 47) - 1) >> -LJ_TISNUM = None >> -PADDING = None >> - >> -# These constants are meaningful only for 'LJ_64' mode. >> -LJ_LIGHTUD_BITS_SEG = 8 >> -LJ_LIGHTUD_BITS_LO = 47 - LJ_LIGHTUD_BITS_SEG >> -LIGHTUD_SEG_MASK = (1 << LJ_LIGHTUD_BITS_SEG) - 1 >> -LIGHTUD_LO_MASK = (1 << LJ_LIGHTUD_BITS_LO) - 1 >> - >> - >> -# }}} >> - >> - >> -def itype(o): >> - return cast('uint32_t', o['it64'] >> 47) if LJ_GC64 else o['it'] >> - >> - >> -def mref(typename, obj): >> - return cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32']) >> - >> - >> -def gcref(obj): >> - return cast('GCobj *', obj['gcptr64'] if LJ_GC64 >> - else cast('uintptr_t', obj['gcptr32'])) >> - >> - >> -def gcval(obj): >> - return cast('GCobj *', obj['gcptr64'] & LJ_GCVMASK if LJ_GC64 >> - else cast('uintptr_t', obj['gcptr32'])) >> - >> - >> -def gcnext(obj): >> - return gcref(obj)['gch']['nextgc'] >> - >> - >> -def L(L=None): >> - # lookup a symbol for the main coroutine considering the host app >> - # XXX Fragile: though the loop initialization looks like a crap but it >> - # respects both Python 2 and Python 3. >> - for lstate in [L] + list(map(lambda main: lookup(main), ( >> - # LuaJIT main coro (see luajit/src/luajit.c) >> - 'globalL', >> - # Tarantool main coro (see tarantool/src/lua/init.h) >> - 'tarantool_L', >> - # TODO: Add more >> - ))): >> - if lstate: >> - return cast('lua_State *', lstate) >> - >> - >> -def G(L): >> - return mref('global_State *', L['glref']) >> - >> - >> -def J(g): >> - typeGG = gtype('GG_State') >> - >> - return cast('jit_State *', int(cast('char *', g)) >> - - int(typeGG['g'].bitpos / 8) >> - + int(typeGG['J'].bitpos / 8)) >> - >> - >> -def vm_state(g): >> - return { >> - i2notu32(0): 'INTERP', >> - i2notu32(1): 'LFUNC', >> - i2notu32(2): 'FFUNC', >> - i2notu32(3): 'CFUNC', >> - i2notu32(4): 'GC', >> - i2notu32(5): 'EXIT', >> - i2notu32(6): 'RECORD', >> - i2notu32(7): 'OPT', >> - i2notu32(8): 'ASM', >> - }.get(int(tou32(g['vmstate'])), 'TRACE') >> - >> - >> -def gc_state(g): >> - return { >> - 0: 'PAUSE', >> - 1: 'PROPAGATE', >> - 2: 'ATOMIC', >> - 3: 'SWEEPSTRING', >> - 4: 'SWEEP', >> - 5: 'FINALIZE', >> - 6: 'LAST', >> - }.get(int(g['gc']['state']), 'INVALID') >> - >> - >> -def jit_state(g): >> - return { >> - 0: 'IDLE', >> - 0x10: 'ACTIVE', >> - 0x11: 'RECORD', >> - 0x12: 'START', >> - 0x13: 'END', >> - 0x14: 'ASM', >> - 0x15: 'ERR', >> - }.get(int(J(g)['state']), 'INVALID') >> - >> - >> -def tvisint(o): >> - return LJ_DUALNUM and itype(o) == LJ_TISNUM >> - >> - >> -def tvisnumber(o): >> - return itype(o) <= LJ_TISNUM >> - >> - >> -def tvislightud(o): >> - if LJ_64 and not LJ_GC64: >> - return (cast('int32_t', itype(o)) >> 15) == -2 >> - else: >> - return itype(o) == LJ_T['LIGHTUD'] >> - >> - >> -def strdata(obj): >> - # String is printed with pointer to it, thanks to gdb. Just strip it. >> - try: >> - return str(cast('char *', cast('GCstr *', obj) + 1))[len(PADDING):] >> - except UnicodeEncodeError: >> - return "<luajit-gdb: error occured while rendering non-ascii slot>" >> - >> - >> -def itypemap(o): >> - if LJ_64 and not LJ_GC64: >> - return LJ_T['NUMX'] if tvisnumber(o) \ >> - else LJ_T['LIGHTUD'] if tvislightud(o) \ >> - else itype(o) >> - else: >> - return LJ_T['NUMX'] if tvisnumber(o) else itype(o) >> - >> - >> -def funcproto(func): >> - assert func['ffid'] == 0 >> - >> - return cast('GCproto *', >> - mref('char *', func['pc']) - gdb.lookup_type('GCproto').sizeof) >> - >> - >> -def gclistlen(root, end=0x0): >> - count = 0 >> - while (gcref(root) != end): >> - count += 1 >> - root = gcnext(root) >> - return count >> - >> - >> -def gcringlen(root): >> - if not gcref(root): >> - return 0 >> - elif gcref(root) == gcref(gcnext(root)): >> - return 1 >> - else: >> - return 1 + gclistlen(gcnext(root), gcref(root)) >> - >> - >> -gclen = { >> - 'root': gclistlen, >> - 'gray': gclistlen, >> - 'grayagain': gclistlen, >> - 'weak': gclistlen, >> - # XXX: gc.mmudata is a ring-list. >> - 'mmudata': gcringlen, >> -} >> - >> - >> -# The generator that implements frame iterator. >> -# Every frame is represented as a tuple of framelink and frametop. >> -def frames(L): >> - frametop = L['top'] >> - framelink = L['base'] - 1 >> - framelink_sentinel = frame_sentinel(L) >> - while True: >> - yield framelink, frametop >> - frametop = framelink - (1 + LJ_FR2) >> - if framelink <= framelink_sentinel: >> - break >> - framelink = frame_prev(framelink) >> - >> - >> -def lightudV(tv): >> - if LJ_64: >> - u = int(tv['u64']) >> - # lightudseg macro expanded. >> - seg = (u >> LJ_LIGHTUD_BITS_LO) & LIGHTUD_SEG_MASK >> - segmap = mref('uint32_t *', G(L(None))['gc']['lightudseg']) >> - # lightudlo macro expanded. >> - return (int(segmap[seg]) << 32) | (u & LIGHTUD_LO_MASK) >> - else: >> - return gcval(tv['gcr']) >> - >> - >> -# Dumpers {{{ >> - >> - >> -def dump_lj_tnil(tv): >> - return 'nil' >> - >> - >> -def dump_lj_tfalse(tv): >> - return 'false' >> - >> - >> -def dump_lj_ttrue(tv): >> - return 'true' >> - >> - >> -def dump_lj_tlightud(tv): >> - return 'light userdata @ {}'.format(strx64(lightudV(tv))) >> - >> - >> -def dump_lj_tstr(tv): >> - return 'string {body} @ {address}'.format( >> - body=strdata(gcval(tv['gcr'])), >> - address=strx64(gcval(tv['gcr'])) >> - ) >> - >> - >> -def dump_lj_tupval(tv): >> - return 'upvalue @ {}'.format(strx64(gcval(tv['gcr']))) >> - >> - >> -def dump_lj_tthread(tv): >> - return 'thread @ {}'.format(strx64(gcval(tv['gcr']))) >> - >> - >> -def dump_lj_tproto(tv): >> - return 'proto @ {}'.format(strx64(gcval(tv['gcr']))) >> - >> - >> -def dump_lj_tfunc(tv): >> - func = cast('struct GCfuncC *', gcval(tv['gcr'])) >> - ffid = func['ffid'] >> - >> - if ffid == 0: >> - pt = funcproto(func) >> - return 'Lua function @ {addr}, {nups} upvalues, {chunk}:{line}'.format( >> - addr=strx64(func), >> - nups=int(func['nupvalues']), >> - chunk=strdata(cast('GCstr *', gcval(pt['chunkname']))), >> - line=pt['firstline'] >> - ) >> - elif ffid == 1: >> - return 'C function @ {}'.format(strx64(func['f'])) >> - else: >> - return 'fast function #{}'.format(int(ffid)) >> - >> - >> -def dump_lj_ttrace(tv): >> - trace = cast('struct GCtrace *', gcval(tv['gcr'])) >> - return 'trace {traceno} @ {addr}'.format( >> - traceno=strx64(trace['traceno']), >> - addr=strx64(trace) >> - ) >> - >> - >> -def dump_lj_tcdata(tv): >> - return 'cdata @ {}'.format(strx64(gcval(tv['gcr']))) >> - >> - >> -def dump_lj_ttab(tv): >> - table = cast('GCtab *', gcval(tv['gcr'])) >> - return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format( >> - gcr=strx64(table), >> - asize=table['asize'], >> - hmask=strx64(table['hmask']), >> - ) >> - >> - >> -def dump_lj_tudata(tv): >> - return 'userdata @ {}'.format(strx64(gcval(tv['gcr']))) >> - >> - >> -def dump_lj_tnumx(tv): >> - if tvisint(tv): >> - return 'integer {}'.format(cast('int32_t', tv['i'])) >> - else: >> - return 'number {}'.format(cast('double', tv['n'])) >> - >> - >> -def dump_lj_invalid(tv): >> - return 'not valid type @ {}'.format(strx64(gcval(tv['gcr']))) >> - >> - >> -# }}} >> - >> - >> -dumpers = { >> - 'LJ_TNIL': dump_lj_tnil, >> - 'LJ_TFALSE': dump_lj_tfalse, >> - 'LJ_TTRUE': dump_lj_ttrue, >> - 'LJ_TLIGHTUD': dump_lj_tlightud, >> - 'LJ_TSTR': dump_lj_tstr, >> - 'LJ_TUPVAL': dump_lj_tupval, >> - 'LJ_TTHREAD': dump_lj_tthread, >> - 'LJ_TPROTO': dump_lj_tproto, >> - 'LJ_TFUNC': dump_lj_tfunc, >> - 'LJ_TTRACE': dump_lj_ttrace, >> - 'LJ_TCDATA': dump_lj_tcdata, >> - 'LJ_TTAB': dump_lj_ttab, >> - 'LJ_TUDATA': dump_lj_tudata, >> - 'LJ_TNUMX': dump_lj_tnumx, >> -} >> - >> - >> -def dump_tvalue(tvalue): >> - return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue) >> - >> - >> -def dump_framelink_slot_address(fr): >> - return '{}:{}'.format(fr - 1, fr) if LJ_FR2 \ >> - else '{}'.format(fr) + PADDING >> - >> - >> -def dump_framelink(L, fr): >> - if fr == frame_sentinel(L): >> - return '{addr} [S ] FRAME: dummy L'.format( >> - addr=dump_framelink_slot_address(fr), >> - ) >> - return '{addr} [ ] FRAME: [{pp}] delta={d}, {f}'.format( >> - addr=dump_framelink_slot_address(fr), >> - pp='PP' if frame_ispcall(fr) else '{frname}{p}'.format( >> - frname=frametypes(int(frame_type(fr))), >> - p='P' if frame_typep(fr) & FRAME_P else '' >> - ), >> - d=cast('TValue *', fr) - cast('TValue *', frame_prev(fr)), >> - f=dump_lj_tfunc(fr - LJ_FR2), >> - ) >> - >> - >> -def dump_stack_slot(L, slot, base=None, top=None): >> - base = base or L['base'] >> - top = top or L['top'] >> - >> - return '{addr}{padding} [ {B}{T}{M}] VALUE: {value}'.format( >> - addr=strx64(slot), >> - padding=PADDING, >> - B='B' if slot == base else ' ', >> - T='T' if slot == top else ' ', >> - M='M' if slot == mref('TValue *', L['maxstack']) else ' ', >> - value=dump_tvalue(slot), >> - ) >> - >> - >> -def dump_stack(L, base=None, top=None): >> - base = base or L['base'] >> - top = top or L['top'] >> - stack = mref('TValue *', L['stack']) >> - maxstack = mref('TValue *', L['maxstack']) >> - red = 5 + 2 * LJ_FR2 >> - >> - dump = [ >> - '{padding} Red zone: {nredslots: >2} slots {padding}'.format( >> - padding='-' * len(PADDING), >> - nredslots=red, >> - ), >> - ] >> - dump.extend([ >> - dump_stack_slot(L, maxstack + offset, base, top) >> - for offset in range(red, 0, -1) # noqa: E131 >> - ]) >> - dump.extend([ >> - '{padding} Stack: {nstackslots: >5} slots {padding}'.format( >> - padding='-' * len(PADDING), >> - nstackslots=int((tou64(maxstack) - tou64(stack)) >> 3), >> - ), >> - dump_stack_slot(L, maxstack, base, top), >> - '{start}:{end} [ ] {nfreeslots} slots: Free stack slots'.format( >> - start=strx64(top + 1), >> - end=strx64(maxstack - 1), >> - nfreeslots=int((tou64(maxstack) - tou64(top) - 8) >> 3), >> - ), >> - ]) >> - >> - for framelink, frametop in frames(L): >> - # Dump all data slots in the (framelink, top) interval. >> - dump.extend([ >> - dump_stack_slot(L, framelink + offset, base, top) >> - for offset in range(frametop - framelink, 0, -1) # noqa: E131 >> - ]) >> - # Dump frame slot (2 slots in case of GC64). >> - dump.append(dump_framelink(L, framelink)) >> - >> - return '\n'.join(dump) >> - >> - >> -def dump_gc(g): >> - gc = g['gc'] >> - stats = ['{key}: {value}'.format(key=f, value=gc[f]) for f in ( >> - 'total', 'threshold', 'debt', 'estimate', 'stepmul', 'pause' >> - )] >> - >> - stats += ['sweepstr: {sweepstr}/{strmask}'.format( >> - sweepstr=gc['sweepstr'], >> - # String hash mask (size of hash table - 1). >> - strmask=g['strmask'] + 1, >> - )] >> - >> - stats += ['{key}: {number} objects'.format( >> - key=stat, >> - number=handler(gc[stat]) >> - ) for stat, handler in gclen.items()] >> - >> - return '\n'.join(map(lambda s: '\t' + s, stats)) >> - >> - >> -class LJBase(gdb.Command): >> - >> - def __init__(self, name): >> - # XXX Fragile: though the command initialization looks like a crap but >> - # it respects both Python 2 and Python 3. >> - gdb.Command.__init__(self, name, gdb.COMMAND_DATA) >> - gdb.write('{} command initialized\n'.format(name)) >> - >> - >> -class LJDumpArch(LJBase): >> - ''' >> -lj-arch >> - >> -The command requires no args and dumps values of LJ_64 and LJ_GC64 >> -compile-time flags. These values define the sizes of host and GC >> -pointers respectively. >> - ''' >> - >> - def invoke(self, arg, from_tty): >> - gdb.write( >> - 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}\n' >> - .format( >> - LJ_64=LJ_64, >> - LJ_GC64=LJ_GC64, >> - LJ_DUALNUM=LJ_DUALNUM >> - ) >> - ) >> - >> - >> -class LJDumpTValue(LJBase): >> - ''' >> -lj-tv <TValue *> >> - >> -The command receives a pointer to <tv> (TValue address) and dumps >> -the type and some info related to it. >> - >> -* LJ_TNIL: nil >> -* LJ_TFALSE: false >> -* LJ_TTRUE: true >> -* LJ_TLIGHTUD: light userdata @ <gcr> >> -* LJ_TSTR: string <string payload> @ <gcr> >> -* LJ_TUPVAL: upvalue @ <gcr> >> -* LJ_TTHREAD: thread @ <gcr> >> -* LJ_TPROTO: proto @ <gcr> >> -* LJ_TFUNC: <LFUNC|CFUNC|FFUNC> >> - <LFUNC>: Lua function @ <gcr>, <nupvals> upvalues, <chunk:line> >> - <CFUNC>: C function <mcode address> >> - <FFUNC>: fast function #<ffid> >> -* LJ_TTRACE: trace <traceno> @ <gcr> >> -* LJ_TCDATA: cdata @ <gcr> >> -* LJ_TTAB: table @ <gcr> (asize: <asize>, hmask: <hmask>) >> -* LJ_TUDATA: userdata @ <gcr> >> -* LJ_TNUMX: number <numeric payload> >> - >> -Whether the type of the given address differs from the listed above, then >> -error message occurs. >> - ''' >> - >> - def invoke(self, arg, from_tty): >> - tv = cast('TValue *', parse_arg(arg)) >> - gdb.write('{}\n'.format(dump_tvalue(tv))) >> - >> - >> -class LJDumpString(LJBase): >> - ''' >> -lj-str <GCstr *> >> - >> -The command receives a <gcr> of the corresponding GCstr object and dumps >> -the payload, size in bytes and hash. >> - >> -*Caveat*: Since Python 2 provides no native Unicode support, the payload >> -is replaced with the corresponding error when decoding fails. >> - ''' >> - >> - def invoke(self, arg, from_tty): >> - string = cast('GCstr *', parse_arg(arg)) >> - gdb.write("String: {body} [{len} bytes] with hash {hash}\n".format( >> - body=strdata(string), >> - hash=strx64(string['hash']), >> - len=string['len'], >> - )) >> - >> - >> -class LJDumpTable(LJBase): >> - ''' >> -lj-tab <GCtab *> >> - >> -The command receives a GCtab adress and dumps the table contents: >> -* Metatable address whether the one is set >> -* Array part <asize> slots: >> - <aslot ptr>: [<index>]: <tv> >> -* Hash part <hsize> nodes: >> - <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr> >> - ''' >> - >> - def invoke(self, arg, from_tty): >> - t = cast('GCtab *', parse_arg(arg)) >> - array = mref('TValue *', t['array']) >> - nodes = mref('struct Node *', t['node']) >> - mt = gcval(t['metatable']) >> - capacity = { >> - 'apart': int(t['asize']), >> - 'hpart': int(t['hmask'] + 1) if t['hmask'] > 0 else 0 >> - } >> - >> - if mt != 0: >> - gdb.write('Metatable detected: {}\n'.format(strx64(mt))) >> - >> - gdb.write('Array part: {} slots\n'.format(capacity['apart'])) >> - for i in range(capacity['apart']): >> - slot = array + i >> - gdb.write('{ptr}: [{index}]: {value}\n'.format( >> - ptr=slot, >> - index=i, >> - value=dump_tvalue(slot) >> - )) >> - >> - gdb.write('Hash part: {} nodes\n'.format(capacity['hpart'])) >> - # See hmask comment in lj_obj.h >> - for i in range(capacity['hpart']): >> - node = nodes + i >> - gdb.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}\n'.format( >> - ptr=node, >> - key=dump_tvalue(node['key']), >> - val=dump_tvalue(node['val']), >> - n=mref('struct Node *', node['next']) >> - )) >> - >> - >> -class LJDumpStack(LJBase): >> - ''' >> -lj-stack [<lua_State *>] >> - >> -The command receives a lua_State address and dumps the given Lua >> -coroutine guest stack: >> - >> -<slot ptr> [<slot attributes>] <VALUE|FRAME> >> - >> -* <slot ptr>: guest stack slot address >> -* <slot attributes>: >> - - S: Bottom of the stack (the slot L->stack points to) >> - - B: Base of the current guest frame (the slot L->base points to) >> - - T: Top of the current guest frame (the slot L->top points to) >> - - M: Last slot of the stack (the slot L->maxstack points to) >> -* <VALUE>: see help lj-tv for more info >> -* <FRAME>: framelink slot differs from the value slot: it contains info >> - related to the function being executed within this guest frame, its >> - type and link to the parent guest frame >> - [<frame type>] delta=<slots in frame>, <lj-tv for LJ_TFUNC slot> >> - - <frame type>: >> - + L: VM performs a call as a result of bytecode execution >> - + C: VM performs a call as a result of lj_vm_call >> - + M: VM performs a call to a metamethod as a result of bytecode >> - execution >> - + V: Variable-length frame for storing arguments of a variadic >> - function >> - + CP: Protected C frame >> - + PP: VM performs a call as a result of executinig pcall or xpcall >> - >> -If L is ommited the main coroutine is used. >> - ''' >> - >> - def invoke(self, arg, from_tty): >> - gdb.write('{}\n'.format(dump_stack(L(parse_arg(arg))))) >> - >> - >> -class LJState(LJBase): >> - ''' >> -lj-state >> -The command requires no args and dumps current VM and GC states >> -* VM state: <INTERP|C|GC|EXIT|RECORD|OPT|ASM|TRACE> >> -* GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST> >> -* JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR> >> - ''' >> - >> - def invoke(self, arg, from_tty): >> - g = G(L(None)) >> - gdb.write('{}\n'.format('\n'.join( >> - map(lambda t: '{} state: {}'.format(*t), { >> - 'VM': vm_state(g), >> - 'GC': gc_state(g), >> - 'JIT': jit_state(g), >> - }.items()) >> - ))) >> - >> - >> -class LJGC(LJBase): >> - ''' >> -lj-gc >> - >> -The command requires no args and dumps current GC stats: >> -* total: <total number of allocated bytes in GC area> >> -* threshold: <limit when gc step is triggered> >> -* debt: <how much GC is behind schedule> >> -* estimate: <estimate of memory actually in use> >> -* stepmul: <incremental GC step granularity> >> -* pause: <pause between successive GC cycles> >> -* sweepstr: <sweep position in string table> >> -* root: <number of all collectable objects> >> -* gray: <number of gray objects> >> -* grayagain: <number of objects for atomic traversal> >> -* weak: <number of weak tables (to be cleared)> >> -* mmudata: <number of udata|cdata to be finalized> >> - ''' >> - >> - def invoke(self, arg, from_tty): >> - g = G(L(None)) >> - gdb.write('GC stats: {state}\n{stats}\n'.format( >> - state=gc_state(g), >> - stats=dump_gc(g) >> - )) >> - >> - >> -def init(commands): >> - global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, LJ_TISNUM, PADDING >> - >> - # XXX Fragile: though connecting the callback looks like a crap but it >> - # respects both Python 2 and Python 3 (see #4828). >> - def connect(callback): >> - if LEGACY: >> - global CONNECTED >> - CONNECTED = True >> - gdb.events.new_objfile.connect(callback) >> - >> - # XXX Fragile: though disconnecting the callback looks like a crap but it >> - # respects both Python 2 and Python 3 (see #4828). >> - def disconnect(callback): >> - if LEGACY: >> - global CONNECTED >> - if not CONNECTED: >> - return >> - CONNECTED = False >> - gdb.events.new_objfile.disconnect(callback) >> - >> - try: >> - # Try to remove the callback at first to not append duplicates to >> - # gdb.events.new_objfile internal list. >> - disconnect(load) >> - except Exception: >> - # Callback is not connected. >> - pass >> - >> - try: >> - # Detect whether libluajit objfile is loaded. >> - gdb.parse_and_eval('luaJIT_setmode') >> - except Exception: >> - gdb.write('luajit-gdb.py initialization is postponed ' >> - 'until libluajit objfile is loaded\n') >> - # Add a callback to be executed when the next objfile is loaded. >> - connect(load) >> - return >> - >> - try: >> - LJ_64 = str(gdb.parse_and_eval('IRT_PTR')) == 'IRT_P64' >> - LJ_FR2 = LJ_GC64 = str(gdb.parse_and_eval('IRT_PGC')) == 'IRT_P64' >> - LJ_DUALNUM = gdb.lookup_global_symbol('lj_lib_checknumber') is not None >> - except Exception: >> - gdb.write('luajit-gdb.py failed to load: ' >> - 'no debugging symbols found for libluajit\n') >> - return >> - >> - for name, command in commands.items(): >> - command(name) >> - >> - PADDING = ' ' * len(':' + hex((1 << (47 if LJ_GC64 else 32)) - 1)) >> - LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] >> - >> - gdb.write('luajit-gdb.py is successfully loaded\n') >> - >> - >> -def load(event=None): >> - init({ >> - 'lj-arch': LJDumpArch, >> - 'lj-tv': LJDumpTValue, >> - 'lj-str': LJDumpString, >> - 'lj-tab': LJDumpTable, >> - 'lj-stack': LJDumpStack, >> - 'lj-state': LJState, >> - 'lj-gc': LJGC, >> - }) >> - >> - >> -load(None) >> diff --git a/src/luajit_lldb.py b/src/luajit_dbg.py >> similarity index 63% >> rename from src/luajit_lldb.py >> rename to src/luajit_dbg.py >> index ef0986cc..cb46b473 100644 >> --- a/src/luajit_lldb.py >> +++ b/src/luajit_dbg.py >> @@ -1,10 +1,231 @@ >> -# LLDB extension for LuaJIT post-mortem analysis. >> -# To use, just put 'command script import <path-to-repo>/src/luajit_lldb.py' >> -# in lldb. >> +# Debug extension for LuaJIT post-mortem analysis. >> +# To use in LLDB: 'command script import <path-to-repo>/src/luajit_dbg.py' >> +# To use in GDB: 'source <path-to-repo>/src/luajit_dbg.py' >> >> import abc >> import re >> -import lldb >> +import sys >> +import types >> + >> +from importlib import import_module >> + >> +# make script compatible with the ancient Python {{{ >> + >> + >> +LEGACY = re.match(r'^2\.', sys.version) >> + >> +if LEGACY: >> + CONNECTED = False >> + int = long >> + range = xrange >> + >> + >> +def is_integer_type(val): >> + return isinstance(val, int) or (LEGACY and isinstance(val, types.IntType)) >> + >> + >> +# }}} >> + >> + >> +class Debugger(object): >> + def __init__(self): >> + self.GDB = False >> + self.LLDB = False >> + >> + debuggers = { >> + 'gdb': lambda lib: True, >> + 'lldb': lambda lib: lib.debugger is not None, >> + } >> + for name, healthcheck in debuggers.items(): >> + lib = None >> + try: >> + lib = import_module(name) >> + except ImportError: >> + continue >> + >> + if healthcheck(lib): >> + setattr(self, name.upper(), True) >> + globals()[name] = lib >> + self.name = name >> + >> + assert self.LLDB != self.GDB >> + >> + def setup_target(self, debugger): >> + global target >> + if self.LLDB: >> + target = debugger.GetSelectedTarget() >> + >> + def write(self, msg): >> + if self.LLDB: >> + print(msg) >> + else: >> + gdb.write(msg + '\n') >> + >> + def cmd_init(self, cmd_cls, debugger=None): >> + if self.LLDB: >> + debugger.HandleCommand( >> + 'command script add --overwrite --class ' >> + 'luajit_dbg.{cls} {cmd}' >> + .format( >> + cls=cmd_cls.__name__, >> + cmd=cmd_cls.command, >> + ) >> + ) >> + else: >> + cmd_cls() >> + >> + def event_connect(self, callback): >> + if not self.LLDB: >> + # XXX Fragile: though connecting the callback looks like a crap but >> + # it respects both Python 2 and Python 3 (see #4828). >> + if LEGACY: >> + global CONNECTED >> + CONNECTED = True >> + gdb.events.new_objfile.connect(callback) >> + >> + def event_disconnect(self, callback): >> + if not self.LLDB: >> + # XXX Fragile: though disconnecting the callback looks like a crap >> + # but it respects both Python 2 and Python 3 (see #4828). >> + if LEGACY: >> + global CONNECTED >> + if not CONNECTED: >> + return >> + CONNECTED = False >> + gdb.events.new_objfile.disconnect(callback) >> + >> + def lookup_variable(self, name): >> + if self.LLDB: >> + return target.FindFirstGlobalVariable(name) >> + else: >> + variable, _ = gdb.lookup_symbol(name) >> + return variable.value() if variable else None >> + >> + def lookup_symbol(self, sym): >> + if self.LLDB: >> + return target.modules[0].FindSymbol(sym) >> + else: >> + return gdb.lookup_global_symbol(sym) >> + >> + def to_unsigned(self, val): >> + return val.unsigned if self.LLDB else int(val) >> + >> + def to_signed(self, val): >> + return val.signed if self.LLDB else int(val) >> + >> + def to_str(self, val): >> + return val.value if self.LLDB else str(val) >> + >> + def find_type(self, typename): >> + if self.LLDB: >> + return target.FindFirstType(typename) >> + else: >> + return gdb.lookup_type(typename) >> + >> + def type_to_pointer_type(self, tp): >> + if self.LLDB: >> + return tp.GetPointerType() >> + else: >> + return tp.pointer() >> + >> + def cast_impl(self, value, t, pointer_type): >> + if self.LLDB: >> + if is_integer_type(value): >> + # Integer casts require some black magic >> + # for lldb to behave properly. >> + if pointer_type: >> + return target.CreateValueFromAddress( >> + 'value', >> + lldb.SBAddress(value, target), >> + t.GetPointeeType(), >> + ).address_of >> + else: >> + return target.CreateValueFromData( >> + name='value', >> + data=lldb.SBData.CreateDataFromInt(value, size=8), >> + type=t, >> + ) >> + else: >> + return value.Cast(t) >> + else: >> + return gdb.Value(value).cast(t) >> + >> + def dereference(self, val): >> + if self.LLDB: >> + return val.Dereference() >> + else: >> + return val.dereference() >> + >> + def eval(self, expression): >> + if self.LLDB: >> + process = target.GetProcess() >> + thread = process.GetSelectedThread() >> + frame = thread.GetSelectedFrame() >> + >> + if not expression: >> + return None >> + >> + return frame.EvaluateExpression(expression) >> + else: >> + return gdb.parse_and_eval(expression) >> + >> + def type_sizeof_impl(self, tp): >> + if self.LLDB: >> + return tp.GetByteSize() >> + else: >> + return tp.sizeof >> + >> + def summary(self, val): >> + if self.LLDB: >> + return val.summary >> + else: >> + return str(val)[len(PADDING):].strip() >> + >> + def type_member(self, type_obj, name): >> + if self.LLDB: >> + return next((x for x in type_obj.members if x.name == name), None) >> + else: >> + return type_obj[name] >> + >> + def type_member_offset(self, member): >> + if self.LLDB: >> + return member.GetOffsetInBytes() >> + else: >> + return member.bitpos / 8 >> + >> + def get_member(self, value, member_name): >> + if self.LLDB: >> + return value.GetChildMemberWithName(member_name) >> + else: >> + return value[member_name] >> + >> + def address_of(self, value): >> + if self.LLDB: >> + return value.address_of >> + else: >> + return value.address >> + >> + def arch_init(self): >> + global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, PADDING, LJ_TISNUM, target >> + if self.LLDB: >> + irtype_enum = dbg.find_type('IRType').enum_members >> + for member in irtype_enum: >> + if member.name == 'IRT_PTR': >> + LJ_64 = dbg.to_unsigned(member) & 0x1f == IRT_P64 >> + if member.name == 'IRT_PGC': >> + LJ_GC64 = dbg.to_unsigned(member) & 0x1f == IRT_P64 >> + else: >> + LJ_64 = str(dbg.eval('IRT_PTR')) == 'IRT_P64' >> + LJ_GC64 = str(dbg.eval('IRT_PGC')) == 'IRT_P64' >> + >> + LJ_FR2 = LJ_GC64 >> + LJ_DUALNUM = dbg.lookup_symbol('lj_lib_checknumber') is not None >> + # Two extra characters are required to fit in the `0x` part. >> + PADDING = ' ' * len(strx64(L())) >> + LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] >> + >> + >> +dbg = Debugger() >> >> LJ_64 = None >> LJ_GC64 = None >> @@ -17,68 +238,73 @@ IRT_P64 = 9 >> LJ_GCVMASK = ((1 << 47) - 1) >> LJ_TISNUM = None >> >> -# Debugger specific {{{ >> - >> - >> # Global >> target = None >> >> >> -class Ptr: >> +class Ptr(object): >> def __init__(self, value, normal_type): >> self.value = value >> self.normal_type = normal_type >> >> @property >> def __deref(self): >> - return self.normal_type(self.value.Dereference()) >> + return self.normal_type(dbg.dereference(self.value)) >> >> def __add__(self, other): >> - assert isinstance(other, int) >> + assert is_integer_type(other) >> return self.__class__( >> cast( >> self.normal_type.__name__ + ' *', >> cast( >> 'uintptr_t', >> - self.value.unsigned + other * self.value.deref.size, >> + dbg.to_unsigned(self.value) + other * sizeof( >> + self.normal_type.__name__ >> + ), >> ), >> ), >> ) >> >> def __sub__(self, other): >> - assert isinstance(other, int) or isinstance(other, Ptr) >> - if isinstance(other, int): >> + assert is_integer_type(other) or isinstance(other, Ptr) >> + if is_integer_type(other): >> return self.__add__(-other) >> else: >> - return int((self.value.unsigned - other.value.unsigned) >> - / sizeof(self.normal_type.__name__)) >> + return int( >> + ( >> + dbg.to_unsigned(self.value) - dbg.to_unsigned(other.value) >> + ) / sizeof(self.normal_type.__name__) >> + ) >> >> def __eq__(self, other): >> - assert isinstance(other, Ptr) or isinstance(other, int) and other >= 0 >> + assert isinstance(other, Ptr) or is_integer_type(other) >> if isinstance(other, Ptr): >> - return self.value.unsigned == other.value.unsigned >> + return dbg.to_unsigned(self.value) == dbg.to_unsigned(other.value) >> else: >> - return self.value.unsigned == other >> + return dbg.to_unsigned(self.value) == other >> >> def __ne__(self, other): >> return not self == other >> >> def __gt__(self, other): >> assert isinstance(other, Ptr) >> - return self.value.unsigned > other.value.unsigned >> + return dbg.to_unsigned(self.value) > dbg.to_unsigned(other.value) >> >> def __ge__(self, other): >> assert isinstance(other, Ptr) >> - return self.value.unsigned >= other.value.unsigned >> + return dbg.to_unsigned(self.value) >= dbg.to_unsigned(other.value) >> >> def __bool__(self): >> - return self.value.unsigned != 0 >> + return dbg.to_unsigned(self.value) != 0 >> >> def __int__(self): >> - return self.value.unsigned >> + return dbg.to_unsigned(self.value) >> + >> + def __long__(self): >> + return dbg.to_unsigned(self.value) >> >> def __str__(self): >> - return self.value.value >> + return dbg.to_str(self.value) >> >> def __getattr__(self, name): >> if name != '__deref': >> @@ -86,53 +312,26 @@ class Ptr: >> return self.__deref >> >> >> -class MetaStruct(type): >> - def __init__(cls, name, bases, nmspc): >> - super(MetaStruct, cls).__init__(name, bases, nmspc) >> - >> - def make_general(field, tp): >> - builtin = { >> - 'uint': 'unsigned', >> - 'int': 'signed', >> - 'string': 'value', >> - } >> - if tp in builtin.keys(): >> - return lambda self: getattr(self[field], builtin[tp]) >> - else: >> - return lambda self: globals()[tp](self[field]) >> - >> - if hasattr(cls, 'metainfo'): >> - for field in cls.metainfo: >> - if not isinstance(field[0], str): >> - setattr(cls, field[1], field[0]) >> - else: >> - setattr( >> - cls, >> - field[1], >> - property(make_general(field[1], field[0])), >> - ) >> - >> - >> -class Struct(metaclass=MetaStruct): >> +class Struct(object): >> def __init__(self, value): >> self.value = value >> >> def __getitem__(self, name): >> - return self.value.GetChildMemberWithName(name) >> + return dbg.get_member(self.value, name) >> >> @property >> def addr(self): >> - return self.value.address_of >> + return dbg.address_of(self.value) >> >> >> c_structs = { >> 'MRef': [ >> - (property(lambda self: self['ptr64'].unsigned if LJ_GC64 >> - else self['ptr32'].unsigned), 'ptr') >> + (property(lambda self: dbg.to_unsigned(self['ptr64']) if LJ_GC64 >> + else dbg.to_unsigned(self['ptr32'])), 'ptr') >> ], >> 'GCRef': [ >> - (property(lambda self: self['gcptr64'].unsigned if LJ_GC64 >> - else self['gcptr32'].unsigned), 'gcptr') >> + (property(lambda self: dbg.to_unsigned(self['gcptr64']) if LJ_GC64 >> + else dbg.to_unsigned(self['gcptr32'])), 'gcptr') >> ], >> 'TValue': [ >> ('GCRef', 'gcr'), >> @@ -141,8 +340,12 @@ c_structs = { >> ('int', 'it64'), >> ('string', 'n'), >> (property(lambda self: FR(self['fr']) if not LJ_GC64 else None), 'fr'), >> - (property(lambda self: self['ftsz'].signed if LJ_GC64 else None), >> - 'ftsz') >> + ( >> + property( >> + lambda self: dbg.to_signed(self['ftsz']) if LJ_GC64 else None >> + ), >> + 'ftsz' >> + ) >> ], >> 'GCState': [ >> ('GCRef', 'root'), >> @@ -216,26 +419,51 @@ c_structs = { >> ('TValue', 'val'), >> ('MRef', 'next') >> ], >> - 'BCIns': [] >> + 'BCIns': [], >> } >> >> >> -for cls in c_structs.keys(): >> - globals()[cls] = type(cls, (Struct, ), {'metainfo': c_structs[cls]}) >> +def make_property_from_metadata(field, tp): >> + builtin = { >> + 'uint': dbg.to_unsigned, >> + 'int': dbg.to_signed, >> + 'string': dbg.to_str, >> + } >> + if tp in builtin.keys(): >> + return lambda self: builtin[tp](self[field]) >> + else: >> + return lambda self: globals()[tp](self[field]) >> + >> + >> +for cls, metainfo in c_structs.items(): >> + cls_dict = {} >> + for field in metainfo: >> + if not isinstance(field[0], str): >> + cls_dict[field[1]] = field[0] >> + else: >> + cls_dict[field[1]] = property( >> + make_property_from_metadata(field[1], field[0]) >> + ) >> + globals()[cls] = type(cls, (Struct, ), cls_dict) >> >> >> for cls in Struct.__subclasses__(): >> ptr_name = cls.__name__ + 'Ptr' >> >> + def make_init(cls): >> + return lambda self, value: super(type(self), self).__init__(value, cls) >> + >> globals()[ptr_name] = type(ptr_name, (Ptr,), { >> - '__init__': >> - lambda self, value: super(type(self), self).__init__(value, cls) >> + '__init__': make_init(cls) >> }) >> >> >> -class Command(object): >> - def __init__(self, debugger, unused): >> - pass >> +class Command(object if dbg.LLDB else gdb.Command): >> + def __init__(self, debugger=None, unused=None): >> + if dbg.GDB: >> + # XXX Fragile: though initialization looks like a crap but it >> + # respects both Python 2 and Python 3 (see #4828). >> + gdb.Command.__init__(self, self.command, gdb.COMMAND_DATA) >> >> def get_short_help(self): >> return self.__doc__.splitlines()[0] >> @@ -245,21 +473,15 @@ class Command(object): >> >> def __call__(self, debugger, command, exe_ctx, result): >> try: >> - self.execute(debugger, command, result) >> + self.execute(command) >> except Exception as e: >> msg = 'Failed to execute command `{}`: {}'.format(self.command, e) >> result.SetError(msg) >> >> def parse(self, command): >> - process = target.GetProcess() >> - thread = process.GetSelectedThread() >> - frame = thread.GetSelectedFrame() >> - >> if not command: >> return None >> - >> - ret = frame.EvaluateExpression(command) >> - return ret >> + return dbg.to_unsigned(dbg.eval(command)) >> >> @abc.abstractproperty >> def command(self): >> @@ -270,7 +492,7 @@ class Command(object): >> """ >> >> @abc.abstractmethod >> - def execute(self, debugger, args, result): >> + def execute(self, args): >> """Implementation of the command. >> Subclasses override this method to implement the logic of a given >> command, e.g. printing a stacktrace. The command output should be >> @@ -278,6 +500,11 @@ class Command(object): >> properly routed to LLDB frontend. Any unhandled exception will be >> automatically transformed into proper errors. >> """ >> + def invoke(self, arg, from_tty): >> + try: >> + self.execute(arg) >> + except Exception as e: >> + dbg.write(e) >> >> >> def cast(typename, value): >> @@ -299,75 +526,38 @@ def cast(typename, value): >> name = name[:-1].strip() >> pointer_type = True >> >> - # Get the lldb type representation. >> - t = target.FindFirstType(name) >> + # Get the inferior type representation. >> + t = dbg.find_type(name) >> if pointer_type: >> - t = t.GetPointerType() >> - >> - if isinstance(value, int): >> - # Integer casts require some black magic for lldb to behave properly. >> - if pointer_type: >> - casted = target.CreateValueFromAddress( >> - 'value', >> - lldb.SBAddress(value, target), >> - t.GetPointeeType(), >> - ).address_of >> - else: >> - casted = target.CreateValueFromData( >> - name='value', >> - data=lldb.SBData.CreateDataFromInt(value, size=8), >> - type=t, >> - ) >> - else: >> - casted = value.Cast(t) >> + t = dbg.type_to_pointer_type(t) >> + >> + casted = dbg.cast_impl(value, t, pointer_type) >> >> if isinstance(typename, type): >> - # Wrap lldb object, if possible >> + # Wrap inferior object, if possible >> return typename(casted) >> else: >> return casted >> >> >> -def lookup_global(name): >> - return target.FindFirstGlobalVariable(name) >> - >> - >> -def type_member(type_obj, name): >> - return next((x for x in type_obj.members if x.name == name), None) >> - >> - >> -def find_type(typename): >> - return target.FindFirstType(typename) >> - >> - >> def offsetof(typename, membername): >> - type_obj = find_type(typename) >> - member = type_member(type_obj, membername) >> + type_obj = dbg.find_type(typename) >> + member = dbg.type_member(type_obj, membername) >> assert member is not None >> - return member.GetOffsetInBytes() >> + return dbg.type_member_offset(member) >> >> >> def sizeof(typename): >> - type_obj = find_type(typename) >> - return type_obj.GetByteSize() >> + type_obj = dbg.find_type(typename) >> + return dbg.type_sizeof_impl(type_obj) >> >> >> def vtou64(value): >> - return value.unsigned & 0xFFFFFFFFFFFFFFFF >> + return dbg.to_unsigned(value) & 0xFFFFFFFFFFFFFFFF >> >> >> def vtoi(value): >> - return value.signed >> - >> - >> -def dbg_eval(expr): >> - process = target.GetProcess() >> - thread = process.GetSelectedThread() >> - frame = thread.GetSelectedFrame() >> - return frame.EvaluateExpression(expr) >> - >> - >> -# }}} Debugger specific >> + return dbg.to_signed(value) >> >> >> def gcval(obj): >> @@ -393,7 +583,7 @@ def gclistlen(root, end=0x0): >> >> >> def gcringlen(root): >> - if not gcref(root): >> + if gcref(root) == 0: >> return 0 >> elif gcref(root) == gcref(gcnext(root)): >> return 1 >> @@ -439,7 +629,7 @@ def J(g): >> J_offset = offsetof('GG_State', 'J') >> return cast( >> jit_StatePtr, >> - vtou64(cast('char *', g)) - g_offset + J_offset, >> + int(vtou64(cast('char *', g)) - g_offset + J_offset), >> ) >> >> >> @@ -451,7 +641,7 @@ def L(L=None): >> # lookup a symbol for the main coroutine considering the host app >> # XXX Fragile: though the loop initialization looks like a crap but it >> # respects both Python 2 and Python 3. >> - for lstate in [L] + list(map(lambda main: lookup_global(main), ( >> + for lstate in [L] + list(map(lambda main: dbg.lookup_variable(main), ( >> # LuaJIT main coro (see luajit/src/luajit.c) >> 'globalL', >> # Tarantool main coro (see tarantool/src/lua/init.h) >> @@ -459,7 +649,7 @@ def L(L=None): >> # TODO: Add more >> ))): >> if lstate: >> - return lua_State(lstate) >> + return lua_StatePtr(lstate) >> >> >> def tou32(val): >> @@ -523,9 +713,9 @@ def funcproto(func): >> def strdata(obj): >> try: >> ptr = cast('char *', obj + 1) >> - return ptr.summary >> + return dbg.summary(ptr) >> except UnicodeEncodeError: >> - return "<luajit-lldb: error occured while rendering non-ascii slot>" >> + return "<luajit_dbg: error occured while rendering non-ascii slot>" >> >> >> def itype(o): >> @@ -730,12 +920,12 @@ def frame_pc(framelink): >> >> >> def frame_prevl(framelink): >> - # We are evaluating the `frame_pc(framelink)[-1])` with lldb's >> + # We are evaluating the `frame_pc(framelink)[-1])` with >> # REPL, because the lldb API is faulty and it's not possible to cast >> # a struct member of 32-bit type to 64-bit type without getting onto >> # the next property bits, despite the fact that it's an actual value, not >> # a pointer to it. >> - bcins = vtou64(dbg_eval('((BCIns *)' + str(frame_pc(framelink)) + ')[-1]')) >> + bcins = vtou64(dbg.eval('((BCIns *)' + str(frame_pc(framelink)) + ')[-1]')) >> return framelink - (1 + LJ_FR2 + bc_a(bcins)) >> >> >> @@ -789,12 +979,12 @@ def frames(L): >> >> def dump_framelink_slot_address(fr): >> return '{start:{padding}}:{end:{padding}}'.format( >> - start=hex(int(fr - 1)), >> - end=hex(int(fr)), >> + start=strx64(fr - 1), >> + end=strx64(fr), >> padding=len(PADDING), >> ) if LJ_FR2 else '{addr:{padding}}'.format( >> - addr=hex(int(fr)), >> - padding=len(PADDING), >> + addr=strx64(fr), >> + padding=2 * len(PADDING) + 1, >> ) >> >> >> @@ -863,7 +1053,6 @@ def dump_stack(L, base=None, top=None): >> nfreeslots=int((maxstack - top - 8) >> 3), >> ), >> ]) >> - >> for framelink, frametop in frames(L): >> # Dump all data slots in the (framelink, top) interval. >> dump.extend([ >> @@ -904,9 +1093,11 @@ the type and some info related to it. >> Whether the type of the given address differs from the listed above, then >> error message occurs. >> ''' >> - def execute(self, debugger, args, result): >> + command = 'lj-tv' >> + >> + def execute(self, args): >> tvptr = TValuePtr(cast('TValue *', self.parse(args))) >> - print('{}'.format(dump_tvalue(tvptr))) >> + dbg.write('{}'.format(dump_tvalue(tvptr))) >> >> >> class LJState(Command): >> @@ -917,9 +1108,11 @@ The command requires no args and dumps current VM and GC states >> * GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST> >> * JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR> >> ''' >> - def execute(self, debugger, args, result): >> + command = 'lj-state' >> + >> + def execute(self, args): >> g = G(L(None)) >> - print('{}'.format('\n'.join( >> + dbg.write('{}'.format('\n'.join( >> map(lambda t: '{} state: {}'.format(*t), { >> 'VM': vm_state(g), >> 'GC': gc_state(g), >> @@ -936,8 +1129,10 @@ The command requires no args and dumps values of LJ_64 and LJ_GC64 >> compile-time flags. These values define the sizes of host and GC >> pointers respectively. >> ''' >> - def execute(self, debugger, args, result): >> - print( >> + command = 'lj-arch' >> + >> + def execute(self, args): >> + dbg.write( >> 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}' >> .format( >> LJ_64=LJ_64, >> @@ -965,9 +1160,11 @@ The command requires no args and dumps current GC stats: >> * weak: <number of weak tables (to be cleared)> >> * mmudata: <number of udata|cdata to be finalized> >> ''' >> - def execute(self, debugger, args, result): >> + command = 'lj-gc' >> + >> + def execute(self, args): >> g = G(L(None)) >> - print('GC stats: {state}\n{stats}'.format( >> + dbg.write('GC stats: {state}\n{stats}'.format( >> state=gc_state(g), >> stats=dump_gc(g) >> )) >> @@ -983,9 +1180,11 @@ the payload, size in bytes and hash. >> *Caveat*: Since Python 2 provides no native Unicode support, the payload >> is replaced with the corresponding error when decoding fails. >> ''' >> - def execute(self, debugger, args, result): >> + command = 'lj-str' >> + >> + def execute(self, args): >> string_ptr = GCstrPtr(cast('GCstr *', self.parse(args))) >> - print("String: {body} [{len} bytes] with hash {hash}".format( >> + dbg.write("String: {body} [{len} bytes] with hash {hash}".format( >> body=strdata(string_ptr), >> hash=strx64(string_ptr.hash), >> len=string_ptr.len, >> @@ -1003,7 +1202,9 @@ The command receives a GCtab adress and dumps the table contents: >> * Hash part <hsize> nodes: >> <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr> >> ''' >> - def execute(self, debugger, args, result): >> + command = 'lj-tab' >> + >> + def execute(self, args): >> t = GCtabPtr(cast('GCtab *', self.parse(args))) >> array = mref(TValuePtr, t.array) >> nodes = mref(NodePtr, t.node) >> @@ -1014,22 +1215,22 @@ The command receives a GCtab adress and dumps the table contents: >> } >> >> if mt: >> - print('Metatable detected: {}'.format(strx64(mt))) >> + dbg.write('Metatable detected: {}'.format(strx64(mt))) >> >> - print('Array part: {} slots'.format(capacity['apart'])) >> + dbg.write('Array part: {} slots'.format(capacity['apart'])) >> for i in range(capacity['apart']): >> slot = array + i >> - print('{ptr}: [{index}]: {value}'.format( >> + dbg.write('{ptr}: [{index}]: {value}'.format( >> ptr=strx64(slot), >> index=i, >> value=dump_tvalue(slot) >> )) >> >> - print('Hash part: {} nodes'.format(capacity['hpart'])) >> + dbg.write('Hash part: {} nodes'.format(capacity['hpart'])) >> # See hmask comment in lj_obj.h >> for i in range(capacity['hpart']): >> node = nodes + i >> - print('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format( >> + dbg.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format( >> ptr=strx64(node), >> key=dump_tvalue(TValuePtr(node.key.addr)), >> val=dump_tvalue(TValuePtr(node.val.addr)), >> @@ -1069,56 +1270,72 @@ coroutine guest stack: >> >> If L is ommited the main coroutine is used. >> ''' >> - def execute(self, debugger, args, result): >> + command = 'lj-stack' >> + >> + def execute(self, args): >> lstate = self.parse(args) >> - lstate_ptr = cast('lua_State *', lstate) if coro is not None else None >> - print('{}'.format(dump_stack(L(lstate_ptr)))) >> + lstate_ptr = cast('lua_State *', lstate) if lstate else None >> + dbg.write('{}'.format(dump_stack(L(lstate_ptr)))) >> >> >> -def register_commands(debugger, commands): >> - for command, cls in commands.items(): >> - cls.command = command >> - debugger.HandleCommand( >> - 'command script add --overwrite --class luajit_lldb.{cls} {cmd}' >> - .format( >> - cls=cls.__name__, >> - cmd=cls.command, >> - ) >> - ) >> - print('{cmd} command intialized'.format(cmd=cls.command)) >> +LJ_COMMANDS = [ >> + LJDumpTValue, >> + LJState, >> + LJDumpArch, >> + LJGC, >> + LJDumpString, >> + LJDumpTable, >> + LJDumpStack, >> +] >> + >> >> +def register_commands(commands, debugger=None): >> + for cls in commands: >> + dbg.cmd_init(cls, debugger) >> + dbg.write('{cmd} command intialized'.format(cmd=cls.command)) >> >> -def configure(debugger): >> - global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, PADDING, LJ_TISNUM, target >> - target = debugger.GetSelectedTarget() >> - module = target.modules[0] >> - LJ_DUALNUM = module.FindSymbol('lj_lib_checknumber') is not None >> >> +def configure(debugger=None): >> + global PADDING, LJ_TISNUM, LJ_DUALNUM >> + dbg.setup_target(debugger) >> try: >> - irtype_enum = target.FindFirstType('IRType').enum_members >> - for member in irtype_enum: >> - if member.name == 'IRT_PTR': >> - LJ_64 = member.unsigned & 0x1f == IRT_P64 >> - if member.name == 'IRT_PGC': >> - LJ_FR2 = LJ_GC64 = member.unsigned & 0x1f == IRT_P64 >> + # Try to remove the callback at first to not append duplicates to >> + # gdb.events.new_objfile internal list. >> + dbg.event_disconnect(load) >> except Exception: >> - print('luajit_lldb.py failed to load: ' >> - 'no debugging symbols found for libluajit') >> - return >> - >> - PADDING = ' ' * len(strx64((TValuePtr(L().addr)))) >> - LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX'] >> - >> - >> -def __lldb_init_module(debugger, internal_dict): >> - configure(debugger) >> - register_commands(debugger, { >> - 'lj-tv': LJDumpTValue, >> - 'lj-state': LJState, >> - 'lj-arch': LJDumpArch, >> - 'lj-gc': LJGC, >> - 'lj-str': LJDumpString, >> - 'lj-tab': LJDumpTable, >> - 'lj-stack': LJDumpStack, >> - }) >> - print('luajit_lldb.py is successfully loaded') >> + # Callback is not connected. >> + pass >> + >> + try: >> + # Detect whether libluajit objfile is loaded. >> + dbg.eval('luaJIT_setmode') >> + except Exception: >> + dbg.write('luajit_dbg.py initialization is postponed ' >> + 'until libluajit objfile is loaded\n') >> + # Add a callback to be executed when the next objfile is loaded. >> + dbg.event_connect(load) >> + return False >> + >> + try: >> + dbg.arch_init() >> + except Exception: >> + dbg.write('LuaJIT debug extension failed to load: ' >> + 'no debugging symbols found for libluajit') >> + return False >> + return True >> + >> + >> +# XXX: The dummy parameter is needed for this function to >> +# work as a gdb callback. >> +def load(_=None, debugger=None): >> + if configure(debugger): >> + register_commands(LJ_COMMANDS, debugger) >> + dbg.write('LuaJIT debug extension is successfully loaded') >> + >> + >> +def __lldb_init_module(debugger, _=None): >> + load(None, debugger) >> + >> + >> +if dbg.GDB: >> + load() [-- Attachment #2: Type: text/html, Size: 62589 bytes --] ^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH luajit v3 2/2] test: add tests for debugging extensions 2023-11-10 20:16 [Tarantool-patches] [PATCH luajit v3 0/2] debug: generalized extension Maksim Kokryashkin via Tarantool-patches 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 1/2] " Maksim Kokryashkin via Tarantool-patches @ 2023-11-10 20:16 ` Maksim Kokryashkin via Tarantool-patches 2023-11-22 14:32 ` Sergey Bronnikov via Tarantool-patches 1 sibling, 1 reply; 7+ messages in thread From: Maksim Kokryashkin via Tarantool-patches @ 2023-11-10 20:16 UTC (permalink / raw) To: tarantool-patches, sergeyb, skaplun, m.kokryashkin, imun Cc: Maksim Kokryashkin This patch adds tests for LuaJIT debugging extensions for lldb and gdb. --- .flake8rc | 4 + test/CMakeLists.txt | 3 + .../CMakeLists.txt | 78 ++++++ .../debug-extension-tests.py | 250 ++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 test/LuaJIT-debug-extensions-tests/CMakeLists.txt create mode 100644 test/LuaJIT-debug-extensions-tests/debug-extension-tests.py diff --git a/.flake8rc b/.flake8rc index 13e6178f..6766ed41 100644 --- a/.flake8rc +++ b/.flake8rc @@ -3,3 +3,7 @@ extend-ignore = # XXX: Suppress F821, since we have autogenerated names for # 'ptr' type complements in luajit_lldb.py. F821 +per-file-ignores = + # XXX: Flake considers regexp special characters to be + # escape sequences. + test/LuaJIT-debug-extensions-tests/debug-extension-tests.py:W605 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 58cba5ba..d7910ea4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -78,6 +78,7 @@ add_subdirectory(PUC-Rio-Lua-5.1-tests) add_subdirectory(lua-Harness-tests) add_subdirectory(tarantool-c-tests) add_subdirectory(tarantool-tests) +add_subdirectory(LuaJIT-debug-extensions-tests) add_custom_target(${PROJECT_NAME}-test DEPENDS LuaJIT-tests @@ -85,6 +86,8 @@ add_custom_target(${PROJECT_NAME}-test DEPENDS lua-Harness-tests tarantool-c-tests tarantool-tests + LuaJIT-lldb-extension-tests + LuaJIT-gdb-extension-tests ) if(LUAJIT_USE_TEST) diff --git a/test/LuaJIT-debug-extensions-tests/CMakeLists.txt b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt new file mode 100644 index 00000000..3b38201d --- /dev/null +++ b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt @@ -0,0 +1,78 @@ +add_custom_target(LuaJIT-gdb-extension-tests + DEPENDS ${LUAJIT_TEST_BINARY} +) + +add_custom_target(LuaJIT-lldb-extension-tests + DEPENDS ${LUAJIT_TEST_BINARY} +) + +# Debug info is required for testing of extensions. +if(NOT (CMAKE_BUILD_TYPE MATCHES Debug)) + message(WARNING + "not a DEBUG build, LuaJIT-lldb-extension-tests and " + "LuaJIT-gdb-extension-tests are dummy" + ) + return() +endif() + +# MacOS asks for permission to debug a process even when the +# machine is set into development mode. To solve the issue, +# it is required to add relevant users to the `_developer` user +# group in MacOS. Disabled for now. +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND DEFINED ENV{CI}) + message(WARNING + "Interactive debugging is unavailable for macOS CI builds," + "LuaJIT-lldb-extension-tests is dummy" + ) + return() +endif() + +find_package(PythonInterp) +if(NOT PYTHONINTERP_FOUND) + message(WARNING + "`python` is not found, LuaJIT-lldb-extension-tests and " + "LuaJIT-gdb-extension-tests are dummy" + ) + return() +endif() + +set(DEBUGGER_TEST_ENV + "LUAJIT_TEST_BINARY=${LUAJIT_TEST_BINARY}" + # Suppresses __pycache__ generation. + "PYTHONDONTWRITEBYTECODE=1" + "DEBUGGER_EXTENSION_PATH=${PROJECT_SOURCE_DIR}/src/luajit_dbg.py" +) + +set(TEST_SCRIPT_PATH + ${PROJECT_SOURCE_DIR}/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py +) + +find_program(GDB gdb) +if(GDB) + set(GDB_TEST_ENV ${DEBUGGER_TEST_ENV} + "DEBUGGER_COMMAND=${GDB}" + ) + add_custom_command(TARGET LuaJIT-gdb-extension-tests + COMMENT "Running luajit_dbg.py tests with gdb" + COMMAND + ${GDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) +else() + message(WARNING "`gdb' is not found, so LuaJIT-gdb-extension-tests is dummy") +endif() + +find_program(LLDB lldb) +if(LLDB) + set(LLDB_TEST_ENV ${DEBUGGER_TEST_ENV} + "DEBUGGER_COMMAND=${LLDB}" + ) + add_custom_command(TARGET LuaJIT-lldb-extension-tests + COMMENT "Running luajit_dbg.py tests with lldb" + COMMAND + ${LLDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) +else() + message(WARNING "`lldb' is not found, so LuaJIT-gdb-extension-tests is dummy") +endif() diff --git a/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py new file mode 100644 index 00000000..6ef87473 --- /dev/null +++ b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py @@ -0,0 +1,250 @@ +# This file provides tests for LuaJIT debug extensions for lldb and gdb. +import os +import re +import subprocess +import sys +import tempfile +import unittest + +from threading import Timer + +LEGACY = re.match(r'^2\.', sys.version) + +LUAJIT_BINARY = os.environ['LUAJIT_TEST_BINARY'] +EXTENSION = os.environ['DEBUGGER_EXTENSION_PATH'] +DEBUGGER = os.environ['DEBUGGER_COMMAND'] +LLDB = 'lldb' in DEBUGGER +TIMEOUT = 10 + +RUN_CMD_FILE = '-s' if LLDB else '-x' +INFERIOR_ARGS = '--' if LLDB else '--args' +PROCESS_RUN = 'process launch' if LLDB else 'r' +LOAD_EXTENSION = ( + 'command script import {ext}' if LLDB else 'source {ext}' +).format(ext=EXTENSION) + + +def persist(data): + tmp = tempfile.NamedTemporaryFile(mode='w') + tmp.write(data) + tmp.flush() + return tmp + + +def execute_process(cmd, timeout=TIMEOUT): + if LEGACY: + # XXX: The Python 2.7 version of `subprocess.Popen` doesn't have a + # timeout option, so the required functionality was implemented via + # `threading.Timer`. + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + timer = Timer(TIMEOUT, process.kill) + timer.start() + stdout, _ = process.communicate() + timer.cancel() + + # XXX: If the timeout is exceeded and the process is killed by the + # timer, then the return code is non-zero, and we are going to blow up. + assert process.returncode == 0 + return stdout.decode('ascii') + else: + process = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT) + return process.stdout.decode('ascii') + + +def filter_debugger_output(output): + descriptor = '(lldb)' if LLDB else '(gdb)' + return ''.join( + filter( + lambda line: not line.startswith(descriptor), + output.splitlines(True), + ), + ) + + +class TestCaseBase(unittest.TestCase): + @classmethod + def construct_cmds(cls): + return '\n'.join([ + 'b {loc}'.format(loc=cls.location), + PROCESS_RUN, + 'n', + LOAD_EXTENSION, + cls.extension_cmds.strip(), + 'q', + ]) + + @classmethod + def setUpClass(cls): + cmd_file = persist(cls.construct_cmds()) + script_file = persist(cls.lua_script) + process_cmd = [ + DEBUGGER, + RUN_CMD_FILE, + cmd_file.name, + INFERIOR_ARGS, + LUAJIT_BINARY, + script_file.name, + ] + cls.output = filter_debugger_output(execute_process(process_cmd)) + cmd_file.close() + script_file.close() + + def check(self): + if LEGACY: + self.assertRegexpMatches(self.output, self.pattern.strip()) + else: + self.assertRegex(self.output, self.pattern.strip()) + + +class TestLoad(TestCaseBase): + extension_cmds = '' + location = 'lj_cf_print' + lua_script = 'print(1)' + pattern = ( + 'lj-tv command intialized\n' + 'lj-state command intialized\n' + 'lj-arch command intialized\n' + 'lj-gc command intialized\n' + 'lj-str command intialized\n' + 'lj-tab command intialized\n' + 'lj-stack command intialized\n' + 'LuaJIT debug extension is successfully loaded\n' + ) + + +class TestLJArch(TestCaseBase): + extension_cmds = 'lj-arch' + location = 'lj_cf_print' + lua_script = 'print(1)' + pattern = ( + 'LJ_64: (True|False), ' + 'LJ_GC64: (True|False), ' + 'LJ_DUALNUM: (True|False)' + ) + + +class TestLJState(TestCaseBase): + extension_cmds = 'lj-state' + location = 'lj_cf_print' + lua_script = 'print(1)' + pattern = ( + 'VM state: [A-Z]+\n' + 'GC state: [A-Z]+\n' + 'JIT state: [A-Z]+\n' + ) + + +class TestLJGC(TestCaseBase): + extension_cmds = 'lj-gc' + location = 'lj_cf_print' + lua_script = 'print(1)' + pattern = ( + 'GC stats: [A-Z]+\n' + '\ttotal: \d+\n' + '\tthreshold: \d+\n' + '\tdebt: \d+\n' + '\testimate: \d+\n' + '\tstepmul: \d+\n' + '\tpause: \d+\n' + '\tsweepstr: \d+/\d+\n' + '\troot: \d+ objects\n' + '\tgray: \d+ objects\n' + '\tgrayagain: \d+ objects\n' + '\tweak: \d+ objects\n' + '\tmmudata: \d+ objects\n' + ) + + +class TestLJStack(TestCaseBase): + extension_cmds = 'lj-stack' + location = 'lj_cf_print' + lua_script = 'print(1)' + pattern = ( + '-+ Red zone:\s+\d+ slots -+\n' + '(0x[a-zA-Z0-9]+\s+\[(S|\s)(B|\s)(T|\s)(M|\s)\] VALUE: nil\n?)*\n' + '-+ Stack:\s+\d+ slots -+\n' + '(0x[A-Za-z0-9]+(:0x[A-Za-z0-9]+)?\s+' + '\[(S|\s)(B|\s)(T|\s)(M|\s)\].*\n?)+\n' + ) + + +class TestLJTV(TestCaseBase): + location = 'lj_cf_print' + lua_script = 'print(1)' + extension_cmds = ( + 'lj-tv L->base\n' + 'lj-tv L->base + 1\n' + 'lj-tv L->base + 2\n' + 'lj-tv L->base + 3\n' + 'lj-tv L->base + 4\n' + 'lj-tv L->base + 5\n' + 'lj-tv L->base + 6\n' + 'lj-tv L->base + 7\n' + 'lj-tv L->base + 8\n' + 'lj-tv L->base + 9\n' + 'lj-tv L->base + 10\n' + 'lj-tv L->base + 11\n' + ) + + lua_script = ( + 'local ffi = require("ffi")\n' + 'print(\n' + ' nil,\n' + ' false,\n' + ' true,\n' + ' "hello",\n' + ' {1},\n' + ' 1,\n' + ' 1.1,\n' + ' coroutine.create(function() end),\n' + ' ffi.new("int*"),\n' + ' function() end,\n' + ' print,\n' + ' require\n' + ')\n' + ) + + pattern = ( + 'nil\n' + 'false\n' + 'true\n' + 'string \"hello\" @ 0x[a-zA-Z0-9]+\n' + 'table @ 0x[a-zA-Z0-9]+ \(asize: \d+, hmask: 0x[a-zA-Z0-9]+\)\n' + '(number|integer) .*1.*\n' + 'number 1.1\d+\n' + 'thread @ 0x[a-zA-Z0-9]+\n' + 'cdata @ 0x[a-zA-Z0-9]+\n' + 'Lua function @ 0x[a-zA-Z0-9]+, [0-9]+ upvalues, .+:[0-9]+\n' + 'fast function #[0-9]+\n' + 'C function @ 0x[a-zA-Z0-9]+\n' + ) + + +class TestLJStr(TestCaseBase): + extension_cmds = 'lj-str fname' + location = 'lj_cf_dofile' + lua_script = 'pcall(dofile("name"))' + pattern = 'String: .* \[\d+ bytes\] with hash 0x[a-zA-Z0-9]+' + + +class TestLJTab(TestCaseBase): + extension_cmds = 'lj-tab t' + location = 'lj_cf_unpack' + lua_script = 'unpack({1; a = 1})' + pattern = ( + 'Array part: 3 slots\n' + '0x[a-zA-Z0-9]+: \[0\]: nil\n' + '0x[a-zA-Z0-9]+: \[1\]: .+ 1\n' + '0x[a-zA-Z0-9]+: \[2\]: nil\n' + 'Hash part: 2 nodes\n' + '0x[a-zA-Z0-9]+: { string "a" @ 0x[a-zA-Z0-9]+ } => ' + '{ .+ 1 }; next = 0x0\n' + '0x[a-zA-Z0-9]+: { nil } => { nil }; next = 0x0\n' + ) + + +for test_cls in TestCaseBase.__subclasses__(): + test_cls.test = lambda self: self.check() + +if __name__ == '__main__': + unittest.main(verbosity=2) -- 2.39.3 (Apple Git-145) ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [Tarantool-patches] [PATCH luajit v3 2/2] test: add tests for debugging extensions 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 2/2] test: add tests for debugging extensions Maksim Kokryashkin via Tarantool-patches @ 2023-11-22 14:32 ` Sergey Bronnikov via Tarantool-patches 2023-12-06 14:03 ` Maxim Kokryashkin via Tarantool-patches 0 siblings, 1 reply; 7+ messages in thread From: Sergey Bronnikov via Tarantool-patches @ 2023-11-22 14:32 UTC (permalink / raw) To: Maksim Kokryashkin, tarantool-patches, skaplun, m.kokryashkin, imun [-- Attachment #1: Type: text/plain, Size: 16563 bytes --] Hello, Max, thanks for the tests! See my comments. First of all - TestLJStr always timed out: ====================================================================== ERROR: setUpClass (__main__.TestLJStr) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 88, in setUpClass cls.output = filter_debugger_output(execute_process(process_cmd)) File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 50, in execute_process process = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT) File "/usr/lib/python3.10/subprocess.py", line 505, in run stdout, stderr = process.communicate(input, timeout=timeout) File "/usr/lib/python3.10/subprocess.py", line 1154, in communicate stdout, stderr = self._communicate(input, endtime, timeout) File "/usr/lib/python3.10/subprocess.py", line 2022, in _communicate self._check_timeout(endtime, orig_timeout, stdout, stderr) File "/usr/lib/python3.10/subprocess.py", line 1198, in _check_timeout raise TimeoutExpired( subprocess.TimeoutExpired: Command '['/bin/gdb', '-x', '/tmp/tmpoo5cpysq', '--args', '/home/sergeyb/sources/MRG/tarantool/third_party/luajit/build/src/luajit', '/tmp/tmpfupekxcq']' timed out after 10 seconds make LuaJIT-debugger-lldb-tests works fine On 11/10/23 23:16, Maksim Kokryashkin wrote: > This patch adds tests for LuaJIT debugging > extensions for lldb and gdb. > --- > .flake8rc | 4 + > test/CMakeLists.txt | 3 + > .../CMakeLists.txt | 78 ++++++ > .../debug-extension-tests.py | 250 ++++++++++++++++++ > 4 files changed, 335 insertions(+) > create mode 100644 test/LuaJIT-debug-extensions-tests/CMakeLists.txt > create mode 100644 test/LuaJIT-debug-extensions-tests/debug-extension-tests.py > > diff --git a/.flake8rc b/.flake8rc > index 13e6178f..6766ed41 100644 > --- a/.flake8rc > +++ b/.flake8rc > @@ -3,3 +3,7 @@ extend-ignore = > # XXX: Suppress F821, since we have autogenerated names for > # 'ptr' type complements in luajit_lldb.py. > F821 > +per-file-ignores = > + # XXX: Flake considers regexp special characters to be > + # escape sequences. > + test/LuaJIT-debug-extensions-tests/debug-extension-tests.py:W605 > diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt > index 58cba5ba..d7910ea4 100644 > --- a/test/CMakeLists.txt > +++ b/test/CMakeLists.txt > @@ -78,6 +78,7 @@ add_subdirectory(PUC-Rio-Lua-5.1-tests) > add_subdirectory(lua-Harness-tests) > add_subdirectory(tarantool-c-tests) > add_subdirectory(tarantool-tests) > +add_subdirectory(LuaJIT-debug-extensions-tests) > > add_custom_target(${PROJECT_NAME}-test DEPENDS > LuaJIT-tests > @@ -85,6 +86,8 @@ add_custom_target(${PROJECT_NAME}-test DEPENDS > lua-Harness-tests > tarantool-c-tests > tarantool-tests > + LuaJIT-lldb-extension-tests > + LuaJIT-gdb-extension-tests > ) > > if(LUAJIT_USE_TEST) > diff --git a/test/LuaJIT-debug-extensions-tests/CMakeLists.txt b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt > new file mode 100644 > index 00000000..3b38201d > --- /dev/null > +++ b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt > @@ -0,0 +1,78 @@ > +add_custom_target(LuaJIT-gdb-extension-tests > + DEPENDS ${LUAJIT_TEST_BINARY} > +) > + > +add_custom_target(LuaJIT-lldb-extension-tests > + DEPENDS ${LUAJIT_TEST_BINARY} > +) > + > +# Debug info is required for testing of extensions. > +if(NOT (CMAKE_BUILD_TYPE MATCHES Debug)) > + message(WARNING > + "not a DEBUG build, LuaJIT-lldb-extension-tests and " > + "LuaJIT-gdb-extension-tests are dummy" > + ) > + return() > +endif() > + > +# MacOS asks for permission to debug a process even when the > +# machine is set into development mode. To solve the issue, > +# it is required to add relevant users to the `_developer` user > +# group in MacOS. Disabled for now. > +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND DEFINED ENV{CI}) > + message(WARNING > + "Interactive debugging is unavailable for macOS CI builds," > + "LuaJIT-lldb-extension-tests is dummy" > + ) > + return() > +endif() > + > +find_package(PythonInterp) > +if(NOT PYTHONINTERP_FOUND) > + message(WARNING > + "`python` is not found, LuaJIT-lldb-extension-tests and " > + "LuaJIT-gdb-extension-tests are dummy" > + ) > + return() > +endif() > + > +set(DEBUGGER_TEST_ENV > + "LUAJIT_TEST_BINARY=${LUAJIT_TEST_BINARY}" I suppose it should be something like this: |get_property(LUAJIT_TEST_BINARY TARGET ${LUAJIT_TEST_BINARY} PROPERTY LOCATION)| > + # Suppresses __pycache__ generation. > + "PYTHONDONTWRITEBYTECODE=1" > + "DEBUGGER_EXTENSION_PATH=${PROJECT_SOURCE_DIR}/src/luajit_dbg.py" > +) > + > +set(TEST_SCRIPT_PATH > + ${PROJECT_SOURCE_DIR}/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py > +) > + > +find_program(GDB gdb) > +if(GDB) > + set(GDB_TEST_ENV ${DEBUGGER_TEST_ENV} > + "DEBUGGER_COMMAND=${GDB}" > + ) > + add_custom_command(TARGET LuaJIT-gdb-extension-tests > + COMMENT "Running luajit_dbg.py tests with gdb" > + COMMAND > + ${GDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH} > + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} > + ) > +else() > + message(WARNING "`gdb' is not found, so LuaJIT-gdb-extension-tests is dummy") > +endif() I propose adding a message in dummy target, like we did in cmake/CodeCoverage.cmake: add_custom_target(${PROJECT_NAME}-coverage COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${MSG} ) > + > +find_program(LLDB lldb) > +if(LLDB) > + set(LLDB_TEST_ENV ${DEBUGGER_TEST_ENV} > + "DEBUGGER_COMMAND=${LLDB}" > + ) > + add_custom_command(TARGET LuaJIT-lldb-extension-tests > + COMMENT "Running luajit_dbg.py tests with lldb" > + COMMAND > + ${LLDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH} > + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} > + ) > +else() > + message(WARNING "`lldb' is not found, so LuaJIT-gdb-extension-tests is dummy") typo: LuaJIT-lldb-extentsion-tests > +endif() > diff --git a/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py > new file mode 100644 > index 00000000..6ef87473 > --- /dev/null > +++ b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py > @@ -0,0 +1,250 @@ > +# This file provides tests for LuaJIT debug extensions for lldb and gdb. > +import os > +import re > +import subprocess > +import sys > +import tempfile > +import unittest > + > +from threading import Timer > + > +LEGACY = re.match(r'^2\.', sys.version) > + > +LUAJIT_BINARY = os.environ['LUAJIT_TEST_BINARY'] > +EXTENSION = os.environ['DEBUGGER_EXTENSION_PATH'] > +DEBUGGER = os.environ['DEBUGGER_COMMAND'] > +LLDB = 'lldb' in DEBUGGER > +TIMEOUT = 10 skip or even fail all testcases if at least one required env variable is missed > + > +RUN_CMD_FILE = '-s' if LLDB else '-x' > +INFERIOR_ARGS = '--' if LLDB else '--args' > +PROCESS_RUN = 'process launch' if LLDB else 'r' > +LOAD_EXTENSION = ( > + 'command script import {ext}' if LLDB else 'source {ext}' > +).format(ext=EXTENSION) > + > + > +def persist(data): > + tmp = tempfile.NamedTemporaryFile(mode='w') > + tmp.write(data) > + tmp.flush() > + return tmp > + > + > +def execute_process(cmd, timeout=TIMEOUT): > + if LEGACY: > + # XXX: The Python 2.7 version of `subprocess.Popen` doesn't have a > + # timeout option, so the required functionality was implemented via > + # `threading.Timer`. > + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) why stderr is ignored? non-empy stderr is a bad symptom > + timer = Timer(TIMEOUT, process.kill) > + timer.start() > + stdout, _ = process.communicate() > + timer.cancel() > + > + # XXX: If the timeout is exceeded and the process is killed by the > + # timer, then the return code is non-zero, and we are going to blow up. > + assert process.returncode == 0 > + return stdout.decode('ascii') > + else: > + process = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT) > + return process.stdout.decode('ascii') > + > + > +def filter_debugger_output(output): > + descriptor = '(lldb)' if LLDB else '(gdb)' > + return ''.join( > + filter( > + lambda line: not line.startswith(descriptor), > + output.splitlines(True), > + ), > + ) > + > + > +class TestCaseBase(unittest.TestCase): > + @classmethod > + def construct_cmds(cls): > + return '\n'.join([ > + 'b {loc}'.format(loc=cls.location), > + PROCESS_RUN, > + 'n', > + LOAD_EXTENSION, > + cls.extension_cmds.strip(), > + 'q', > + ]) > + > + @classmethod > + def setUpClass(cls): > + cmd_file = persist(cls.construct_cmds()) > + script_file = persist(cls.lua_script) > + process_cmd = [ > + DEBUGGER, > + RUN_CMD_FILE, > + cmd_file.name, > + INFERIOR_ARGS, > + LUAJIT_BINARY, > + script_file.name, > + ] > + cls.output = filter_debugger_output(execute_process(process_cmd)) > + cmd_file.close() > + script_file.close() > + > + def check(self): > + if LEGACY: > + self.assertRegexpMatches(self.output, self.pattern.strip()) > + else: > + self.assertRegex(self.output, self.pattern.strip()) message from assertRegex is totally unreadable: ====================================================================== FAIL: test (__main__.TestLoad) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 247, in <lambda> test_cls.test = lambda self: self.check() File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 96, in check self.assertRegex(self.output, self.pattern.strip()) AssertionError: Regex didn't match: 'lj-tv command intialized\nlj-state command intialized\nlj-arch command intialized\nlj-gc command intialized\nlj-str command intialized\nlj-tab command intialized\nlj-stack command intialized\nLuaJIT debug extension is successfully loaded' not found in 'GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1\nCopyright (C) 2022 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.\nType "show copying" and "show warranty" for details.\nThis GDB was configured as "x86_64-linux-gnu".\nType "show configuration" for configuration details.\nFor bug reporting instructions, please see:\n<https://www.gnu.org/software/gdb/bugs/>.\nFind the GDB manual and other documentation resources online at:\n <http://www.gnu.org/software/gdb/documentation/>.\n\nFor help, type "help".\nType "apropos word" to search for commands related to "word"...\nReading symbols from /home/sergeyb/sources/MRG/tarantool/third_party/luajit/build/src/luajit...\nBreakpoint 1 at 0x2e640: file /home/sergeyb/sources/MRG/tarantool/third_party/luajit/src/lib_base.c, line 496.\n[Thread debugging using libthread_db enabled]\nUsing host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".\n\nBreakpoint 1, lj_cf_print (L=0x55555555c8f0 <pmain>) at /home/sergeyb/sources/MRG/tarantool/third_party/luajit/src/lib_base.c:496\n496\t{\n497\t ptrdiff_t i, nargs = L->top - L->base;\n' > + > + > +class TestLoad(TestCaseBase): > + extension_cmds = '' > + location = 'lj_cf_print' > + lua_script = 'print(1)' > + pattern = ( > + 'lj-tv command intialized\n' > + 'lj-state command intialized\n' > + 'lj-arch command intialized\n' > + 'lj-gc command intialized\n' > + 'lj-str command intialized\n' > + 'lj-tab command intialized\n' > + 'lj-stack command intialized\n' > + 'LuaJIT debug extension is successfully loaded\n' > + ) > + > + > +class TestLJArch(TestCaseBase): > + extension_cmds = 'lj-arch' > + location = 'lj_cf_print' > + lua_script = 'print(1)' > + pattern = ( > + 'LJ_64: (True|False), ' > + 'LJ_GC64: (True|False), ' > + 'LJ_DUALNUM: (True|False)' > + ) > + > + > +class TestLJState(TestCaseBase): > + extension_cmds = 'lj-state' > + location = 'lj_cf_print' > + lua_script = 'print(1)' > + pattern = ( > + 'VM state: [A-Z]+\n' > + 'GC state: [A-Z]+\n' > + 'JIT state: [A-Z]+\n' > + ) > + > + > +class TestLJGC(TestCaseBase): > + extension_cmds = 'lj-gc' > + location = 'lj_cf_print' > + lua_script = 'print(1)' > + pattern = ( > + 'GC stats: [A-Z]+\n' > + '\ttotal: \d+\n' > + '\tthreshold: \d+\n' > + '\tdebt: \d+\n' > + '\testimate: \d+\n' > + '\tstepmul: \d+\n' > + '\tpause: \d+\n' > + '\tsweepstr: \d+/\d+\n' > + '\troot: \d+ objects\n' > + '\tgray: \d+ objects\n' > + '\tgrayagain: \d+ objects\n' > + '\tweak: \d+ objects\n' > + '\tmmudata: \d+ objects\n' > + ) > + > + > +class TestLJStack(TestCaseBase): > + extension_cmds = 'lj-stack' > + location = 'lj_cf_print' > + lua_script = 'print(1)' > + pattern = ( > + '-+ Red zone:\s+\d+ slots -+\n' > + '(0x[a-zA-Z0-9]+\s+\[(S|\s)(B|\s)(T|\s)(M|\s)\] VALUE: nil\n?)*\n' > + '-+ Stack:\s+\d+ slots -+\n' > + '(0x[A-Za-z0-9]+(:0x[A-Za-z0-9]+)?\s+' > + '\[(S|\s)(B|\s)(T|\s)(M|\s)\].*\n?)+\n' > + ) > + > + > +class TestLJTV(TestCaseBase): > + location = 'lj_cf_print' > + lua_script = 'print(1)' > + extension_cmds = ( > + 'lj-tv L->base\n' > + 'lj-tv L->base + 1\n' > + 'lj-tv L->base + 2\n' > + 'lj-tv L->base + 3\n' > + 'lj-tv L->base + 4\n' > + 'lj-tv L->base + 5\n' > + 'lj-tv L->base + 6\n' > + 'lj-tv L->base + 7\n' > + 'lj-tv L->base + 8\n' > + 'lj-tv L->base + 9\n' > + 'lj-tv L->base + 10\n' > + 'lj-tv L->base + 11\n' > + ) > + > + lua_script = ( > + 'local ffi = require("ffi")\n' > + 'print(\n' > + ' nil,\n' > + ' false,\n' > + ' true,\n' > + ' "hello",\n' > + ' {1},\n' > + ' 1,\n' > + ' 1.1,\n' > + ' coroutine.create(function() end),\n' > + ' ffi.new("int*"),\n' > + ' function() end,\n' > + ' print,\n' > + ' require\n' > + ')\n' > + ) > + > + pattern = ( > + 'nil\n' > + 'false\n' > + 'true\n' > + 'string \"hello\" @ 0x[a-zA-Z0-9]+\n' > + 'table @ 0x[a-zA-Z0-9]+ \(asize: \d+, hmask: 0x[a-zA-Z0-9]+\)\n' > + '(number|integer) .*1.*\n' > + 'number 1.1\d+\n' > + 'thread @ 0x[a-zA-Z0-9]+\n' > + 'cdata @ 0x[a-zA-Z0-9]+\n' > + 'Lua function @ 0x[a-zA-Z0-9]+, [0-9]+ upvalues, .+:[0-9]+\n' > + 'fast function #[0-9]+\n' > + 'C function @ 0x[a-zA-Z0-9]+\n' > + ) > + > + > +class TestLJStr(TestCaseBase): > + extension_cmds = 'lj-str fname' > + location = 'lj_cf_dofile' > + lua_script = 'pcall(dofile("name"))' > + pattern = 'String: .* \[\d+ bytes\] with hash 0x[a-zA-Z0-9]+' > + > + > +class TestLJTab(TestCaseBase): > + extension_cmds = 'lj-tab t' > + location = 'lj_cf_unpack' > + lua_script = 'unpack({1; a = 1})' > + pattern = ( > + 'Array part: 3 slots\n' > + '0x[a-zA-Z0-9]+: \[0\]: nil\n' > + '0x[a-zA-Z0-9]+: \[1\]: .+ 1\n' > + '0x[a-zA-Z0-9]+: \[2\]: nil\n' > + 'Hash part: 2 nodes\n' > + '0x[a-zA-Z0-9]+: { string "a" @ 0x[a-zA-Z0-9]+ } => ' > + '{ .+ 1 }; next = 0x0\n' > + '0x[a-zA-Z0-9]+: { nil } => { nil }; next = 0x0\n' > + ) > + > + > +for test_cls in TestCaseBase.__subclasses__(): > + test_cls.test = lambda self: self.check() > + > +if __name__ == '__main__': > + unittest.main(verbosity=2) [-- Attachment #2: Type: text/html, Size: 18251 bytes --] ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [Tarantool-patches] [PATCH luajit v3 2/2] test: add tests for debugging extensions 2023-11-22 14:32 ` Sergey Bronnikov via Tarantool-patches @ 2023-12-06 14:03 ` Maxim Kokryashkin via Tarantool-patches 0 siblings, 0 replies; 7+ messages in thread From: Maxim Kokryashkin via Tarantool-patches @ 2023-12-06 14:03 UTC (permalink / raw) To: Sergey Bronnikov; +Cc: Maksim Kokryashkin, tarantool-patches [-- Attachment #1: Type: text/plain, Size: 17302 bytes --] Hi, Sergey! Thanks for the review! >Среда, 22 ноября 2023, 17:32 +03:00 от Sergey Bronnikov <sergeyb@tarantool.org>: > >Hello, Max, >thanks for the tests! >See my comments. > >First of all - TestLJStr always timed out: >====================================================================== >ERROR: setUpClass (__main__.TestLJStr) >---------------------------------------------------------------------- >Traceback (most recent call last): > File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 88, in setUpClass > cls.output = filter_debugger_output(execute_process(process_cmd)) > File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 50, in execute_process > process = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT) > File "/usr/lib/python3.10/subprocess.py", line 505, in run > stdout, stderr = process.communicate(input, timeout=timeout) > File "/usr/lib/python3.10/subprocess.py", line 1154, in communicate > stdout, stderr = self._communicate(input, endtime, timeout) > File "/usr/lib/python3.10/subprocess.py", line 2022, in _communicate > self._check_timeout(endtime, orig_timeout, stdout, stderr) > File "/usr/lib/python3.10/subprocess.py", line 1198, in _check_timeout > raise TimeoutExpired( >subprocess.TimeoutExpired: Command '['/bin/gdb', '-x', '/tmp/tmpoo5cpysq', '--args', '/home/sergeyb/sources/MRG/tarantool/third_party/luajit/build/src/luajit', '/tmp/tmpfupekxcq']' timed out after 10 seconds Can’t reproduce regardless of lldb being installed or not. >make LuaJIT-debugger-lldb-tests works fine > >On 11/10/23 23:16, Maksim Kokryashkin wrote: >>This patch adds tests for LuaJIT debugging >>extensions for lldb and gdb. >>--- >> .flake8rc | 4 + >> test/CMakeLists.txt | 3 + >> .../CMakeLists.txt | 78 ++++++ >> .../debug-extension-tests.py | 250 ++++++++++++++++++ >> 4 files changed, 335 insertions(+) >> create mode 100644 test/LuaJIT-debug-extensions-tests/CMakeLists.txt >> create mode 100644 test/LuaJIT-debug-extensions-tests/debug-extension-tests.py >> >>diff --git a/.flake8rc b/.flake8rc >>index 13e6178f..6766ed41 100644 >>--- a/.flake8rc >>+++ b/.flake8rc >>@@ -3,3 +3,7 @@ extend-ignore = >> # XXX: Suppress F821, since we have autogenerated names for >> # 'ptr' type complements in luajit_lldb.py. >> F821 >>+per-file-ignores = >>+ # XXX: Flake considers regexp special characters to be >>+ # escape sequences. >>+ test/LuaJIT-debug-extensions-tests/debug-extension-tests.py:W605 >>diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt >>index 58cba5ba..d7910ea4 100644 >>--- a/test/CMakeLists.txt >>+++ b/test/CMakeLists.txt >>@@ -78,6 +78,7 @@ add_subdirectory(PUC-Rio-Lua-5.1-tests) >> add_subdirectory(lua-Harness-tests) >> add_subdirectory(tarantool-c-tests) >> add_subdirectory(tarantool-tests) >>+add_subdirectory(LuaJIT-debug-extensions-tests) >> >> add_custom_target(${PROJECT_NAME}-test DEPENDS >> LuaJIT-tests >>@@ -85,6 +86,8 @@ add_custom_target(${PROJECT_NAME}-test DEPENDS >> lua-Harness-tests >> tarantool-c-tests >> tarantool-tests >>+ LuaJIT-lldb-extension-tests >>+ LuaJIT-gdb-extension-tests >> ) >> >> if(LUAJIT_USE_TEST) >>diff --git a/test/LuaJIT-debug-extensions-tests/CMakeLists.txt b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt >>new file mode 100644 >>index 00000000..3b38201d >>--- /dev/null >>+++ b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt >>@@ -0,0 +1,78 @@ >>+add_custom_target(LuaJIT-gdb-extension-tests >>+ DEPENDS ${LUAJIT_TEST_BINARY} >>+) >>+ >>+add_custom_target(LuaJIT-lldb-extension-tests >>+ DEPENDS ${LUAJIT_TEST_BINARY} >>+) >>+ >>+# Debug info is required for testing of extensions. >>+if(NOT (CMAKE_BUILD_TYPE MATCHES Debug)) >>+ message(WARNING >>+ "not a DEBUG build, LuaJIT-lldb-extension-tests and " >>+ "LuaJIT-gdb-extension-tests are dummy" >>+ ) >>+ return() >>+endif() >>+ >>+# MacOS asks for permission to debug a process even when the >>+# machine is set into development mode. To solve the issue, >>+# it is required to add relevant users to the `_developer` user >>+# group in MacOS. Disabled for now. >>+if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND DEFINED ENV{CI}) >>+ message(WARNING >>+ "Interactive debugging is unavailable for macOS CI builds," >>+ "LuaJIT-lldb-extension-tests is dummy" >>+ ) >>+ return() >>+endif() >>+ >>+find_package(PythonInterp) >>+if(NOT PYTHONINTERP_FOUND) >>+ message(WARNING >>+ "`python` is not found, LuaJIT-lldb-extension-tests and " >>+ "LuaJIT-gdb-extension-tests are dummy" >>+ ) >>+ return() >>+endif() >>+ >>+set(DEBUGGER_TEST_ENV >>+ "LUAJIT_TEST_BINARY=${LUAJIT_TEST_BINARY}" >I suppose it should be something like this: >get_property(LUAJIT_TEST_BINARY TARGET ${LUAJIT_TEST_BINARY} PROPERTY LOCATION) > That variable comes from the parent scope, so there is no need to do that. Ignoring. >>+ # Suppresses __pycache__ generation. >>+ "PYTHONDONTWRITEBYTECODE=1" >>+ "DEBUGGER_EXTENSION_PATH=${PROJECT_SOURCE_DIR}/src/luajit_dbg.py" >>+) >>+ >>+set(TEST_SCRIPT_PATH >>+ ${PROJECT_SOURCE_DIR}/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py >>+) >>+ >>+find_program(GDB gdb) >>+if(GDB) >>+ set(GDB_TEST_ENV ${DEBUGGER_TEST_ENV} >>+ "DEBUGGER_COMMAND=${GDB}" >>+ ) >>+ add_custom_command(TARGET LuaJIT-gdb-extension-tests >>+ COMMENT "Running luajit_dbg.py tests with gdb" >>+ COMMAND >>+ ${GDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH} >>+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} >>+ ) >>+else() >>+ message(WARNING "`gdb' is not found, so LuaJIT-gdb-extension-tests is dummy") >>+endif() >I propose adding a message in dummy target, like we did in cmake/CodeCoverage.cmake: > add_custom_target(${PROJECT_NAME}-coverage > COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${MSG} > ) Added, thanks! >>+ >>+find_program(LLDB lldb) >>+if(LLDB) >>+ set(LLDB_TEST_ENV ${DEBUGGER_TEST_ENV} >>+ "DEBUGGER_COMMAND=${LLDB}" >>+ ) >>+ add_custom_command(TARGET LuaJIT-lldb-extension-tests >>+ COMMENT "Running luajit_dbg.py tests with lldb" >>+ COMMAND >>+ ${LLDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH} >>+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} >>+ ) >>+else() >>+ message(WARNING "`lldb' is not found, so LuaJIT-gdb-extension-tests is dummy") >typo: LuaJIT-lldb-extentsion-tests Fixed, thanks! >>+endif() >>diff --git a/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py >>new file mode 100644 >>index 00000000..6ef87473 >>--- /dev/null >>+++ b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py >>@@ -0,0 +1,250 @@ >>+# This file provides tests for LuaJIT debug extensions for lldb and gdb. >>+import os >>+import re >>+import subprocess >>+import sys >>+import tempfile >>+import unittest >>+ >>+from threading import Timer >>+ >>+LEGACY = re.match(r'^2\.', sys.version) >>+ >>+LUAJIT_BINARY = os.environ['LUAJIT_TEST_BINARY'] >>+EXTENSION = os.environ['DEBUGGER_EXTENSION_PATH'] >>+DEBUGGER = os.environ['DEBUGGER_COMMAND'] >>+LLDB = 'lldb' in DEBUGGER >>+TIMEOUT = 10 >skip or even fail all testcases if at least one required env variable is missed Well, unittest is not TAP, so there is no easy way to skip all test cases. That’s not an issue anyway, since if any variable is missing then the `KeyError` is raised and the suite fails. Ignoring. >>+ >>+RUN_CMD_FILE = '-s' if LLDB else '-x' >>+INFERIOR_ARGS = '--' if LLDB else '--args' >>+PROCESS_RUN = 'process launch' if LLDB else 'r' >>+LOAD_EXTENSION = ( >>+ 'command script import {ext}' if LLDB else 'source {ext}' >>+).format(ext=EXTENSION) >>+ >>+ >>+def persist(data): >>+ tmp = tempfile.NamedTemporaryFile(mode='w') >>+ tmp.write(data) >>+ tmp.flush() >>+ return tmp >>+ >>+ >>+def execute_process(cmd, timeout=TIMEOUT): >>+ if LEGACY: >>+ # XXX: The Python 2.7 version of `subprocess.Popen` doesn't have a >>+ # timeout option, so the required functionality was implemented via >>+ # `threading.Timer`. >>+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE) >why stderr is ignored? non-empy stderr is a bad symptom Fixed, thanks! >>+ timer = Timer(TIMEOUT, process.kill) >>+ timer.start() >>+ stdout, _ = process.communicate() >>+ timer.cancel() >>+ >>+ # XXX: If the timeout is exceeded and the process is killed by the >>+ # timer, then the return code is non-zero, and we are going to blow up. >>+ assert process.returncode == 0 >>+ return stdout.decode('ascii') >>+ else: >>+ process = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT) >>+ return process.stdout.decode('ascii') >>+ >>+ >>+def filter_debugger_output(output): >>+ descriptor = '(lldb)' if LLDB else '(gdb)' >>+ return ''.join( >>+ filter( >>+ lambda line: not line.startswith(descriptor), >>+ output.splitlines(True), >>+ ), >>+ ) >>+ >>+ >>+class TestCaseBase(unittest.TestCase): >>+ @classmethod >>+ def construct_cmds(cls): >>+ return '\n'.join([ >>+ 'b {loc}'.format(loc=cls.location), >>+ PROCESS_RUN, >>+ 'n', >>+ LOAD_EXTENSION, >>+ cls.extension_cmds.strip(), >>+ 'q', >>+ ]) >>+ >>+ @classmethod >>+ def setUpClass(cls): >>+ cmd_file = persist(cls.construct_cmds()) >>+ script_file = persist(cls.lua_script) >>+ process_cmd = [ >>+ DEBUGGER, >>+ RUN_CMD_FILE, >>+ cmd_file.name, >>+ INFERIOR_ARGS, >>+ LUAJIT_BINARY, >>+ script_file.name, >>+ ] >>+ cls.output = filter_debugger_output(execute_process(process_cmd)) >>+ cmd_file.close() >>+ script_file.close() >>+ >>+ def check(self): >>+ if LEGACY: >>+ self.assertRegexpMatches(self.output, self.pattern.strip()) >>+ else: >>+ self.assertRegex(self.output, self.pattern.strip()) >message from assertRegex is totally unreadable: >====================================================================== >FAIL: test (__main__.TestLoad) >---------------------------------------------------------------------- >Traceback (most recent call last): > File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 247, in <lambda> > test_cls.test = lambda self: self.check() > File "/home/sergeyb/sources/MRG/tarantool/third_party/luajit/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py", line 96, in check > self.assertRegex(self.output, self.pattern.strip()) >AssertionError: Regex didn't match: 'lj-tv command intialized\nlj-state command intialized\nlj-arch command intialized\nlj-gc command intialized\nlj-str command intialized\nlj-tab command intialized\nlj-stack command intialized\nLuaJIT debug extension is successfully loaded' not found in 'GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1\nCopyright (C) 2022 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> \nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.\nType "show copying" and "show warranty" for details.\nThis GDB was configured as "x86_64-linux-gnu".\nType "show configuration" for configuration details.\nFor bug reporting instructions, please see:\n <https://www.gnu.org/software/gdb/bugs/> .\nFind the GDB manual and other documentation resources online at:\n <http://www.gnu.org/software/gdb/documentation/> .\n\nFor help, type "help".\nType "apropos word" to search for commands related to "word"...\nReading symbols from /home/sergeyb/sources/MRG/tarantool/third_party/luajit/build/src/luajit...\nBreakpoint 1 at 0x2e640: file /home/sergeyb/sources/MRG/tarantool/third_party/luajit/src/lib_base.c, line 496.\n[Thread debugging using libthread_db enabled]\nUsing host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".\n\nBreakpoint 1, lj_cf_print (L=0x55555555c8f0 <pmain>) at /home/sergeyb/sources/MRG/tarantool/third_party/luajit/src/lib_base.c:496\n496\t{\n497\t ptrdiff_t i, nargs = L->top - L->base;\n' I’ve implemented my own check based on unittest’s public API, so now it looks nicer. Thanks! > >>+ >>+ >>+class TestLoad(TestCaseBase): >>+ extension_cmds = '' >>+ location = 'lj_cf_print' >>+ lua_script = 'print(1)' >>+ pattern = ( >>+ 'lj-tv command intialized\n' >>+ 'lj-state command intialized\n' >>+ 'lj-arch command intialized\n' >>+ 'lj-gc command intialized\n' >>+ 'lj-str command intialized\n' >>+ 'lj-tab command intialized\n' >>+ 'lj-stack command intialized\n' >>+ 'LuaJIT debug extension is successfully loaded\n' >>+ ) >>+ >>+ >>+class TestLJArch(TestCaseBase): >>+ extension_cmds = 'lj-arch' >>+ location = 'lj_cf_print' >>+ lua_script = 'print(1)' >>+ pattern = ( >>+ 'LJ_64: (True|False), ' >>+ 'LJ_GC64: (True|False), ' >>+ 'LJ_DUALNUM: (True|False)' >>+ ) >>+ >>+ >>+class TestLJState(TestCaseBase): >>+ extension_cmds = 'lj-state' >>+ location = 'lj_cf_print' >>+ lua_script = 'print(1)' >>+ pattern = ( >>+ 'VM state: [A-Z]+\n' >>+ 'GC state: [A-Z]+\n' >>+ 'JIT state: [A-Z]+\n' >>+ ) >>+ >>+ >>+class TestLJGC(TestCaseBase): >>+ extension_cmds = 'lj-gc' >>+ location = 'lj_cf_print' >>+ lua_script = 'print(1)' >>+ pattern = ( >>+ 'GC stats: [A-Z]+\n' >>+ '\ttotal: \d+\n' >>+ '\tthreshold: \d+\n' >>+ '\tdebt: \d+\n' >>+ '\testimate: \d+\n' >>+ '\tstepmul: \d+\n' >>+ '\tpause: \d+\n' >>+ '\tsweepstr: \d+/\d+\n' >>+ '\troot: \d+ objects\n' >>+ '\tgray: \d+ objects\n' >>+ '\tgrayagain: \d+ objects\n' >>+ '\tweak: \d+ objects\n' >>+ '\tmmudata: \d+ objects\n' >>+ ) >>+ >>+ >>+class TestLJStack(TestCaseBase): >>+ extension_cmds = 'lj-stack' >>+ location = 'lj_cf_print' >>+ lua_script = 'print(1)' >>+ pattern = ( >>+ '-+ Red zone:\s+\d+ slots -+\n' >>+ '(0x[a-zA-Z0-9]+\s+\[(S|\s)(B|\s)(T|\s)(M|\s)\] VALUE: nil\n?)*\n' >>+ '-+ Stack:\s+\d+ slots -+\n' >>+ '(0x[A-Za-z0-9]+(:0x[A-Za-z0-9]+)?\s+' >>+ '\[(S|\s)(B|\s)(T|\s)(M|\s)\].*\n?)+\n' >>+ ) >>+ >>+ >>+class TestLJTV(TestCaseBase): >>+ location = 'lj_cf_print' >>+ lua_script = 'print(1)' >>+ extension_cmds = ( >>+ 'lj-tv L->base\n' >>+ 'lj-tv L->base + 1\n' >>+ 'lj-tv L->base + 2\n' >>+ 'lj-tv L->base + 3\n' >>+ 'lj-tv L->base + 4\n' >>+ 'lj-tv L->base + 5\n' >>+ 'lj-tv L->base + 6\n' >>+ 'lj-tv L->base + 7\n' >>+ 'lj-tv L->base + 8\n' >>+ 'lj-tv L->base + 9\n' >>+ 'lj-tv L->base + 10\n' >>+ 'lj-tv L->base + 11\n' >>+ ) >>+ >>+ lua_script = ( >>+ 'local ffi = require("ffi")\n' >>+ 'print(\n' >>+ ' nil,\n' >>+ ' false,\n' >>+ ' true,\n' >>+ ' "hello",\n' >>+ ' {1},\n' >>+ ' 1,\n' >>+ ' 1.1,\n' >>+ ' coroutine.create(function() end),\n' >>+ ' ffi.new("int*"),\n' >>+ ' function() end,\n' >>+ ' print,\n' >>+ ' require\n' >>+ ')\n' >>+ ) >>+ >>+ pattern = ( >>+ 'nil\n' >>+ 'false\n' >>+ 'true\n' >>+ 'string \"hello\" @ 0x[a-zA-Z0-9]+\n' >>+ 'table @ 0x[a-zA-Z0-9]+ \(asize: \d+, hmask: 0x[a-zA-Z0-9]+\)\n' >>+ '(number|integer) .*1.*\n' >>+ 'number 1.1\d+\n' >>+ 'thread @ 0x[a-zA-Z0-9]+\n' >>+ 'cdata @ 0x[a-zA-Z0-9]+\n' >>+ 'Lua function @ 0x[a-zA-Z0-9]+, [0-9]+ upvalues, .+:[0-9]+\n' >>+ 'fast function #[0-9]+\n' >>+ 'C function @ 0x[a-zA-Z0-9]+\n' >>+ ) >>+ >>+ >>+class TestLJStr(TestCaseBase): >>+ extension_cmds = 'lj-str fname' >>+ location = 'lj_cf_dofile' >>+ lua_script = 'pcall(dofile("name"))' >>+ pattern = 'String: .* \[\d+ bytes\] with hash 0x[a-zA-Z0-9]+' >>+ >>+ >>+class TestLJTab(TestCaseBase): >>+ extension_cmds = 'lj-tab t' >>+ location = 'lj_cf_unpack' >>+ lua_script = 'unpack({1; a = 1})' >>+ pattern = ( >>+ 'Array part: 3 slots\n' >>+ '0x[a-zA-Z0-9]+: \[0\]: nil\n' >>+ '0x[a-zA-Z0-9]+: \[1\]: .+ 1\n' >>+ '0x[a-zA-Z0-9]+: \[2\]: nil\n' >>+ 'Hash part: 2 nodes\n' >>+ '0x[a-zA-Z0-9]+: { string "a" @ 0x[a-zA-Z0-9]+ } => ' >>+ '{ .+ 1 }; next = 0x0\n' >>+ '0x[a-zA-Z0-9]+: { nil } => { nil }; next = 0x0\n' >>+ ) >>+ >>+ >>+for test_cls in TestCaseBase.__subclasses__(): >>+ test_cls.test = lambda self: self.check() >>+ >>+if __name__ == '__main__': >>+ unittest.main(verbosity=2) >> [-- Attachment #2: Type: text/html, Size: 19968 bytes --] ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2023-12-06 14:03 UTC | newest] Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2023-11-10 20:16 [Tarantool-patches] [PATCH luajit v3 0/2] debug: generalized extension Maksim Kokryashkin via Tarantool-patches 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 1/2] " Maksim Kokryashkin via Tarantool-patches 2023-11-22 14:46 ` Sergey Bronnikov via Tarantool-patches 2023-12-06 13:58 ` Maxim Kokryashkin via Tarantool-patches 2023-11-10 20:16 ` [Tarantool-patches] [PATCH luajit v3 2/2] test: add tests for debugging extensions Maksim Kokryashkin via Tarantool-patches 2023-11-22 14:32 ` Sergey Bronnikov via Tarantool-patches 2023-12-06 14:03 ` Maxim Kokryashkin via Tarantool-patches
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox