* [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; 5+ 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] 5+ 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; 5+ 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] 5+ 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; 5+ 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] 5+ 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
0 siblings, 0 replies; 5+ 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] 5+ 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
0 siblings, 0 replies; 5+ 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] 5+ messages in thread
end of thread, other threads:[~2023-11-22 14:46 UTC | newest]
Thread overview: 5+ 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-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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox