Tarantool development patches archive
 help / color / mirror / Atom feed
* [Tarantool-patches] [PATCH luajit v5 0/2] debug: generalized extension
@ 2023-12-06 14:14 Maxim Kokryashkin via Tarantool-patches
  2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 1/2] " Maxim Kokryashkin via Tarantool-patches
  2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions Maxim Kokryashkin via Tarantool-patches
  0 siblings, 2 replies; 7+ messages in thread
From: Maxim Kokryashkin via Tarantool-patches @ 2023-12-06 14:14 UTC (permalink / raw)
  To: tarantool-patches, skaplun, sergeyb, imun

Changes in v5:
- Cleaned up junk from commit with tests.

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}         | 616 ++++++++----
 test/CMakeLists.txt                           |   3 +
 .../CMakeLists.txt                            | 102 ++
 .../debug-extension-tests.py                  | 262 ++++++
 6 files changed, 787 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.43.0


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [Tarantool-patches] [PATCH luajit v5 1/2] debug: generalized extension
  2023-12-06 14:14 [Tarantool-patches] [PATCH luajit v5 0/2] debug: generalized extension Maxim Kokryashkin via Tarantool-patches
@ 2023-12-06 14:14 ` Maxim Kokryashkin via Tarantool-patches
  2023-12-19  8:01   ` Sergey Bronnikov via Tarantool-patches
  2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions Maxim Kokryashkin via Tarantool-patches
  1 sibling, 1 reply; 7+ messages in thread
From: Maxim Kokryashkin via Tarantool-patches @ 2023-12-06 14:14 UTC (permalink / raw)
  To: tarantool-patches, skaplun, sergeyb, imun

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 d2070e9b..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 occurred 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 address 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 omitted 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 5ac11b65..e79b7c32 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 occurred 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 address 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 address 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 omitted 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.43.0


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions
  2023-12-06 14:14 [Tarantool-patches] [PATCH luajit v5 0/2] debug: generalized extension Maxim Kokryashkin via Tarantool-patches
  2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 1/2] " Maxim Kokryashkin via Tarantool-patches
@ 2023-12-06 14:14 ` Maxim Kokryashkin via Tarantool-patches
  2023-12-19  8:16   ` Sergey Bronnikov via Tarantool-patches
  1 sibling, 1 reply; 7+ messages in thread
From: Maxim Kokryashkin via Tarantool-patches @ 2023-12-06 14:14 UTC (permalink / raw)
  To: tarantool-patches, skaplun, sergeyb, imun; +Cc: Maksim Kokryashkin

From: Maksim Kokryashkin <max.kokryashkin@gmail.com>

This patch adds tests for LuaJIT debugging
extensions for lldb and gdb.
---
 .flake8rc                                     |   4 +
 src/luajit_dbg.py                             |  11 +-
 test/CMakeLists.txt                           |   3 +
 .../CMakeLists.txt                            | 102 +++++++
 .../debug-extension-tests.py                  | 262 ++++++++++++++++++
 5 files changed, 376 insertions(+), 6 deletions(-)
 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/src/luajit_dbg.py b/src/luajit_dbg.py
index e79b7c32..a42d8f25 100644
--- a/src/luajit_dbg.py
+++ b/src/luajit_dbg.py
@@ -40,14 +40,13 @@ class Debugger(object):
             lib = None
             try:
                 lib = import_module(name)
-            except ImportError:
+                if healthcheck(lib):
+                    setattr(self, name.upper(), True)
+                    globals()[name] = lib
+                    self.name = name
+            except Exception:
                 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):
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 8afc42df..83d4bfc7 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -79,6 +79,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
@@ -86,6 +87,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..78eafd02
--- /dev/null
+++ b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt
@@ -0,0 +1,102 @@
+add_custom_target(LuaJIT-gdb-extension-tests
+  DEPENDS ${LUAJIT_TEST_BINARY}
+)
+
+add_custom_target(LuaJIT-lldb-extension-tests
+  DEPENDS ${LUAJIT_TEST_BINARY}
+)
+
+set(DUMMY_GDB_MSG "LuaJIT-gdb-extension-tests is a dummy target")
+set(DUMMY_LLDB_MSG "LuaJIT-lldb-extension-tests is a dummy target")
+
+# 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"
+  )
+  add_custom_command(TARGET LuaJIT-gdb-extension-tests
+    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_GDB_MSG}
+  )
+  add_custom_command(TARGET LuaJIT-lldb-extension-tests
+    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_LLDB_MSG}
+  )
+  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"
+  )
+  add_custom_command(TARGET LuaJIT-lldb-extension-tests
+    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_LLDB_MSG}
+  )
+  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"
+  )
+  add_custom_command(TARGET LuaJIT-gdb-extension-tests
+    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_GDB_MSG}
+  )
+  add_custom_command(TARGET LuaJIT-lldb-extension-tests
+    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_LLDB_MSG}
+  )
+  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")
+  add_custom_command(TARGET LuaJIT-gdb-extension-tests
+    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_GDB_MSG}
+  )
+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-lldb-extension-tests is dummy")
+  add_custom_command(TARGET LuaJIT-lldb-extension-tests
+    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_LLDB_MSG}
+  )
+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..818247ca
--- /dev/null
+++ b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py
@@ -0,0 +1,262 @@
+# 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,
+            stderr=subprocess.PIPE,
+        )
+        timer = Timer(TIMEOUT, process.kill)
+        timer.start()
+        stdout, stderr = 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'), stderr.decode('ascii')
+    else:
+        process = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT)
+        return process.stdout.decode('ascii'), process.stderr.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, cls.stderr = execute_process(process_cmd)
+        cls.output = filter_debugger_output(cls.output)
+        cmd_file.close()
+        script_file.close()
+
+    def check(self):
+        if self.stderr != '':
+            raise self.failureException(
+                'Non-empty stderr: {}'.format(self.stderr),
+            )
+
+        pattern = self.pattern.strip()
+        match = re.search(pattern, self.output)
+
+        if not match:
+            fmt = '\nEXPECTED PATTERN:\n{}\n\nGOT OUTPUT:\n{}'
+            raise self.failureException(fmt.format(pattern, self.output))
+
+
+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.43.0


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [Tarantool-patches] [PATCH luajit v5 1/2] debug: generalized extension
  2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 1/2] " Maxim Kokryashkin via Tarantool-patches
@ 2023-12-19  8:01   ` Sergey Bronnikov via Tarantool-patches
  0 siblings, 0 replies; 7+ messages in thread
From: Sergey Bronnikov via Tarantool-patches @ 2023-12-19  8:01 UTC (permalink / raw)
  To: Maxim Kokryashkin, tarantool-patches, skaplun, imun

Hi, Max


thanks for the patch! LGTM

I appreciate your efforts in deduplication code,

I believe it will reduce maintenance cost for these extensions.


On 12/6/23 17:14, Maxim Kokryashkin wrote:
> 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.
> ---
<snipped>

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions
  2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions Maxim Kokryashkin via Tarantool-patches
@ 2023-12-19  8:16   ` Sergey Bronnikov via Tarantool-patches
  2023-12-19 14:34     ` Maxim Kokryashkin via Tarantool-patches
  0 siblings, 1 reply; 7+ messages in thread
From: Sergey Bronnikov via Tarantool-patches @ 2023-12-19  8:16 UTC (permalink / raw)
  To: Maxim Kokryashkin, tarantool-patches, skaplun, imun

Hi, Max!

many thanks for the patch! LGTM with a a number of minor comments,

please see below.


Sergey


On 12/6/23 17:14, Maxim Kokryashkin wrote:
> From: Maksim Kokryashkin <max.kokryashkin@gmail.com>
>
> This patch adds tests for LuaJIT debugging
> extensions for lldb and gdb.
> ---
>   .flake8rc                                     |   4 +
>   src/luajit_dbg.py                             |  11 +-
>   test/CMakeLists.txt                           |   3 +
>   .../CMakeLists.txt                            | 102 +++++++
>   .../debug-extension-tests.py                  | 262 ++++++++++++++++++
>   5 files changed, 376 insertions(+), 6 deletions(-)
>   create mode 100644 test/LuaJIT-debug-extensions-tests/CMakeLists.txt
>   create mode 100644 test/LuaJIT-debug-extensions-tests/debug-extension-tests.py
<snipped>
>   
>   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..78eafd02
> --- /dev/null
> +++ b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt
> @@ -0,0 +1,102 @@
> +add_custom_target(LuaJIT-gdb-extension-tests
> +  DEPENDS ${LUAJIT_TEST_BINARY}
> +)
> +
> +add_custom_target(LuaJIT-lldb-extension-tests
> +  DEPENDS ${LUAJIT_TEST_BINARY}
> +)
> +
> +set(DUMMY_GDB_MSG "LuaJIT-gdb-extension-tests is a dummy target")
> +set(DUMMY_LLDB_MSG "LuaJIT-lldb-extension-tests is a dummy target")
> +
> +# 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"
> +  )
> +  add_custom_command(TARGET LuaJIT-gdb-extension-tests
> +    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_GDB_MSG}
> +  )
> +  add_custom_command(TARGET LuaJIT-lldb-extension-tests
> +    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_LLDB_MSG}
> +  )
> +  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"
> +  )
> +  add_custom_command(TARGET LuaJIT-lldb-extension-tests
> +    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_LLDB_MSG}
> +  )
> +  return()
> +endif()
> +
> +find_package(PythonInterp)

CMake Warning (dev) at 
test/LuaJIT-debug-extensions-tests/CMakeLists.txt:30 (find_package):
   Policy CMP0148 is not set: The FindPythonInterp and FindPythonLibs 
modules
   are removed.  Run "cmake --help-policy CMP0148" for policy details.  Use
   the cmake_policy command to set the policy and suppress this warning.

This warning is for project developers.  Use -Wno-dev to suppress it.

See https://cmake.org/cmake/help/latest/policy/CMP0148.html

Let's fix this warning.



> +if(NOT PYTHONINTERP_FOUND)
> +  message(WARNING
> +    "`python` is not found, LuaJIT-lldb-extension-tests and "
> +    "LuaJIT-gdb-extension-tests are dummy"
> +  )
> +  add_custom_command(TARGET LuaJIT-gdb-extension-tests
> +    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_GDB_MSG}
> +  )
> +  add_custom_command(TARGET LuaJIT-lldb-extension-tests
> +    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_LLDB_MSG}
> +  )
> +  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"
Nit: I would omit a filename in a comment
> +    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")
> +  add_custom_command(TARGET LuaJIT-gdb-extension-tests
> +    COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --red ${DUMMY_GDB_MSG}
> +  )
> +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"

Nit: I would omit a filename in a comment


> +
<snipped>
> +
> +for test_cls in TestCaseBase.__subclasses__():
> +    test_cls.test = lambda self: self.check()
> +
> +if __name__ == '__main__':
> +    unittest.main(verbosity=2)

there are three verbosity levels: quiet (0), default (1) and verbose (3).

I would left a comment that "2" is a verbose level.

Feel free to ignore.


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions
  2023-12-19  8:16   ` Sergey Bronnikov via Tarantool-patches
@ 2023-12-19 14:34     ` Maxim Kokryashkin via Tarantool-patches
  2023-12-20 12:23       ` Sergey Bronnikov via Tarantool-patches
  0 siblings, 1 reply; 7+ messages in thread
From: Maxim Kokryashkin via Tarantool-patches @ 2023-12-19 14:34 UTC (permalink / raw)
  To: Sergey Bronnikov; +Cc: Maxim Kokryashkin, tarantool-patches

Hi!
Thanks for the review!
Fixed your comments, except for the comment about CMake policy. The
policy you've mentioned is too new to include in our CMakeLists.txt.

Branch is force pushed, diff is below:
===
diff --git a/test/LuaJIT-debug-extensions-tests/CMakeLists.txt b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt
index 3b38201d..1956cf2a 100644
--- a/test/LuaJIT-debug-extensions-tests/CMakeLists.txt
+++ b/test/LuaJIT-debug-extensions-tests/CMakeLists.txt
@@ -6,6 +6,15 @@ add_custom_target(LuaJIT-lldb-extension-tests
   DEPENDS ${LUAJIT_TEST_BINARY}
 )

+# Skip tests for Tarantool.
+if(ENABLE_BUNDLED_LUAJIT)
+  message(WARNING
+    "Tarantool build, LuaJIT-lldb-extension-tests and "
+    "LuaJIT-gdb-extension-tests are dummy"
+  )
+  return()
+endif()
+
 # Debug info is required for testing of extensions.
 if(NOT (CMAKE_BUILD_TYPE MATCHES Debug))
   message(WARNING
@@ -53,7 +56,7 @@ if(GDB)
     "DEBUGGER_COMMAND=${GDB}"
   )
   add_custom_command(TARGET LuaJIT-gdb-extension-tests
-    COMMENT "Running luajit_dbg.py tests with gdb"
+    COMMENT "Running debug extension tests with gdb"
     COMMAND
     ${GDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH}
     WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
@@ -68,7 +71,7 @@ if(LLDB)
     "DEBUGGER_COMMAND=${LLDB}"
   )
   add_custom_command(TARGET LuaJIT-lldb-extension-tests
-    COMMENT "Running luajit_dbg.py tests with lldb"
+    COMMENT "Running debug extension tests with lldb"
     COMMAND
     ${LLDB_TEST_ENV} ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH}
     WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
diff --git a/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py
index 6ef87473..380e4324 100644
--- a/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py
+++ b/test/LuaJIT-debug-extensions-tests/debug-extension-tests.py
@@ -247,4 +247,5 @@ for test_cls in TestCaseBase.__subclasses__():
     test_cls.test = lambda self: self.check()

 if __name__ == '__main__':
+    # XXX: Verbosity level 2 is the most verbose.
     unittest.main(verbosity=2)
===

Regards,
Maxim Kokryashkin

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions
  2023-12-19 14:34     ` Maxim Kokryashkin via Tarantool-patches
@ 2023-12-20 12:23       ` Sergey Bronnikov via Tarantool-patches
  0 siblings, 0 replies; 7+ messages in thread
From: Sergey Bronnikov via Tarantool-patches @ 2023-12-20 12:23 UTC (permalink / raw)
  To: Maxim Kokryashkin; +Cc: Maxim Kokryashkin, tarantool-patches

Thanks again, LGTM.

On 12/19/23 17:34, Maxim Kokryashkin wrote:
> Hi!
> Thanks for the review!
> Fixed your comments, except for the comment about CMake policy. The
> policy you've mentioned is too new to include in our CMakeLists.txt.
>
> Branch is force pushed, diff is below:
> ===


<snipped>


^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2023-12-20 12:23 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-12-06 14:14 [Tarantool-patches] [PATCH luajit v5 0/2] debug: generalized extension Maxim Kokryashkin via Tarantool-patches
2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 1/2] " Maxim Kokryashkin via Tarantool-patches
2023-12-19  8:01   ` Sergey Bronnikov via Tarantool-patches
2023-12-06 14:14 ` [Tarantool-patches] [PATCH luajit v5 2/2] test: add tests for debugging extensions Maxim Kokryashkin via Tarantool-patches
2023-12-19  8:16   ` Sergey Bronnikov via Tarantool-patches
2023-12-19 14:34     ` Maxim Kokryashkin via Tarantool-patches
2023-12-20 12:23       ` Sergey Bronnikov 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