From: Sergey Kaplun via Tarantool-patches <tarantool-patches@dev.tarantool.org>
To: Sergey Bronnikov <sergeyb@tarantool.org>,
Evgeniy Temirgaleev <e.temirgaleev@tarantool.org>
Cc: tarantool-patches@dev.tarantool.org
Subject: [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers
Date: Thu, 25 Jun 2026 23:29:01 +0300 [thread overview]
Message-ID: <20260625202903.3157425-2-skaplun@tarantool.org> (raw)
In-Reply-To: <20260625202903.3157425-1-skaplun@tarantool.org>
This patch adds dumpers for a single IR instruction (`lj-ir`), as well
as for all bytecodes inside one trace (`lj-trace`). Its dump is quite
similar to the -jdump flag but also reports types of register operands
(`ref`, `lit`, `cst`) and operation mode (`N`, `A`, `W`, etc.).
The `lj-trace` command accepts optional /rs flags to dump registers
associated with IR and snapshots for the trace correspondingly.
The `lj-ir` command can be used for dumping IR constants as well.
The `lj-jslots` command dumps the content of `J->slot`. It is useful to
simplify debugging of `rec_check_slots()` assertion failures.
For LLDB value, the `__getitem__` metamethod now accepts bool keys.
Also, `__index__` is set to allow lldb.value to be used as an index
without explicit conversion to int. Old GDB versions (below 7.12) are
not supported because of the gdb.Value lacks the `__index__` metamethod
and can't be monkey-patched. The support for these versions may be added
by demand.
Part of tarantool/tarantool#4808
---
src/luajit_dbg.py | 1216 ++++++++++++++++-
.../debug-extension-tests.py | 365 +++++
2 files changed, 1570 insertions(+), 11 deletions(-)
diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
index 2edb199a..fd6ca8a5 100644
--- a/src/luajit_dbg.py
+++ b/src/luajit_dbg.py
@@ -58,6 +58,26 @@ class Debugger(object):
self.LLDB = True
return super(Debugger, self).__new__(_LLDBDebugger)
+ def parse_flags(self, raw_flags, permitted_flags):
+ flags = {}
+ for flag in raw_flags:
+ if flag not in permitted_flags:
+ raise self.error('Unrecongnized option: "{}"'.format(flag))
+ flags[flag] = True
+ return flags
+
+ def extract_flags(self, arg, permitted_flags):
+ if not arg:
+ return None, None
+ flags = {}
+ if arg.startswith('/'):
+ match = re.match(r'/(\S*)\s+(.*)$', arg)
+ if not match:
+ return arg, flags
+ raw_flags, arg = match.group(1, 2)
+ flags = self.parse_flags(raw_flags, permitted_flags)
+ return arg, flags
+
def configure(self):
global PADDING, LJ_TISNUM
if not self.check_libluajit():
@@ -70,6 +90,17 @@ class Debugger(object):
self.write('luajit_dbg.py failed to load: '
'no debugging symbols found for libluajit\n')
return False
+
+ # Setup arch.
+ try:
+ self.arch = str(self.eval('LJ_ARCH_NAME')).split('"')[1]
+ except Exception:
+ try:
+ self.arch = self.detect_arch()
+ except Exception:
+ # Setup on demand if necessary.
+ pass
+
return True
def initialize_extension(self, commands):
@@ -99,21 +130,42 @@ class Debugger(object):
'''Return the content of the string by the given pointer.'''
pass
+ @abc.abstractmethod
+ def address(self, obj):
+ '''Return the address in memory of the given object.'''
+ pass
+
@abc.abstractmethod
def lookup_global(self, symbol):
'''Look up the global C symbol by the given name.'''
pass
+ @abc.abstractmethod
+ def member_by_offset(self, typename, offset, prev_name=None):
+ '''Look up the global C symbol by the given name.'''
+ pass
+
@abc.abstractmethod
def eval(self, command):
'''Parse and evaluate the given debugger command.'''
pass
+ @abc.abstractmethod
+ def detect_arch(self):
+ '''Detect the CPU architecture and canonicalize it to the LuaJIT
+ notation.'''
+ pass
+
@abc.abstractmethod
def write(self, msg):
'''Print the message.'''
pass
+ @abc.abstractmethod
+ def error(self, msg):
+ '''Create the error object with message.'''
+ pass
+
@abc.abstractmethod
def check_libluajit(self):
'''Check that libluajit is loaded.
@@ -172,10 +224,50 @@ class _GDBDebugger(Debugger):
# A string is printed with a pointer to it. Just strip it.
return re.sub(r'^0x[a-f0-9]+\s+(?=")', '', str(strptr))
+ def address(self, obj):
+ return obj.address
+
def lookup_global(self, symbol):
variable, _ = gdb.lookup_symbol(symbol)
return variable.value() if variable else None
+ def member_by_offset(self, tp, offset, prev_name=None):
+ if isinstance(tp, str):
+ tp = self._dbgtype(tp)
+ assert offset < tp.sizeof, 'offset is bigger than object size'
+ if tp.code == gdb.TYPE_CODE_TYPEDEF:
+ tp = tp.strip_typedefs()
+ if tp.code == gdb.TYPE_CODE_STRUCT:
+ fields = tp.fields()
+ for n_field in range(len(fields)):
+ islast = n_field == (len(fields) - 1)
+ field = fields[n_field]
+ start_field = field.bitpos / 8
+ end_field = fields[n_field + 1].bitpos / 8 if not islast \
+ else tp.sizeof
+ if start_field <= offset and offset < end_field:
+ next_name = self.member_by_offset(
+ field.type,
+ offset - start_field,
+ prev_name=field.name
+ )
+ return '.{field}{suffix}'.format(
+ field=field.name,
+ suffix=next_name if next_name else ''
+ )
+ elif tp.code == gdb.TYPE_CODE_ARRAY:
+ # Get array field type.
+ target = tp.target()
+ tsize = target.sizeof
+ idx = int(offset // tsize)
+ next_name = self.member_by_offset(target, offset - idx * tsize)
+ idxname = idx_name(prev_name)
+ if idxname and idx in idxname:
+ idx = idxname[idx]
+ return '[{}]{}'.format(idx, next_name if next_name else '')
+ else:
+ return None
+
def eval(self, command):
if not command:
return None
@@ -185,9 +277,23 @@ class _GDBDebugger(Debugger):
raise gdb.GdbError('table argument empty')
return ret
+ def detect_arch(self):
+ if hasattr(self, 'arch'):
+ return self.arch
+ target = str(gdb.execute('info target', False, True))
+ if re.match('.*x86-64.*', target, flags=re.DOTALL):
+ return 'x64'
+ elif re.match('.*aarch64.*', target, flags=re.DOTALL):
+ return 'arm64'
+ else:
+ return ''
+
def write(self, msg):
gdb.write(msg)
+ def error(self, errmsg):
+ return gdb.GdbError(errmsg)
+
def check_libluajit(self):
# XXX Fragile: Though connecting the callback looks bad,
# it respects both Python 2 and Python 3 (see #4828).
@@ -322,8 +428,26 @@ class _LLDBDebugger(Debugger):
def lldb__getitem__(lldbval, key):
if type(key) is lldb.value:
key = int(key)
+ if type(key) is bool:
+ key = int(key)
if type(key) is int:
# Allow array access.
+ ltp = lldbval.sbvalue.GetType()
+ # XXX: LLDB in versions 17 - 19 can't use an array
+ # object as the initializer for `lldb.value` since
+ # `GetValue()` for it returns `None` leading to
+ # the invalid result. See
+ # https://github.com/llvm/llvm-project/pull/90144.
+ if (self.version < 17 or self.version > 19) or \
+ ltp.GetTypeClass() != lldb.eTypeClassArray:
+ pass
+ else:
+ ptr_tp = ltp.GetArrayElementType().GetPointerType()
+ lldbval = self._lldb_value_from_raw(
+ lldbval.sbvalue.GetLoadAddress(),
+ ptr_tp.GetByteSize(),
+ ptr_tp
+ )
if key >= 0 and not lldbval.sbvalue.TypeIsPointerType():
return lldb.value(
lldbval.sbvalue.GetValueForExpressionPath('[%i]' % key)
@@ -349,6 +473,9 @@ class _LLDBDebugger(Debugger):
def lldb__gt__(lldbval, other):
return int(lldbval) > int(other)
+ def lldb__index__(lldbval):
+ return int(lldbval)
+
def lldb__le__(lldbval, other):
return int(lldbval) <= int(other)
@@ -406,6 +533,7 @@ class _LLDBDebugger(Debugger):
lldb.value.__ge__ = lldb__ge__
lldb.value.__getitem__ = lldb__getitem__
lldb.value.__gt__ = lldb__gt__
+ lldb.value.__index__ = lldb__index__
lldb.value.__le__ = lldb__le__
lldb.value.__lt__ = lldb__lt__
lldb.value.__str__ = lldb__str__
@@ -474,6 +602,9 @@ class _LLDBDebugger(Debugger):
def cstr(self, strptr):
return strptr.sbvalue.summary
+ def address(self, obj):
+ return lldb.value(obj.sbvalue.address_of)
+
def lookup_global(self, symbol):
sbvalue = self.target.FindFirstGlobalVariable(symbol)
tp = sbvalue.GetType()
@@ -492,6 +623,46 @@ class _LLDBDebugger(Debugger):
ptr_tp
)
+ def member_by_offset(self, tp, offset, prev_name=None):
+ if isinstance(tp, str):
+ tp = self._dbgtype(tp)
+ assert offset < tp.GetByteSize(), 'offset is bigger than object size'
+ tp = tp.GetCanonicalType()
+ if tp.GetTypeClass() == lldb.eTypeClassStruct:
+ len_fields = tp.GetNumberOfFields()
+ for n_field in range(len_fields):
+ islast = n_field == (len_fields - 1)
+ field = tp.GetFieldAtIndex(n_field)
+ start_field = field.GetOffsetInBytes()
+ if not islast:
+ end_field = tp.GetFieldAtIndex(
+ n_field + 1
+ ).GetOffsetInBytes()
+ else:
+ end_field = tp.GetByteSize()
+ if start_field <= offset and offset < end_field:
+ next_name = self.member_by_offset(
+ field.GetType(),
+ offset - start_field,
+ prev_name=field.GetName()
+ )
+ return '.{field}{suffix}'.format(
+ field=field.GetName(),
+ suffix=next_name if next_name else ''
+ )
+ if tp.GetTypeClass() == lldb.eTypeClassArray:
+ # Get array field type.
+ target = tp.GetArrayElementType()
+ tsize = target.GetByteSize()
+ idx = int(offset // tsize)
+ next_name = self.member_by_offset(target, offset - idx * tsize)
+ idxname = idx_name(prev_name)
+ if idxname and idx in idxname:
+ idx = idxname[idx]
+ return '[{}]{}'.format(idx, next_name if next_name else '')
+ else:
+ return None
+
def eval(self, command):
if not command:
return None
@@ -502,9 +673,23 @@ class _LLDBDebugger(Debugger):
ret = frame.EvaluateExpression(command)
return ret
+ def detect_arch(self):
+ if hasattr(self, 'arch'):
+ return self.arch
+ target = self.target.GetTriple().split('-')[0]
+ if target == 'x86_64':
+ return 'x64'
+ elif target == 'arm64' or target == 'aarch64':
+ return 'arm64'
+ else:
+ return ''
+
def write(self, msg):
sys.stdout.write(msg)
+ def error(self, errmsg):
+ return Exception(errmsg)
+
def check_libluajit(self):
# TODO: Implement postpone loading for LLDB too.
return True
@@ -997,6 +1182,86 @@ def J(g):
return dbg.cast('jit_State *', dbg.cast('char *', g) - g_offset + J_offset)
+# Matched `MMDEF(_)`.
+MM_NAMES = [
+ 'index',
+ 'newindex',
+ 'gc',
+ 'mode',
+ 'eq',
+ 'len',
+ 'lt',
+ 'le',
+ 'concat',
+ 'call',
+ 'add',
+ 'sub',
+ 'mul',
+ 'div',
+ 'mod',
+ 'pow',
+ 'unm',
+ 'metatable',
+ 'tostring',
+ # TODO: depends on LJ_HASFFI, see `MMDEF_FFI(_)`.
+ 'new',
+ # TODO: depends on LJ_52 || LJ_HASFFI, see `MMDEF_PAIRS(_)`.
+ 'pairs',
+ 'ipairs',
+]
+
+
+GCROOT_MMNAME = 0
+GCROOT_BASEMT = GCROOT_MMNAME + len(MM_NAMES)
+GCROOT_IO_INPUT = GCROOT_BASEMT + i2notu32(LJ_T['NUMX']) + 1
+GCROOT_IO_OUTPUT = GCROOT_IO_INPUT + 1
+
+
+# Get the name of the index in the predefined arrays.
+def idx_name(field_name):
+ # Don't use **{ to be compatible with Python 2.
+ gcroot = {}
+ gcroot.update({
+ i: 'GCROOT_MMNAME_' + MM_NAMES[i] for i in range(len(MM_NAMES))
+ })
+ gcroot.update({
+ i2notu32(LJ_T[k]) + GCROOT_BASEMT: 'GCROOT_BASEMT_' + k
+ for k in LJ_T.keys()
+ })
+ gcroot.update({
+ GCROOT_IO_INPUT: 'GCROOT_IO_INPUT',
+ GCROOT_IO_OUTPUT: 'GCROOT_IO_OUTPUT',
+ })
+ return {
+ # May be one of 2 slots depending on the result address.
+ 'ksimd': {
+ 0 * 2 + 0: 'LJ_KSIMD_ABS',
+ 0 * 2 + 1: 'LJ_KSIMD_ABS',
+ 1 * 2 + 0: 'LJ_KSIMD_NEG',
+ 1 * 2 + 1: 'LJ_KSIMD_NEG',
+ },
+ 'gcroot': gcroot,
+ }.get(field_name, None)
+
+
+ggfname_cache = {}
+
+
+# Get GG field name by given offset. Use in JIT dump.
+def ggfname_by_offset(offset):
+ if offset in ggfname_cache:
+ return ggfname_cache[offset]
+
+ field_path = dbg.member_by_offset('GG_State', offset)
+ if not field_path:
+ return None
+
+ # Remove first '.'.
+ ggfname = 'offsetof(GG, {})'.format(field_path[1:])
+ ggfname_cache[offset] = ggfname
+ return ggfname
+
+
def vm_state(g):
return {
i2notu32(0): 'INTERP',
@@ -1087,6 +1352,555 @@ def lightudV(tv):
return gcval(tv['gcr'])
+# JIT engine.
+
+
+IRS = [
+ # Guarded assertions.
+ 'LT',
+ 'GE',
+ 'LE',
+ 'GT',
+
+ 'ULT',
+ 'UGE',
+ 'ULE',
+ 'UGT',
+
+ 'EQ',
+ 'NE',
+
+ 'ABC',
+ 'RETF',
+
+ # Miscellaneous ops.
+ 'NOP',
+ 'BASE',
+ 'PVAL',
+ 'GCSTEP',
+ 'HIOP',
+ 'LOOP',
+ 'USE',
+ 'PHI',
+ 'RENAME',
+ 'PROF',
+
+ # Constants.
+ 'KPRI',
+ 'KINT',
+ 'KGC',
+ 'KPTR',
+ 'KKPTR',
+ 'KNULL',
+ 'KNUM',
+ 'KINT64',
+ 'KSLOT',
+
+ # Bit ops.
+ 'BNOT',
+ 'BSWAP',
+ 'BAND',
+ 'BOR',
+ 'BXOR',
+ 'BSHL',
+ 'BSHR',
+ 'BSAR',
+ 'BROL',
+ 'BROR',
+
+ # Arithmetic ops. ORDER ARITH
+ 'ADD',
+ 'SUB',
+ 'MUL',
+ 'DIV',
+ 'MOD',
+ 'POW',
+ 'NEG',
+
+ 'ABS',
+ 'LDEXP',
+ 'MIN',
+ 'MAX',
+ 'FPMATH',
+
+ # Overflow-checking arithmetic ops.
+ 'ADDOV',
+ 'SUBOV',
+ 'MULOV',
+
+ # Memory ops. A = array, H = hash, U = upvalue, F = field,
+ # S = stack.
+
+ # Memory references.
+ 'AREF',
+ 'HREFK',
+ 'HREF',
+ 'NEWREF',
+ 'UREFO',
+ 'UREFC',
+ 'FREF',
+ 'STRREF',
+ 'LREF',
+
+ # Loads and Stores. These must be in the same order.
+ 'ALOAD',
+ 'HLOAD',
+ 'ULOAD',
+ 'FLOAD',
+ 'XLOAD',
+ 'SLOAD',
+ 'VLOAD',
+
+ 'ASTORE',
+ 'HSTORE',
+ 'USTORE',
+ 'FSTORE',
+ 'XSTORE',
+
+ # Allocations.
+ 'SNEW',
+ 'XSNEW',
+ 'TNEW',
+ 'TDUP',
+ 'CNEW',
+ 'CNEWI',
+
+ # Buffer operations.
+ 'BUFHDR',
+ 'BUFPUT',
+ 'BUFSTR',
+
+ # Barriers.
+ 'TBAR',
+ 'OBAR',
+ 'XBAR',
+
+ # Type conversions.
+ 'CONV',
+ 'TOBIT',
+ 'TOSTR',
+ 'STRTO',
+
+ # Calls.
+ 'CALLN',
+ 'CALLA',
+ 'CALLL',
+ 'CALLS',
+ 'CALLXS',
+ 'CARG',
+]
+
+
+# Mode bits: Commutative, {Normal/Ref, Alloc, Load, Store},
+# Non-weak guard. */
+IRM_C = 0x10
+IRM_A = 0x20
+IRM_L = 0x40
+IRM_S = 0x60
+IRM_W = 0x80
+
+
+# IR operand mode (2 bit).
+IRM = [
+ 'ref',
+ 'lit',
+ 'cst',
+ '', # none
+]
+
+
+lj_ir_mode_ = None
+
+
+def lj_ir_mode():
+ global lj_ir_mode_
+ if lj_ir_mode_:
+ return lj_ir_mode_
+ lj_ir_mode_ = dbg.lookup_global('lj_ir_mode')
+ return lj_ir_mode_
+
+
+def ir_left(op):
+ return IRM[int(lj_ir_mode()[op] & 3)]
+
+
+def ir_right(op):
+ return IRM[int(lj_ir_mode()[op] >> 2 & 3)]
+
+
+def ir_mode(op):
+ mode = ''
+ ir_mode = int(lj_ir_mode()[op] ^ IRM_W)
+ if ir_mode == IRM_C:
+ mode = 'C'
+ elif ir_mode == IRM_A:
+ mode = 'A'
+ elif ir_mode == IRM_L:
+ mode = 'L'
+ elif ir_mode == IRM_S:
+ mode = 'S'
+ else:
+ mode = 'N'
+ mode += 'W' if ir_mode & IRM_W else ''
+ return mode
+
+
+IRTYPES = [
+ 'nil',
+ 'fal',
+ 'tru',
+ 'lud',
+ 'str',
+ 'p32',
+ 'thr',
+ 'pro',
+ 'fun',
+ 'p64',
+ 'cdt',
+ 'tab',
+ 'udt',
+ 'flt',
+ 'num',
+ 'i8 ',
+ 'u8 ',
+ 'i16',
+ 'u16',
+ 'int',
+ 'u32',
+ 'i64',
+ 'u64',
+ 'sfp',
+]
+
+
+IRT_NUM = 14
+assert IRTYPES[IRT_NUM] == 'num', 'incorrect IRT_NUM definition'
+
+
+IRFIELDS = [
+ 'str.len',
+ 'func.env',
+ 'func.pc',
+ 'func.ffid',
+ 'thread.env',
+ 'tab.meta',
+ 'tab.array',
+ 'tab.node',
+ 'tab.asize',
+ 'tab.hmask',
+ 'tab.nomm',
+ 'udata.meta',
+ 'udata.udtype',
+ 'udata.file',
+ 'cdata.ctypeid',
+ 'cdata.ptr',
+ 'cdata.int',
+ 'cdata.int64',
+ 'cdata.int64_4',
+]
+
+
+IRFPMS = [
+ 'floor',
+ 'ceil',
+ 'trunc',
+ 'sqrt',
+ 'exp2',
+ 'log',
+ 'log2',
+ 'other'
+]
+
+
+# Don't use *[ to be compatible with Python 2.
+REGISTERS = {'x64': [
+ 'rax',
+ 'rcx',
+ 'rdx',
+ 'rbx',
+ 'rsp',
+ 'rbp',
+ 'rsi',
+ 'rdi',
+] + [
+ 'r{}'.format(i) for i in range(8, 16) # r8 .. r15
+] + [
+ 'xmm{}'.format(i) for i in range(0, 16) # xmm0 .. xmm15
+], 'arm64': [
+ 'x{}'.format(i) for i in range(0, 31) # x0 .. x30
+] + ['sp'] + [ # x31
+ 'd{}'.format(i) for i in range(0, 32) # d0 .. d31
+]}
+
+
+IR_CALLS = [
+ 'lj_str_cmp',
+ 'lj_str_find',
+ 'lj_str_new',
+ 'lj_strscan_num',
+ 'lj_strfmt_int',
+ 'lj_strfmt_num',
+ 'lj_strfmt_char',
+ 'lj_strfmt_putint',
+ 'lj_strfmt_putnum',
+ 'lj_strfmt_putquoted',
+ 'lj_strfmt_putfxint',
+ 'lj_strfmt_putfnum_int',
+ 'lj_strfmt_putfnum_uint',
+ 'lj_strfmt_putfnum',
+ 'lj_strfmt_putfstr',
+ 'lj_strfmt_putfchar',
+ 'lj_buf_putmem',
+ 'lj_buf_putstr',
+ 'lj_buf_putchar',
+ 'lj_buf_putstr_reverse',
+ 'lj_buf_putstr_lower',
+ 'lj_buf_putstr_upper',
+ 'lj_buf_putstr_rep',
+ 'lj_buf_puttab',
+ 'lj_buf_tostr',
+ 'lj_tab_new_ah',
+ 'lj_tab_new1',
+ 'lj_tab_dup',
+ 'lj_tab_clear',
+ 'lj_tab_newkey',
+ 'lj_tab_len',
+ 'lj_gc_step_jit',
+ 'lj_gc_barrieruv',
+ 'lj_mem_newgco',
+ 'lj_math_random_step',
+ 'lj_vm_modi',
+ 'log10',
+ 'exp',
+ 'sin',
+ 'cos',
+ 'tan',
+ 'asin',
+ 'acos',
+ 'atan',
+ 'sinh',
+ 'cosh',
+ 'tanh',
+ 'fputc',
+ 'fwrite',
+ 'fflush',
+ 'lj_vm_floor',
+ 'lj_vm_ceil',
+ 'lj_vm_trunc',
+ 'sqrt',
+ 'log',
+ 'lj_vm_log2',
+ 'pow',
+ 'atan2',
+ 'ldexp',
+ 'lj_vm_tobit',
+ 'softfp_add',
+ 'softfp_sub',
+ 'softfp_mul',
+ 'softfp_div',
+ 'softfp_cmp',
+ 'softfp_i2d',
+ 'softfp_d2i',
+ 'lj_vm_sfmin',
+ 'lj_vm_sfmax',
+ 'lj_vm_tointg',
+ 'softfp_ui2d',
+ 'softfp_f2d',
+ 'softfp_d2ui',
+ 'softfp_d2f',
+ 'softfp_i2f',
+ 'softfp_ui2f',
+ 'softfp_f2i',
+ 'softfp_f2ui',
+ 'fp64_l2d',
+ 'fp64_ul2d',
+ 'fp64_l2f',
+ 'fp64_ul2f',
+ 'fp64_d2l',
+ 'fp64_d2ul',
+ 'fp64_f2l',
+ 'fp64_f2ul',
+ 'lj_carith_divi64',
+ 'lj_carith_divu64',
+ 'lj_carith_modi64',
+ 'lj_carith_modu64',
+ 'lj_carith_powi64',
+ 'lj_carith_powu64',
+ 'lj_cdata_newv',
+ 'lj_cdata_setfin',
+ 'strlen',
+ 'memcpy',
+ 'memset',
+ 'lj_vm_errno',
+ 'lj_carith_mul64',
+ 'lj_carith_shl64',
+ 'lj_carith_shr64',
+ 'lj_carith_sar64',
+ 'lj_carith_rol64',
+ 'lj_carith_ror64',
+]
+
+
+def regname(reg_number):
+ if not hasattr(dbg, 'arch'):
+ dbg.arch = dbg.detect_arch()
+ return REGISTERS[dbg.arch][reg_number]
+
+
+def litname_sload(mode):
+ modes_str = ''
+ modes_str += 'P' if mode & 0x1 else ''
+ modes_str += 'F' if mode & 0x2 else ''
+ modes_str += 'T' if mode & 0x4 else ''
+ modes_str += 'C' if mode & 0x8 else ''
+ modes_str += 'R' if mode & 0x10 else ''
+ modes_str += 'I' if mode & 0x20 else ''
+ return modes_str
+
+
+def litname_xload(mode):
+ flags = ['-', 'R', 'V', 'RV', 'U', 'RU', 'VU', 'RVU']
+ return flags[mode]
+
+
+def litname_conv(mode):
+ IRCONV_DSH = 5
+ IRCONV_CSH = 12
+ IRCONV_SEXT = 0x800
+ IRCONV_SRCMASK = 0x1f
+ conv_str = '{to}.{frm}'.format(
+ to=IRTYPES[(mode >> IRCONV_DSH) & IRCONV_SRCMASK],
+ frm=IRTYPES[mode & IRCONV_SRCMASK]
+ )
+ conv_str += ' sext' if mode & IRCONV_SEXT else ''
+ num2int_mode = mode >> IRCONV_CSH
+ if num2int_mode == 2:
+ conv_str += ' index'
+ elif num2int_mode == 3:
+ conv_str += ' check'
+ return conv_str
+
+
+def litname_irfield(mode):
+ if mode >= len(IRFIELDS):
+ return 'unknown irfield'
+ return IRFIELDS[mode]
+
+
+def litname_fpm(mode):
+ if mode >= len(IRFPMS):
+ return 'unknown irfpm'
+ return IRFPMS[mode]
+
+
+def litname_bufhdr(mode):
+ modes = ['RESET', 'APPEND']
+ if mode >= len(modes):
+ return 'unknown bufhdr mode'
+ return modes[mode]
+
+
+def litname_tostr(mode):
+ modes = ['INT', 'NUM', 'CHAR']
+ if mode >= len(modes):
+ return 'unknown tostr mode'
+ return modes[mode]
+
+
+IR_LITNAMES = {
+ 'SLOAD': litname_sload,
+ 'XLOAD': litname_xload,
+ 'CONV': litname_conv,
+ 'FLOAD': litname_irfield,
+ 'FREF': litname_irfield,
+ 'FPMATH': litname_fpm,
+ 'BUFHDR': litname_bufhdr,
+ 'TOSTR': litname_tostr
+}
+
+# Additional flags.
+IRT_MARK = 0x20 # Marker for misc. purposes.
+IRT_ISPHI = 0x40 # Instruction is left or right PHI operand.
+IRT_GUARD = 0x80 # Instruction is a guard.
+# Masks.
+IRT_TYPE = 0x1f
+
+RID_NONE = 0x80
+RID_MASK = 0x7f
+RID_INIT = (RID_NONE | RID_MASK)
+RID_SINK = (RID_INIT - 1)
+RID_SUNK = (RID_INIT - 2)
+# Spill slot 0 means no spill slot has been allocated.
+SPS_NONE = 0
+
+REF_BIAS = 0x8000
+
+TREF_SHIFT = 24
+
+TREF_REFMASK = 0x0000ffff
+TREF_FRAME = 0x00010000
+TREF_CONT = 0x00020000
+# Snapshot flags and masks.
+SNAP_FRAME = 0x010000
+SNAP_SOFTFPNUM = 0x080000
+
+
+def irt_type(t):
+ return dbg.cast('IRType', t['irt'] & IRT_TYPE)
+
+
+def tref_type(tr):
+ return dbg.cast('IRType', (tr >> TREF_SHIFT) & IRT_TYPE)
+
+
+def tref_ref(tr):
+ return int(tr & TREF_REFMASK)
+
+
+def irt_ismarked(t):
+ return t['irt'] & IRT_MARK
+
+
+def irt_isphi(t):
+ return t['irt'] & IRT_ISPHI
+
+
+def irt_isguard(t):
+ return t['irt'] & IRT_GUARD
+
+
+def irt_toitype(irt):
+ t = irt_type(irt)
+ if LJ_DUALNUM and t > IRT_NUM:
+ return LJ_T['NUMX']
+ else:
+ return i2notu32(t)
+
+
+def ir_kptr(ir):
+ irname = IRS[ir['o']]
+ assert irname == 'KPTR' or irname == 'KKPTR', 'wrong IR for ir_iptr()'
+ return mref('void *', dbg.cast('IRIns *', dbg.address(ir))[LJ_GC64]['ptr'])
+
+
+def ir_kgc(ir):
+ irname = IRS[ir['o']]
+ assert irname == 'KGC', 'wrong IR for ir_kgc()'
+ return gcref(dbg.cast('IRIns *', dbg.address(ir))[LJ_GC64]['gcr'])
+
+
+def ir_knum(ir):
+ irname = IRS[ir['o']]
+ assert irname == 'KNUM', 'wrong IR for ir_knum()'
+ return dbg.address(dbg.cast('IRIns *', dbg.address(ir))[1]['tv'])
+
+
+def ir_kint64(ir):
+ irname = IRS[ir['o']]
+ assert irname == 'KINT64', 'wrong IR for ir_knum()'
+ return dbg.address(dbg.cast('IRIns *', dbg.address(ir))[1]['tv'])
+
+
# Dumpers.
# GCobj dumpers.
@@ -1467,6 +2281,325 @@ def dump_func(func):
return 'fast function #{}\n'.format(int(ffid))
+# JIT dumpers.
+
+
+def dump_call_func(trace, callop):
+ ctype = ''
+ if callop > 0:
+ ir = trace['ir'][REF_BIAS + callop]
+ if IRTYPES[irt_type(ir['t'])] == 'nil': # nil == CARG(func, ctype)
+ callop = int(ir['op1']) - REF_BIAS
+ cdt_idx_irk = trace['ir'][ir['op2']]
+ assert IRS[cdt_idx_irk['o']] == 'KINT', \
+ 'unexpected IR for ctype storage'
+ ctype_idx = cdt_idx_irk['i']
+ ctype = 'ctype: {}'.format(ctype_idx)
+
+ func_str = ''
+ if callop < 0:
+ irk = trace['ir'][REF_BIAS + callop]
+ assert IRS[irk['o']] == 'KINT64', \
+ 'unexpected IR for FFI function storage'
+ func_addr = int(ir_kint64(irk)['u64'])
+ # TODO: Symbol demangling.
+ func_str = '[{:#x}]'.format(func_addr)
+ else:
+ func_str = '[{:04d}]'.format(callop)
+
+ return func_str, ctype
+
+
+def dump_call_args(trace, ins):
+ if ins < 0:
+ return '{{{}}}'.format(dump_irk(trace, ins))
+ else:
+ ir = trace['ir'][REF_BIAS + ins]
+ irname = IRS[ir['o']]
+ if irname == 'CARG':
+ last_arg = ''
+ args = dump_call_args(trace, int(ir['op1']) - REF_BIAS)
+ op2 = int(ir['op2']) - REF_BIAS
+ if op2 < 0:
+ last_arg = '{{{}}}'.format(dump_irk(trace, op2))
+ else:
+ last_arg = '{{{:04d}}}'.format(op2)
+ return args + ', ' + last_arg
+ else:
+ return '{{{:04d}}}'.format(ins)
+
+
+# Special FP constant.
+CONST_BIAS = 2 ** 52 + 2 ** 51
+
+
+def dump_irk(trace, idx):
+ ref = idx + REF_BIAS
+ assert ref >= trace['nk'] and ref < REF_BIAS, 'bad constant in IR dump'
+ irins = trace['ir'][ref]
+ irname = IRS[irins['o']]
+ slot = ''
+ if irname == 'KSLOT':
+ slot = ' KSLOT: @{}'.format(int(irins['op2']))
+ irins = trace['ir'][irins['op1']]
+ irname = IRS[irins['o']]
+
+ irtype = irins['t']
+ if irname == 'KPRI':
+ typename = typenames(irt_toitype(irtype))
+ # Trivial dump for primitives.
+ irk = tv_dumpers.get(
+ typename, dump_lj_tv_invalid # noqa: F821 # Generated.
+ )(0)
+ elif irname == 'KINT':
+ irk = 'integer {}'.format(dbg.cast('int32_t', irins['i']))
+ elif irname == 'KGC':
+ typename = typenames(irt_toitype(irtype))
+ irk = gco_dumpers.get(typename, dump_lj_gco_invalid)(ir_kgc(irins))
+ elif irname == 'KKPTR':
+ addr = ir_kptr(irins)
+ if addr == dbg.address(G(L())['nilnode']):
+ return '[g->nilnode]' + slot
+ irk = '[{}]'.format(strx64(addr))
+ elif irname == 'KPTR':
+ irk = '[{}]'.format(strx64(ir_kptr(irins)))
+ elif irname == 'KNULL':
+ irk = 'NULL'
+ elif irname == 'KNUM':
+ tv_num = ir_knum(irins)
+ if float(tv_num['n']) == CONST_BIAS:
+ return 'bias'
+ irk = dump_lj_tv_numx(tv_num)
+ elif irname == 'KINT64':
+ irk = 'int64_t {}'.format(dbg.cast(
+ 'int64_t', int(ir_kint64(irins)['u64'])
+ ))
+ else:
+ return 'Unknown IRK: ' + irname
+ return irk + slot
+
+
+def dump_irins(irins, trace=None):
+ irop = int(irins['o'])
+ if irop >= len(IRS):
+ return 'INVALID'
+
+ irname = IRS[irop]
+ leftop = ir_left(irop)
+ rightop = ir_right(irop)
+ irt = irins['t']
+ is_sinksunk = irins['r'] == RID_SINK or irins['r'] == RID_SUNK
+ flags = '{is_sinksunk}{is_marked}{is_guard}{is_phi}'.format(
+ # Sink flag should be the first to match sink slots during
+ # the dump of registers.
+ is_sinksunk='}' if is_sinksunk else ' ',
+ is_marked='!' if irt_ismarked(irt) else ' ',
+ is_guard='>' if irt_isguard(irt) else ' ',
+ is_phi='+' if irt_isphi(irt) else ' '
+ )
+
+ if not trace:
+ g = G(L(None))
+ compiling = jit_state(g) != 'IDLE'
+ assert compiling, 'attempt to dump IR for J.cur trace in bad VM state'
+ trace = J(g)['cur']
+
+ left = ''
+ right = ''
+ lisref = leftop == 'ref'
+ risref = rightop == 'ref'
+ op1 = int((irins['op1'] - REF_BIAS) if lisref else irins['op1'])
+ op2 = int((irins['op2'] - REF_BIAS) if risref else irins['op2'])
+
+ skip_right = False
+ if re.match('CALL', irname):
+ ctype = ''
+ args = ''
+ if rightop == 'lit':
+ func = IR_CALLS[op2]
+ else:
+ func, ctype = dump_call_func(trace, op2)
+
+ if op1 != -1:
+ args = dump_call_args(trace, int(op1))
+
+ return '{flags} {type} {name:6} [{mode:2}] {f}({args}) {ct}\n'.format(
+ flags=flags,
+ name=irname,
+ mode=ir_mode(irop),
+ type=IRTYPES[irt_type(irt)],
+ ct=ctype,
+ args=args,
+ f=func,
+ )
+ elif irname == 'CNEW' and op2 == -1:
+ left = dump_irk(trace, op1)
+ skip_right = True
+ elif leftop:
+ if op1 < 0:
+ left = dump_irk(trace, op1)
+ elif leftop == 'cst':
+ idx = irins - dbg.address(trace['ir'][REF_BIAS])
+ left = dump_irk(trace, idx)
+ else:
+ left = ('{:04d}' if lisref else '#{:<3d}').format(op1)
+
+ if rightop:
+ if rightop == 'lit':
+ litname = IR_LITNAMES.get(irname, None)
+ if litname:
+ # Try to handle `lj_ir_ggfload()`.
+ ggfname = None
+ if irname == 'FLOAD' and left == 'nil' \
+ and op2 >= len(IRFIELDS):
+ ggfname = ggfname_by_offset(op2 << 2)
+
+ if ggfname:
+ right = ggfname
+ else:
+ right = litname(op2)
+ elif irname == 'UREFO' or irname == 'UREFC':
+ right = '#{:<3d}'.format(op2 >> 8)
+ else:
+ right = '#{:<3d}'.format(op2)
+ elif op2 < 0:
+ right = dump_irk(trace, op2)
+ else:
+ right = ('{:04d}').format(op2)
+
+ typename = ''
+ if irname == 'LOOP':
+ typename = '---'
+ elif irname == 'NOP':
+ typename = ' '
+ else:
+ typename = IRTYPES[irt_type(irt)]
+
+ return '{flags} {type} {name:6} [{mode:2}] {left:<9s} {right}\n'.format(
+ flags=flags,
+ name=irname,
+ mode=ir_mode(irop),
+ type=typename,
+ left=(leftop + ': ' + left) if leftop else '',
+ right=(rightop + ': ' + right) if rightop and not skip_right else '',
+ )
+
+
+def dump_snap(trace, snapno, snap):
+ dump = 'SNAP #{:<3d} ['.format(snapno)
+ snap_map = dbg.address(trace['snapmap'][snap['mapofs']])
+ snap_entry_num = 0
+ for slot in range(0, snap['nslots']):
+ dump += ' '
+ snap_entry = int(snap_map[snap_entry_num])
+ if snap_entry_num < snap['nent'] and snap_entry >> TREF_SHIFT == slot:
+ snap_entry_num += 1
+ ref = int((snap_entry & TREF_REFMASK) - REF_BIAS)
+ if ref < 0:
+ if int(snap_entry) == 0x1057fff:
+ dump += '----'
+ continue
+ elif (snap_entry & TREF_CONT):
+ dump += 'contpc'
+ elif (snap_entry & TREF_FRAME):
+ dump += 'ftsz '
+ else:
+ dump += '{{{const}}}'.format(const=dump_irk(trace, ref))
+ elif snap_entry & SNAP_SOFTFPNUM:
+ dump += '{:04d}/{:04d}'.format(ref, ref + 1)
+ else:
+ dump += '{:04d}'.format(ref)
+
+ if snap_entry & SNAP_FRAME:
+ dump += '|'
+ else:
+ dump += '----'
+
+ dump += ' ]\n'
+ return dump
+
+
+def dump_sink_slot(rid, spill, ins_number):
+ assert rid == RID_SINK or rid == RID_SUNK, 'incorrect rid in sink dump'
+ tp = 'sink' if rid == RID_SINK else 'sunk'
+ return '{{{}'.format(tp) if spill == RID_INIT or spill == SPS_NONE \
+ else '{{{:04d}'.format(int(ins_number - spill))
+
+
+def dump_regsp(irins, ins_number):
+ rid = irins['r']
+ spill = irins['s']
+ if rid == RID_SINK or rid == RID_SUNK:
+ return dump_sink_slot(rid, spill, ins_number)
+ elif irins['prev'] > 255:
+ return '[{:#05x}]'.format(int(spill * 4))
+ elif rid < 128:
+ return regname(rid)
+ else:
+ return ''
+
+
+def dump_trace(trace, flags):
+ dump = 'Trace {num} start\n\tproto: {start_pt}\n\tBC: {start_bc}\n'.format(
+ num=trace['traceno'],
+ start_pt=gcref(trace['startpt']),
+ start_bc=mref('BCIns *', trace['startpc']),
+ )
+
+ nins = trace['nins'] - REF_BIAS
+ dump += '---- TRACE IR\n'
+ nsnap = 0
+ snap = trace['snap'][nsnap]
+ snapref = snap['ref']
+ for irnum in range(1, nins):
+ irref = REF_BIAS + irnum
+ if 's' in flags and irref >= snapref and nsnap < trace['nsnap']:
+ dump += '.... '
+ if 'r' in flags:
+ dump += ' ' * 7
+ dump += dump_snap(trace, nsnap, snap)
+ nsnap += 1
+ snap = trace['snap'][nsnap]
+ snapref = snap['ref']
+ dump += '{:04d} '.format(irnum)
+ if 'r' in flags:
+ dump += '{:>7}'.format(dump_regsp(trace['ir'][irref], irnum))
+ dump += dump_irins(trace['ir'][irref], trace)
+ return dump
+
+
+def dump_tref(tref):
+ return '[{F}{C}] {tp} {ref:#x}'.format(
+ F='F' if tref & TREF_FRAME else ' ',
+ C='C' if tref & TREF_CONT else ' ',
+ tp=IRTYPES[tref_type(tref)],
+ ref=tref_ref(tref)
+ )
+
+
+def dump_jslots(coroutine):
+ lstate = L(None)
+ g = G(lstate or coroutine)
+ j = J(g)
+
+ dump = ''
+ maxslot = j['baseslot'] + j['maxslot']
+ first_base_slot = 1 + LJ_FR2
+ for n in reversed(range(first_base_slot, maxslot)):
+ tref = j['slot'][n]
+ ref = tref_ref(tref)
+ address = dbg.address(tref)
+ dump += '{addr} {nslot:04d} {base:1s} {tref}{const}\n'.format(
+ addr=address,
+ base='B' if address == j['base'] else ' ',
+ nslot=n,
+ tref=dump_tref(tref),
+ const=' ' + dump_irk(j['cur'], ref - REF_BIAS)
+ if ref != 0 and ref < REF_BIAS else ''
+ )
+ return dump
+
+
# Extension commands. ############################################
@@ -1600,6 +2733,42 @@ error message occurs.
dbg.write('{}\n'.format(dump_gcobj(gcobj)))
+class LJDumpIR(dbg.LJBase):
+ '''
+lj-ir <IRIns *>
+
+The command receives a pointer to <ir> (IRIns address) and dumps
+the IR type and some info related to it. The format is similar to
+the `jit.dump` tool but also provides information about IR mode and
+operands modes.
+
+For the list of IR names and modes (operand types), see:
+https://github.com/tarantool/tarantool/wiki/LuaJIT-SSA-IR.
+ '''
+
+ def execute(self, arg):
+ dbg.write('{}'.format(dump_irins(dbg.cast('IRIns *', dbg.eval(arg)))))
+
+
+class LJDumpJSlots(dbg.LJBase):
+ '''
+lj-jslots [<lua_State *>]
+
+The command receives an optional lua_State address and dumps the
+slots of JIT stack map:
+
+<slot ptr> <slot number> [<FRAME|CONTINUATION>] <IR reference>
+
+The lua_State pointer is optional to help in finding the VM's JIT state
+when there is no coroutine to be inspected in the debugged frame.
+ '''
+
+ def execute(self, arg):
+ dbg.write('{}'.format(
+ dump_jslots(dbg.cast('lua_State *', dbg.eval(arg)))
+ ))
+
+
class LJDumpProto(dbg.LJBase):
'''
lj-proto <GCproto *>
@@ -1784,19 +2953,44 @@ error message occurs.
dbg.write('{}\n'.format(dump_tvalue(tv)))
+class LJDumpTrace(dbg.LJBase):
+ '''
+lj-trace [/FLAGS] <GCtrace *>
+
+The command receives a pointer to <trace> (IRIns address) and dumps
+its number, IRs, and information about start location. The format is
+similar to the `jit.dump` tool but also provides information about
+IR mode and operands modes.
+
+Trace may be preceded with /FLAGS:
+* r: Dump registers associated with IR, if any.
+* s: Dump snapshots for the trace.
+ '''
+
+ def execute(self, arg):
+ arg, flags = dbg.extract_flags(arg, 'rs')
+ dbg.write('{}'.format(dump_trace(
+ dbg.cast('GCtrace *', dbg.eval(arg)),
+ flags
+ )))
+
+
def load(event=None):
dbg.initialize_extension({
- 'lj-arch': LJDumpArch,
- 'lj-bc': LJDumpBC,
- 'lj-func': LJDumpFunc,
- 'lj-gc': LJGC,
- 'lj-gco': LJDumpGCobj,
- 'lj-proto': LJDumpProto,
- 'lj-stack': LJDumpStack,
- 'lj-state': LJState,
- 'lj-str': LJDumpString,
- 'lj-tab': LJDumpTable,
- 'lj-tv': LJDumpTValue,
+ 'lj-arch': LJDumpArch,
+ 'lj-bc': LJDumpBC,
+ 'lj-func': LJDumpFunc,
+ 'lj-gc': LJGC,
+ 'lj-gco': LJDumpGCobj,
+ 'lj-ir': LJDumpIR,
+ 'lj-jslots': LJDumpJSlots,
+ 'lj-proto': LJDumpProto,
+ 'lj-stack': LJDumpStack,
+ 'lj-state': LJState,
+ 'lj-str': LJDumpString,
+ 'lj-tab': LJDumpTable,
+ 'lj-trace': LJDumpTrace,
+ 'lj-tv': LJDumpTValue,
})
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index 7e8ea5a2..76543daa 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -46,7 +46,9 @@ else:
RX_ADDR = r'0x[a-f0-9]+'
RX_HASH = RX_ADDR # The same pattern for hexademic values.
RX_BCN = r'00\d\d'
+RX_IRN = RX_BCN # The same as for the bytecodes.
RX_FRAME = r'\[(S|\s)(B|\s)(T|\s)(M|\s)\]'
+RX_IRREF = r'0x\d\d\d\d'
def persist(data):
@@ -101,6 +103,9 @@ IS_GC64 = execute_process([
LUAJIT_BINARY, '-e', "print(require('ffi').abi('gc64'))"
]).strip() == 'true'
+# Regexp for pointer type in IR.
+RX_P = 'p64' if IS_GC64 else 'p32'
+
# If it is the guaranteed DUALNUM build (for example, on aarch64),
# we use this regexp for the guaranteed 'integer' check and
# 'number' for single-number build.
@@ -108,6 +113,18 @@ RX_INT = r'integer' if IS_DUALNUM else r'number'
RX_ISDUALNUM = r'True' if IS_DUALNUM else r'False'
+# Assume not cross-platform debugging.
+machine = os.uname().machine
+if machine == 'x86_64':
+ RX_GPR = r'r\w\w'
+ RX_FPR = r'xmm\d+'
+elif machine == 'arm64' or machine == 'aarch64':
+ RX_GPR = r'x\d+'
+ RX_FPR = r'd\d+'
+else:
+ raise Exception('Unknown archeticture in testing')
+
+
class TestCaseBase(unittest.TestCase):
@classmethod
def construct_cmds(cls):
@@ -193,6 +210,16 @@ def mref(arg, tp):
return '((' + tp + '*)(' + arg + ').ptr32)'
+def gcref(arg):
+ if SUPPORT_MACRO_EXPAND:
+ return 'gcref(' + arg + ')'
+ else:
+ if IS_GC64:
+ return '(' + arg + ').gcptr64'
+ else:
+ return '(' + arg + ').gcptr32'
+
+
class TestLoad(TestCaseBase):
extension_cmds = ''
location = 'lj_cf_print'
@@ -203,11 +230,14 @@ class TestLoad(TestCaseBase):
r'lj-func command initialized\n'
r'lj-gc command initialized\n'
r'lj-gco command initialized\n'
+ r'lj-ir command initialized\n'
+ r'lj-jslots command initialized\n'
r'lj-proto command initialized\n'
r'lj-stack command initialized\n'
r'lj-state command initialized\n'
r'lj-str command initialized\n'
r'lj-tab command initialized\n'
+ r'lj-trace command initialized\n'
r'lj-tv command initialized\n'
r'LuaJIT debug extension is successfully loaded'
)
@@ -473,6 +503,341 @@ class TestLJBC(TestCaseBase):
)
+# JIT engine.
+
+
+class TestLJTraceBase(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'for _ = 1, 4 do end\n'
+ 'print()\n'
+ )
+ pattern = (
+ r'Trace 1 start\n'
+ r'\t*proto: ' + RX_ADDR + r'\n' +
+ r'\t*BC: ' + RX_ADDR + r'\n' +
+ r'---- TRACE IR\n' +
+ RX_IRN + r'\s+ int SLOAD \[N \] lit: #[12] lit: C?I\n' +
+ RX_IRN + r'\s+ \+ int ADD \[C \] ref: ' + RX_IRN +
+ r' ref: integer 1\n' +
+ RX_IRN + r'\s+ > int LE \[N \] ref: ' + RX_IRN +
+ r' ref: integer 4\n' +
+ RX_IRN + r'\s+ > --- LOOP \[N \]\s*\n' +
+ RX_IRN + r'\s+ \+ int ADD \[C \] ref: ' + RX_IRN +
+ r' ref: integer 1\n' +
+ RX_IRN + r'\s+ > int LE \[N \] ref: ' + RX_IRN +
+ r' ref: integer 4\n' +
+ RX_IRN + r'\s+ int PHI \[S \] ref: ' + RX_IRN + r' ref: ' +
+ RX_IRN + r'\n' +
+ RX_IRN + r'\s+ NOP \[N \]\s*\n'
+ )
+
+
+# Check the IR enumeration correcness by test the lowest (LT) and
+# the highest (CARG) IRs. Also, checks CALL* occasionally.
+class TestLJTraceIRRange(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'local ffi = require("ffi")\n'
+ 'ffi.cdef[[int getpid(int, int);]]\n' # Use argument for testing.
+ 'jit.opt.start("hotloop=1")\n'
+ 'for i = 1, 4 do\n'
+ ' if i < 100 then\n' # LT.
+ ' ffi.C.getpid(i, 1LL)\n' # CARG and CALLXS.
+ ' end\n'
+ 'end\n'
+ 'print()\n'
+ )
+ # IRs from variant part of the trace.
+ pattern = (
+ RX_IRN + r'\s+ > int LT \[N \] ref: ' +
+ RX_IRN + r' ref: integer 100\n' +
+ RX_IRN + r'\s+ nil CARG \[N \] ref: ' +
+ RX_IRN + r' ref: integer 1\n' +
+ RX_IRN + r'\s+ int CALLXS \[S \] \[' + RX_ADDR +
+ r'\]\(\{' + RX_IRN + r'\}, \{integer 1\}\)'
+ )
+
+
+# Test /rs flags.
+class TestLJTraceFlags(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace /rs ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local r = 0.1\n'
+ 'for i = 1, 4 do\n'
+ ' r = i + r\n'
+ 'end\n'
+ 'print()\n'
+ )
+ # IRs and snapshot from variant part of the trace.
+ pattern = (
+ RX_IRN + r'\s+' + RX_FPR + r'\s* \+ num ADD.*\n' +
+ RX_IRN + r'\s+' + RX_GPR + r'\s* \+ int ADD.*\n' +
+ r'\.\.\.\.\s* SNAP #\d \[ (---- )*' + RX_IRN + r' \]'
+ )
+
+
+class TestLJIRConst(TestCaseBase):
+ location = 'trace_stop'
+
+ # No narrowing of 42.
+ if IS_DUALNUM:
+ # KNUM occupies 2 slots.
+ _knum_irnum = '6'
+ _kgc_irnum = '8' if IS_GC64 else '7'
+ _kptr_irnum = '10' if IS_GC64 else '8'
+ else:
+ # KNUM occupies 2 slots.
+ _knum_irnum = '8'
+ _kgc_irnum = '10' if IS_GC64 else '9'
+ _kptr_irnum = '12' if IS_GC64 else '10'
+ extension_cmds = (
+ 'n\n' # Load J.
+ 'lj-ir &J->cur.ir[0x8000 - 0]\n'
+ 'lj-ir &J->cur.ir[0x8000 - 1]\n'
+ 'lj-ir &J->cur.ir[0x8000 - 2]\n'
+ 'lj-ir &J->cur.ir[0x8000 - 3]\n'
+ 'lj-ir &J->cur.ir[0x8000 - 4]\n'
+ # Skip non-DUALNUM narrowed value.
+ 'lj-ir &J->cur.ir[0x8000 - ' + _knum_irnum + ']\n'
+ 'lj-ir &J->cur.ir[0x8000 - ' + _kgc_irnum + ']\n'
+ 'lj-ir &J->cur.ir[0x8000 - ' + _kptr_irnum + ']\n'
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local function trace(x)\n'
+ ' return x + 42, x + 0.5, x .. "1"\n'
+ 'end\n'
+ 'trace(1)\n'
+ 'trace(1)\n'
+ )
+ pattern = (
+ RX_P + r' BASE.*\n' +
+ r'\s* nil KPRI.*\n'
+ r'\s* fal KPRI.*\n'
+ r'\s* tru KPRI.*\n'
+ r'\s* int KINT.*cst: integer 42\s*\n'
+ r'\s* num KNUM.*cst: number 0.5\s*\n'
+ r'\s* str KGC.*cst: string "1".*\n' +
+ r'\s*' + RX_P + r' KPTR.*cst: \[' + RX_ADDR + r'\]'
+ )
+
+
+class TestLJIRFloadNeg(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local function trace(a)\n'
+ ' local x = -a\n'
+ ' return x\n'
+ 'end\n'
+ 'trace(1.1)\n'
+ 'trace(1.1)\n'
+ 'print()\n'
+ )
+ pattern = (
+ r'num FLOAD .* ref: nil lit: offsetof\(GG, J\.ksimd\[LJ_KSIMD_NEG\]\)'
+ )
+
+
+class TestLJIRFloadAbs(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local math_abs = math.abs\n'
+ 'local function trace(a)\n'
+ ' local x = math_abs(a)\n'
+ ' return x\n'
+ 'end\n'
+ 'trace(1)\n'
+ 'trace(1)\n'
+ 'print()\n'
+ )
+ pattern = (
+ r'num FLOAD .* ref: nil lit: offsetof\(GG, J\.ksimd\[LJ_KSIMD_ABS\]\)'
+ )
+
+
+# XXX: Implemented only for GC64 in LuaJIT until backporting the
+# corresponding commit.
+if IS_GC64:
+ class TestLJIRFloadGCRootBaseMT(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local function trace(a)\n'
+ 'local x = a.sub(1, 2)\n'
+ ' return x\n'
+ 'end\n'
+ 'trace("12")\n'
+ 'trace("12")\n'
+ 'print()\n'
+ )
+ pattern = (
+ r'tab FLOAD .* ref: nil lit: '
+ r'offsetof\(GG, g\.gcroot\[GCROOT_BASEMT_STR\]\.gcptr64\)'
+ )
+
+ class TestLJIRFloadGCRootIO(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local io_flush = io.flush\n'
+ 'local function trace()\n'
+ ' io_flush()\n'
+ 'end\n'
+ 'trace()\n'
+ 'trace()\n'
+ 'print()\n'
+ )
+ pattern = (
+ r'udt FLOAD .* ref: nil lit: '
+ r'offsetof\(GG, g\.gcroot\[GCROOT_IO_OUTPUT\]\.gcptr64\)'
+ )
+
+
+# Some IRs related to tables.
+class TestLJIRTable(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local function trace(t)\n'
+ ' t.a = nil\n'
+ ' t.b = 1\n'
+ ' return t\n'
+ 'end\n'
+ 'trace({a = 1})\n'
+ 'trace({a = 1})\n'
+ 'print()\n'
+ )
+ pattern = (
+ r'(?s)int FLOAD .* tab\.hmask\n'
+ r'.*' + RX_P + r' FLOAD .* tab\.node\n'
+ r'.*' + RX_P + r' HREFK .* string "a" @ ' + RX_ADDR +
+ r' KSLOT: @\d\n'
+ r'.*' + RX_P + r' HREF .* string "b" @ ' + RX_ADDR + r'\s*\n'
+ r'.*' + RX_P + r' EQ .* \[g->nilnode\]'
+ )
+
+
+class TestLJIRUref(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'local uv = 0\n'
+ 'local function trace(a)\n'
+ ' uv = a\n'
+ ' return uv\n'
+ 'end\n'
+ 'trace(1)\n'
+ 'trace(1)\n'
+ 'print()\n'
+ )
+ pattern = r'UREFO .* lit: #0'
+
+
+# Check border values (that always avalable) of CALL IRs.
+class TestLJIRCall(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'local ffi = require("ffi")\n'
+ 'jit.opt.start("hotloop=1")\n'
+ 'local function trace(a, b)\n'
+ ' return a < b, ffi.errno()\n'
+ 'end\n'
+ 'trace("abc", "abd")\n'
+ 'trace("abc", "abd")\n'
+ 'print(1)\n'
+ )
+ pattern = (
+ r'(?s)int CALLN .* '
+ r'lj_str_cmp\(\{' + RX_IRN + r'\}, \{' + RX_IRN + r'\}\)'
+ r'.*int CALLS .* lj_vm_errno\(\)'
+ )
+
+
+# Test ffi call with ctype stored in CARG.
+class TestLJIRCallXSCType(TestCaseBase):
+ location = 'lj_cf_print'
+ extension_cmds = (
+ 'n\n' # Load L.
+ 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
+ )
+ lua_script = (
+ 'local ffi = require("ffi")\n'
+ 'ffi.cdef[[int printf(const char *fmt, ...);]]\n'
+ 'jit.opt.start("hotloop=1")\n'
+ 'local function trace()\n'
+ ' local t = ffi.C.printf("")\n'
+ ' return t\n'
+ 'end\n'
+ 'trace()\n'
+ 'trace()\n'
+ 'print()\n'
+ )
+ pattern = r'int CALLXS .* [' + RX_ADDR + r'\]\(.*\) ctype: \d+'
+
+
+class TestLJJSlotsBase(TestCaseBase):
+ location = 'trace_stop'
+ extension_cmds = (
+ 'n\n' # Load J.
+ 'lj-jslots J->L\n'
+ )
+ lua_script = (
+ 'jit.opt.start("hotloop=1")\n'
+ 'for _ = 1, 4 do end\n'
+ )
+ pattern = (
+ r'(?s)(.*' +
+ RX_ADDR + ' ' + RX_IRN + r' (B|\s) \[(F|\s)(C|\s)\] \w\w\w ' +
+ RX_IRREF +
+ r'.*)+'
+ )
+
+
for test_cls in TestCaseBase.__subclasses__():
test_cls.test = lambda self: self.check()
--
2.54.0
next prev parent reply other threads:[~2026-06-25 20:30 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-25 20:29 [Tarantool-patches] [PATCH luajit 0/3] Extend debug extension Sergey Kaplun via Tarantool-patches
2026-06-25 20:29 ` Sergey Kaplun via Tarantool-patches [this message]
2026-06-25 20:29 ` [Tarantool-patches] [PATCH luajit 2/3] dbg: introduce lj-ctype command, extend cdata dump Sergey Kaplun via Tarantool-patches
2026-06-25 20:29 ` [Tarantool-patches] [PATCH luajit 3/3] test: add verbose mode for debug extension tests Sergey Kaplun via Tarantool-patches
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260625202903.3157425-2-skaplun@tarantool.org \
--to=tarantool-patches@dev.tarantool.org \
--cc=e.temirgaleev@tarantool.org \
--cc=sergeyb@tarantool.org \
--cc=skaplun@tarantool.org \
--subject='Re: [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers' \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox