* [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers
@ 2026-05-19 12:39 Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 1/6] test: introduce tests for debugging extensions Sergey Kaplun via Tarantool-patches
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-05-19 12:39 UTC (permalink / raw)
To: Mikhail Elhimov, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
This patchset adds tests for LLDB and GDB debugger extensions via the
first commit. The next two commits refactor the LLDB extension and sort
initialized commands alphabetically. The fourth commit adds the
lightuserdata support for LJ_64 for LLDB too. The next commit joins
extension logic into the single file, making the LuaJIT-related code
debugger-agnostic. The last commit adds the CI workflow for arm64 and
x86_64 architectures.
Branch: https://github.com/tarantool/luajit/tree/skaplun/unified-debugger
Changes in the v2:
Moved squashed part of the refactoring from the third commit to the
second one.
Maksim Kokryashkin (1):
test: introduce tests for debugging extensions
Sergey Kaplun (5):
lldb: refactor extension
dbg: sort initialization of commands
lldb: support full-range 64-bit lightuserdata
dbg: generalize extension
ci: introduce workflow to test debugger extension
.flake8rc | 5 -
.github/actions/setup-debuggers/README.md | 13 +
.github/actions/setup-debuggers/action.yml | 12 +
.github/workflows/debuggers.yml | 61 +
src/luajit-gdb.py | 884 ------------
src/luajit_dbg.py | 1281 +++++++++++++++++
src/luajit_lldb.py | 1124 ---------------
test/CMakeLists.txt | 7 +
test/tarantool-debugger-tests/CMakeLists.txt | 93 ++
.../debug-extension-tests.py | 292 ++++
10 files changed, 1759 insertions(+), 2013 deletions(-)
create mode 100644 .github/actions/setup-debuggers/README.md
create mode 100644 .github/actions/setup-debuggers/action.yml
create mode 100644 .github/workflows/debuggers.yml
delete mode 100644 src/luajit-gdb.py
create mode 100644 src/luajit_dbg.py
delete mode 100644 src/luajit_lldb.py
create mode 100644 test/tarantool-debugger-tests/CMakeLists.txt
create mode 100644 test/tarantool-debugger-tests/debug-extension-tests.py
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH v2 luajit 1/6] test: introduce tests for debugging extensions
2026-05-19 12:39 [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers Sergey Kaplun via Tarantool-patches
@ 2026-05-19 12:39 ` Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 2/6] lldb: refactor extension Sergey Kaplun via Tarantool-patches
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-05-19 12:39 UTC (permalink / raw)
To: Mikhail Elhimov, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
From: Maxim Kokryashkin <m.kokryashkin@tarantool.org>
This patch adds tests for LuaJIT debugging extensions for lldb and gdb.
The tests are written in Python's unittest framework [1].
Most of the tests are failed for the lldb due to outdated extension
sources and overcomplicated hard-coded C structures fields
introspection. Hence, tests for LLDB are disabled since they are failing
anyway.
The tarantool-debugger-tests target is introduced. This target is
included in the LuaJIT-check-all target but not in the LuaJIT-test
target to avoid it running for all LuaJIT builds by default in CI.
[1]: https://docs.python.org/3/library/unittest.html
---
test/CMakeLists.txt | 7 +
test/tarantool-debugger-tests/CMakeLists.txt | 93 ++++++
.../debug-extension-tests.py | 286 ++++++++++++++++++
3 files changed, 386 insertions(+)
create mode 100644 test/tarantool-debugger-tests/CMakeLists.txt
create mode 100644 test/tarantool-debugger-tests/debug-extension-tests.py
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index f48afa25..26b15892 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -175,6 +175,7 @@ add_subdirectory(LuaJIT-tests)
add_subdirectory(PUC-Rio-Lua-5.1-tests)
add_subdirectory(lua-Harness-tests)
add_subdirectory(tarantool-c-tests)
+add_subdirectory(tarantool-debugger-tests)
add_subdirectory(tarantool-tests)
# Each testsuite has its own CMake target, but combining these
@@ -186,6 +187,9 @@ add_subdirectory(tarantool-tests)
# command that runs all generated CMake tests.
add_custom_target(${PROJECT_NAME}-test
COMMAND ${CMAKE_CTEST_COMMAND} ${CTEST_FLAGS}
+ # Omit this target in LuaJIT-test since we don't want to set
+ # up and run debuggers for every build.
+ --label-exclude tarantool-debugger-tests
DEPENDS tarantool-c-tests-deps
tarantool-tests-deps
lua-Harness-tests-deps
@@ -195,5 +199,8 @@ add_custom_target(${PROJECT_NAME}-test
add_custom_target(${PROJECT_NAME}-check-all
DEPENDS ${PROJECT_NAME}-test
+ # Omit this target in LuaJIT-test since we don't want to
+ # set up and run debuggers for every build.
+ tarantool-debugger-tests
${PROJECT_NAME}-lint
)
diff --git a/test/tarantool-debugger-tests/CMakeLists.txt b/test/tarantool-debugger-tests/CMakeLists.txt
new file mode 100644
index 00000000..7fd0debc
--- /dev/null
+++ b/test/tarantool-debugger-tests/CMakeLists.txt
@@ -0,0 +1,93 @@
+set(TEST_SUITE_NAME "tarantool-debugger-tests")
+
+# XXX: The call produces both test and target
+# <tarantool-debugger-tests-deps> as a side effect.
+add_test_suite_target(tarantool-debugger-tests
+ LABELS ${TEST_SUITE_NAME}
+ 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, tarantool-debugger-tests is 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,"
+ " tarantool-debugger-tests is dummy"
+ )
+ return()
+endif()
+
+if(CMAKE_VERSION VERSION_LESS "3.12")
+ # TODO:Can remove this after upgrading to CMake >= 3.12.
+ find_package(PythonInterp)
+ if(NOT PYTHONINTERP_FOUND)
+ message(WARNING "`python` is not found, tarantool-debugger-tests is dummy")
+ return()
+ endif()
+else()
+ find_package(Python COMPONENTS Interpreter)
+ if(NOT PYTHON_FOUND)
+ message(WARNING "`python` is not found, tarantool-debugger-tests is dummy")
+ return()
+ endif()
+ set(PYTHON_EXECUTABLE "${Python_EXECUTABLE}")
+endif()
+
+set(DEBUGGER_TEST_ENV
+ "LUAJIT_TEST_BINARY=${LUAJIT_TEST_BINARY}"
+ # Suppresses __pycache__ generation.
+ "PYTHONDONTWRITEBYTECODE=1"
+ "DEBUGGER_EXTENSION_PATH=${PROJECT_SOURCE_DIR}/src"
+)
+
+set(TEST_SCRIPT_PATH
+ ${CMAKE_CURRENT_SOURCE_DIR}/debug-extension-tests.py
+)
+
+find_program(GDB gdb)
+if(GDB)
+ set(test_title "test/${TEST_SUITE_NAME}/gdb")
+ set(GDB_TEST_ENV ${DEBUGGER_TEST_ENV} "DEBUGGER_COMMAND=${GDB}")
+ add_test(NAME "${test_title}"
+ COMMAND ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ )
+ set_tests_properties("${test_title}" PROPERTIES
+ ENVIRONMENT "${GDB_TEST_ENV}"
+ LABELS ${TEST_SUITE_NAME}
+ DEPENDS tarantool-debugger-tests-deps
+ )
+else()
+ message(WARNING
+ "`gdb' is not found, so tarantool-debugger-tests/gdb is omitted"
+ )
+endif()
+
+find_program(LLDB lldb)
+if(LLDB)
+ set(test_title "test/${TEST_SUITE_NAME}/lldb")
+ set(LLDB_TEST_ENV ${DEBUGGER_TEST_ENV} "DEBUGGER_COMMAND=${LLDB}")
+ add_test(NAME "${test_title}"
+ COMMAND ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ )
+ set_tests_properties("${test_title}" PROPERTIES
+ ENVIRONMENT "${LLDB_TEST_ENV}"
+ LABELS ${TEST_SUITE_NAME}
+ DEPENDS tarantool-debugger-tests-deps
+ )
+else()
+ message(WARNING
+ "`lldb' is not found, so tarantool-debugger-tests/lldb is omitted"
+ )
+endif()
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
new file mode 100644
index 00000000..6094c535
--- /dev/null
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -0,0 +1,286 @@
+# 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_PATH = os.environ['DEBUGGER_EXTENSION_PATH']
+DEBUGGER = os.environ['DEBUGGER_COMMAND']
+LLDB = 'lldb' in DEBUGGER
+EXTENSION = EXTENSION_PATH + ('/luajit_lldb.py' if LLDB else '/luajit-gdb.py')
+TIMEOUT = 10
+
+# Don't run any initialization scripts.
+RUN_CMD_FILE = []
+
+if LLDB:
+ RUN_CMD_FILE = [
+ '--batch',
+ '--no-lldbinit',
+ '--no-use-colors',
+ '--source-quietly',
+ '--source'
+ ]
+else:
+ # GDB.
+ RUN_CMD_FILE = ['--batch', '--nx', '--quiet', '--command']
+
+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)
+
+
+RX_ADDR = r'0x[a-f0-9]+'
+RX_HASH = RX_ADDR # The same pattern for hexademic values.
+RX_FRAME = r'\[(S|\s)(B|\s)(T|\s)(M|\s)\]'
+
+
+def persist(data):
+ tmp = tempfile.NamedTemporaryFile(mode='w')
+ tmp.write(data)
+ tmp.flush()
+ return tmp
+
+
+def execute_process(cmd, timeout=TIMEOUT):
+ if LEGACY:
+ # XXX: The Python 2.7 version of `subprocess.Popen`
+ # doesn't have a timeout option, so the required
+ # functionality was implemented via `threading.Timer`.
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ # This prevents sending of SIGSTTOU to the test when
+ # running by `make'. Stdin is unused anyway.
+ stdin=subprocess.DEVNULL
+ )
+ 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,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ # This prevents sending of SIGSTTOU to the test when
+ # running by `make'. Stdin is unused anyway.
+ stdin=subprocess.DEVNULL,
+ universal_newlines=True,
+ timeout=TIMEOUT
+ )
+ return process.stdout
+
+
+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 = 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 = (
+ r'lj-arch command initialized\n'
+ r'lj-tv command initialized\n'
+ r'lj-str command initialized\n'
+ r'lj-tab command initialized\n'
+ r'lj-stack command initialized\n'
+ r'lj-state command initialized\n'
+ r'lj-gc command initialized\n'
+ r'.*is successfully loaded'
+ )
+
+
+class TestLJArch(TestCaseBase):
+ extension_cmds = 'lj-arch'
+ location = 'lj_cf_print'
+ lua_script = 'print(1)'
+ pattern = (
+ r'LJ_64: (True|False), '
+ r'LJ_GC64: (True|False), '
+ r'LJ_DUALNUM: (True|False)'
+ )
+
+
+class TestLJState(TestCaseBase):
+ extension_cmds = 'lj-state'
+ location = 'lj_cf_print'
+ lua_script = 'print(1)'
+ pattern = (
+ r'VM state: [A-Z]+\n'
+ r'GC state: [A-Z]+\n'
+ r'JIT state: [A-Z]+\n'
+ )
+
+
+class TestLJGC(TestCaseBase):
+ extension_cmds = 'lj-gc'
+ location = 'lj_cf_print'
+ lua_script = 'print(1)'
+ pattern = (
+ r'GC stats: [A-Z]+\n'
+ r'\ttotal: \d+\n'
+ r'\tthreshold: \d+\n'
+ r'\tdebt: \d+\n'
+ r'\testimate: \d+\n'
+ r'\tstepmul: \d+\n'
+ r'\tpause: \d+\n'
+ r'\tsweepstr: \d+/\d+\n'
+ r'\troot: \d+ objects\n'
+ r'\tgray: \d+ objects\n'
+ r'\tgrayagain: \d+ objects\n'
+ r'\tweak: \d+ objects\n'
+ r'\tmmudata: \d+ objects\n'
+ )
+
+
+class TestLJStack(TestCaseBase):
+ extension_cmds = 'lj-stack'
+ location = 'lj_cf_print'
+ lua_script = 'print(1)'
+ pattern = (
+ r'-+ Red zone:\s+\d+ slots -+\n'
+ r'(' + RX_ADDR + r'\s+' + RX_FRAME + r' VALUE: nil\n?)*\n'
+ r'-+ Stack:\s+\d+ slots -+\n'
+ r'(' + RX_ADDR + r'(:' + RX_ADDR + r')?\s+' + RX_FRAME + r'.*\n?)+\n'
+ )
+
+
+class TestLJTV(TestCaseBase):
+ location = 'lj_cf_print'
+ 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 = (
+ r'nil\n'
+ r'false\n'
+ r'true\n'
+ r'string \"hello\" @ ' + RX_ADDR + r'\n'
+ r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' + RX_HASH + r'\)\n'
+ r'(number|integer) .*1.*\n'
+ r'number 1.1\d+\n'
+ r'thread @ ' + RX_ADDR + r'\n'
+ r'cdata @ ' + RX_ADDR + r'\n'
+ r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[0-9]+\n'
+ r'fast function #[0-9]+\n'
+ r'C function @ ' + RX_ADDR + r'\n'
+ )
+
+
+class TestLJStr(TestCaseBase):
+ extension_cmds = (
+ # XXX: Get the value to the stack slot for the variable.
+ 'n\n'
+ 'lj-str fname\n'
+ )
+ location = 'lj_cf_dofile'
+ lua_script = 'pcall(dofile("name"))'
+ pattern = r'String: .* \[\d+ bytes\] with hash ' + RX_HASH
+
+
+class TestLJTab(TestCaseBase):
+ extension_cmds = (
+ # XXX: Get the value to the stack slot for the variable.
+ 'n\n'
+ 'lj-tab t\n'
+ )
+ location = 'lj_cf_unpack'
+ lua_script = 'unpack({1; a = 1})'
+ pattern = (
+ r'Array part: 3 slots\n' +
+ RX_ADDR + r': \[0\]: nil\n' +
+ RX_ADDR + r': \[1\]: .+ 1\n' +
+ RX_ADDR + r': \[2\]: nil\n' +
+ r'Hash part: 2 nodes\n' +
+ RX_ADDR + r': { string "a" @ ' + RX_ADDR + r' } => ' +
+ r'{ .+ 1 }; next = 0x0\n' +
+ RX_ADDR + r': { nil } => { nil }; next = 0x0\n'
+ )
+
+
+for test_cls in TestCaseBase.__subclasses__():
+ test_cls.test = lambda self: self.check()
+
+# FIXME: skip for LLDB since most commands are not working anyway.
+if __name__ == '__main__' and not LLDB:
+ unittest.main(verbosity=2)
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH v2 luajit 2/6] lldb: refactor extension
2026-05-19 12:39 [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 1/6] test: introduce tests for debugging extensions Sergey Kaplun via Tarantool-patches
@ 2026-05-19 12:39 ` Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 3/6] dbg: sort initialization of commands Sergey Kaplun via Tarantool-patches
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-05-19 12:39 UTC (permalink / raw)
To: Mikhail Elhimov, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
This patch refactors lldb python extension C structure casts and type
handling. Now it uses a monkey-patched lldb.value class instead of a
handwritten one. Hence, there is no need for hardcoded C type structure
fields. All LuaJIT-related functions use the same semantics as in the
gdb extension. Also, this patch changes all fields' access to item
access instead of attribute access.
This commit fixes all tests except the initialization check since the
order of loaded commands is different from gdb. This will be fixed in
the next commit.
---
src/luajit_lldb.py | 605 +++++++-----------
.../debug-extension-tests.py | 33 +-
2 files changed, 256 insertions(+), 382 deletions(-)
diff --git a/src/luajit_lldb.py b/src/luajit_lldb.py
index 6e66954f..218cc8d1 100644
--- a/src/luajit_lldb.py
+++ b/src/luajit_lldb.py
@@ -5,6 +5,7 @@
import abc
import re
import lldb
+import struct
LJ_64 = None
LJ_GC64 = None
@@ -24,213 +25,132 @@ LJ_TISNUM = None
target = None
-class Ptr:
- 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())
-
- def __add__(self, other):
- assert isinstance(other, int)
- return self.__class__(
- cast(
- self.normal_type.__name__ + ' *',
- cast(
- 'uintptr_t',
- self.value.unsigned + other * self.value.deref.size,
- ),
- ),
- )
+def lldb_tp_isfp(tp):
+ return tp.GetBasicType() in [
+ lldb.eBasicTypeFloat,
+ lldb.eBasicTypeDouble,
+ lldb.eBasicTypeLongDouble
+ ]
- def __sub__(self, other):
- assert isinstance(other, int) or isinstance(other, Ptr)
- if isinstance(other, int):
- return self.__add__(-other)
- else:
- return int((self.value.unsigned - other.value.unsigned)
- / sizeof(self.normal_type.__name__))
- def __eq__(self, other):
- assert isinstance(other, Ptr) or isinstance(other, int) and other >= 0
- if isinstance(other, Ptr):
- return self.value.unsigned == other.value.unsigned
- else:
- return self.value.unsigned == other
-
- def __ne__(self, other):
- return not self == other
-
- def __gt__(self, other):
- assert isinstance(other, Ptr)
- return self.value.unsigned > other.value.unsigned
-
- def __ge__(self, other):
- assert isinstance(other, Ptr)
- return self.value.unsigned >= other.value.unsigned
-
- def __bool__(self):
- return self.value.unsigned != 0
-
- def __int__(self):
- return self.value.unsigned
-
- def __str__(self):
- return self.value.value
-
- def __getattr__(self, name):
- if name != '__deref':
- return getattr(self.__deref, name)
- 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):
- def __init__(self, value):
- self.value = value
-
- def __getitem__(self, name):
- return self.value.GetChildMemberWithName(name)
-
- @property
- def addr(self):
- return self.value.address_of
-
-
-c_structs = {
- 'MRef': [
- (property(lambda self: self['ptr64'].unsigned if LJ_GC64
- else self['ptr32'].unsigned), 'ptr')
- ],
- 'GCRef': [
- (property(lambda self: self['gcptr64'].unsigned if LJ_GC64
- else self['gcptr32'].unsigned), 'gcptr')
- ],
- 'TValue': [
- ('GCRef', 'gcr'),
- ('uint', 'it'),
- ('uint', 'i'),
- ('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')
- ],
- 'GCState': [
- ('GCRef', 'root'),
- ('GCRef', 'gray'),
- ('GCRef', 'grayagain'),
- ('GCRef', 'weak'),
- ('GCRef', 'mmudata'),
- ('uint', 'state'),
- ('uint', 'total'),
- ('uint', 'threshold'),
- ('uint', 'debt'),
- ('uint', 'estimate'),
- ('uint', 'stepmul'),
- ('uint', 'pause'),
- ('uint', 'sweepstr')
- ],
- 'lua_State': [
- ('MRef', 'glref'),
- ('MRef', 'stack'),
- ('MRef', 'maxstack'),
- ('TValuePtr', 'top'),
- ('TValuePtr', 'base')
- ],
- 'global_State': [
- ('GCState', 'gc'),
- ('uint', 'vmstate'),
- ('uint', 'strmask')
- ],
- 'jit_State': [
- ('uint', 'state')
- ],
- 'GChead': [
- ('GCRef', 'nextgc')
- ],
- 'GCobj': [
- ('GChead', 'gch')
- ],
- 'GCstr': [
- ('uint', 'hash'),
- ('uint', 'len')
- ],
- 'FrameLink': [
- ('MRef', 'pcr'),
- ('int', 'ftsz')
- ],
- 'FR': [
- ('FrameLink', 'tp')
- ],
- 'GCfuncC': [
- ('MRef', 'pc'),
- ('uint', 'ffid'),
- ('uint', 'nupvalues'),
- ('uint', 'f')
- ],
- 'GCtab': [
- ('MRef', 'array'),
- ('MRef', 'node'),
- ('GCRef', 'metatable'),
- ('uint', 'asize'),
- ('uint', 'hmask')
- ],
- 'GCproto': [
- ('GCRef', 'chunkname'),
- ('int', 'firstline')
- ],
- 'GCtrace': [
- ('uint', 'traceno')
- ],
- 'Node': [
- ('TValue', 'key'),
- ('TValue', 'val'),
- ('MRef', 'next')
- ],
- 'BCIns': []
-}
+def lldb_value_from_raw(raw_value, size, tp):
+ isfp = lldb_tp_isfp(tp)
+ pack_flag = '<d' if isfp else '<Q'
+ raw_data = struct.pack(pack_flag, raw_value)
+ sbdata = lldb.SBData()
+ sbdata.SetData(
+ lldb.SBError(),
+ raw_data,
+ lldb.eByteOrderLittle,
+ size
+ )
+ sbval_res = target.CreateValueFromData(
+ # XXX: The name is required. Let's make it meaningful.
+ '({tp}){val}'.format(
+ tp=tp.name,
+ val=raw_value if isfp else hex(raw_value)
+ ),
+ sbdata,
+ tp
+ )
+ return lldb.value(sbval_res)
-for cls in c_structs.keys():
- globals()[cls] = type(cls, (Struct, ), {'metainfo': c_structs[cls]})
+def lldb__add__(self, other):
+ other = int(other)
+ sbvalue = self.sbvalue
+ if sbvalue.TypeIsPointerType():
+ tp = sbvalue.GetType()
+ sz = sbvalue.deref.size
+ addr = sbvalue.GetValueAsUnsigned() + other * sz
+ return lldb_value_from_raw(addr, sbvalue.GetByteSize(), tp)
+ else:
+ return int(self) + other
-for cls in Struct.__subclasses__():
- ptr_name = cls.__name__ + 'Ptr'
+def lldb__bool__(self):
+ return int(self) != 0
- globals()[ptr_name] = type(ptr_name, (Ptr,), {
- '__init__':
- lambda self, value: super(type(self), self).__init__(value, cls)
- })
+
+def lldb__ge__(self, other):
+ return int(self) >= int(other)
+
+
+def lldb__getitem__(self, key):
+ if type(key) is lldb.value:
+ key = int(key)
+ if type(key) is int:
+ # Allow array access.
+ return lldb.value(self.sbvalue.GetValueForExpressionPath('[%i]' % key))
+ elif type(key) is str:
+ return lldb.value(self.sbvalue.GetChildMemberWithName(key))
+ raise Exception(TypeError('No item of type %s' % str(type(key))))
+
+
+def lldb__gt__(self, other):
+ return int(self) > int(other)
+
+
+def lldb__le__(self, other):
+ return int(self) <= int(other)
+
+
+def lldb__lt__(self, other):
+ return int(self) < int(other)
+
+
+def lldb__str__(self):
+ # Instead of default GetSummary.
+ if not self.sbvalue.TypeIsPointerType():
+ tp = self.sbvalue.GetType()
+ is_float = lldb_tp_isfp(tp)
+ if is_float:
+ return self.sbvalue.GetValue()
+ else:
+ return str(int(self))
+
+ s = self.sbvalue.GetValue()
+ if s[:2] == '0x':
+ # Strip useless leading zeros.
+ res = s[2:].lstrip('0')
+ return '0x' + (res if res else '0')
+ return s
+
+
+def lldb__sub__(self, other):
+ if type(other) is not lldb.value or \
+ type(other) is lldb.value and not other.sbvalue.TypeIsPointerType():
+ other = int(other)
+ if type(other) is int:
+ return lldb__add__(self, -other)
+ elif self.sbvalue.TypeIsPointerType():
+ ssbval = self.sbvalue
+ osbval = other.sbvalue
+ self_tp = ssbval.GetType()
+ other_tp = osbval.GetType()
+ # Subtract pointers of the same size only.
+ elsz = self_tp.GetDereferencedType().size
+ if other_tp.GetDereferencedType().size != elsz:
+ raise Exception('Attempt to substruct {otp} from {stp}'.format(
+ stp=self_tp.name,
+ otp=other_tp.name
+ ))
+ diff = ssbval.GetValueAsUnsigned() - osbval.GetValueAsUnsigned()
+ return int(diff / elsz)
+ else:
+ return int(self) - int(other)
+
+
+# Monkey-patch the lldb.value class.
+lldb.value.__add__ = lldb__add__
+lldb.value.__bool__ = lldb__bool__
+lldb.value.__ge__ = lldb__ge__
+lldb.value.__getitem__ = lldb__getitem__
+lldb.value.__gt__ = lldb__gt__
+lldb.value.__le__ = lldb__le__
+lldb.value.__lt__ = lldb__lt__
+lldb.value.__str__ = lldb__str__
+lldb.value.__sub__ = lldb__sub__
class Command(object):
@@ -280,52 +200,38 @@ class Command(object):
"""
-def cast(typename, value):
- pointer_type = False
- name = None
- if isinstance(value, Struct) or isinstance(value, Ptr):
- # Get underlying value, if passed object is a wrapper.
- value = value.value
+gtype_cache = {}
- # Obtain base type name, decide whether it's a pointer.
- if isinstance(typename, type):
- name = typename.__name__
- if name.endswith('Ptr'):
- pointer_type = True
- name = name[:-3]
- else:
- name = typename
- if name[-1] == '*':
- name = name[:-1].strip()
- pointer_type = True
-
- # Get the lldb type representation.
- t = target.FindFirstType(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)
- if isinstance(typename, type):
- # Wrap lldb object, if possible
- return typename(casted)
- else:
- return casted
+def gtype(typestr):
+ if typestr in gtype_cache:
+ return gtype_cache[typestr]
+
+ m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr)
+
+ gtype = target.FindFirstType(typestr) if m is None \
+ else target.FindFirstType(m.group(1)).GetPointerType()
+
+ gtype_cache[typestr] = gtype
+ return gtype
+
+
+def cast(typestr, val):
+ if isinstance(val, lldb.value):
+ val = val.sbvalue
+ elif type(val) is int:
+ tp = gtype(typestr)
+ return lldb_value_from_raw(val, tp.GetByteSize(), tp)
+ elif not isinstance(val, lldb.SBValue):
+ raise Exception('unexpected cast from type: {t}'.format(t=type(val)))
+
+ # XXX: Simply SBValue.Cast() works incorrectly since it may
+ # take the 8 bytes of memory instead of 4, before the cast.
+ # Construct the value on the fly.
+ tp = gtype(typestr)
+ is_fp = lldb_tp_isfp(tp)
+ rawval = float(val.GetValue()) if is_fp else val.GetValueAsUnsigned()
+ return lldb_value_from_raw(rawval, val.GetByteSize(), tp)
def lookup_global(name):
@@ -336,28 +242,20 @@ 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)
+ type_obj = gtype(typename)
member = type_member(type_obj, membername)
assert member is not None
return member.GetOffsetInBytes()
def sizeof(typename):
- type_obj = find_type(typename)
+ type_obj = gtype(typename)
return type_obj.GetByteSize()
-def vtou64(value):
- return value.unsigned & 0xFFFFFFFFFFFFFFFF
-
-
-def vtoi(value):
- return value.signed
+def tou64(val):
+ return cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF
def dbg_eval(expr):
@@ -371,17 +269,17 @@ def dbg_eval(expr):
def gcval(obj):
- return cast(GCobjPtr, cast('uintptr_t', obj.gcptr & LJ_GCVMASK) if LJ_GC64
- else cast('uintptr_t', obj.gcptr))
+ return cast('GCobj *', obj['gcptr64'] & LJ_GCVMASK if LJ_GC64
+ else cast('uintptr_t', obj['gcptr32']))
def gcref(obj):
- return cast(GCobjPtr, obj.gcptr if LJ_GC64
- else cast('uintptr_t', obj.gcptr))
+ return cast('GCobj *', obj['gcptr64'] if LJ_GC64
+ else cast('uintptr_t', obj['gcptr32']))
def gcnext(obj):
- return gcref(obj).gch.nextgc
+ return gcref(obj)['gch']['nextgc']
def gclistlen(root, end=0x0):
@@ -412,15 +310,15 @@ gclen = {
def dump_gc(g):
- gc = g.gc
+ gc = g['gc']
stats = ['{key}: {value}'.format(key=f, value=getattr(gc, f)) for f in (
'total', 'threshold', 'debt', 'estimate', 'stepmul', 'pause'
)]
stats += ['sweepstr: {sweepstr}/{strmask}'.format(
- sweepstr=gc.sweepstr,
+ sweepstr=gc['sweepstr'],
# String hash mask (size of hash table - 1).
- strmask=g.strmask + 1,
+ strmask=g['strmask'] + 1,
)]
stats += ['{key}: {number} objects'.format(
@@ -431,20 +329,17 @@ def dump_gc(g):
def mref(typename, obj):
- return cast(typename, obj.ptr)
+ return cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32'])
def J(g):
g_offset = offsetof('GG_State', 'g')
J_offset = offsetof('GG_State', 'J')
- return cast(
- jit_StatePtr,
- vtou64(cast('char *', g)) - g_offset + J_offset,
- )
+ return cast('jit_State *', (cast('char *', g) - g_offset + J_offset))
def G(L):
- return mref(global_StatePtr, L.glref)
+ return mref('global_State *', L['glref'])
def L(L=None):
@@ -459,7 +354,7 @@ def L(L=None):
# TODO: Add more
))):
if lstate:
- return lua_State(lstate)
+ return cast('lua_State *', lstate)
def tou32(val):
@@ -481,7 +376,7 @@ def vm_state(g):
i2notu32(6): 'RECORD',
i2notu32(7): 'OPT',
i2notu32(8): 'ASM',
- }.get(int(tou32(g.vmstate)), 'TRACE')
+ }.get(int(tou32(g['vmstate'])), 'TRACE')
def gc_state(g):
@@ -493,7 +388,7 @@ def gc_state(g):
4: 'SWEEP',
5: 'FINALIZE',
6: 'LAST',
- }.get(g.gc.state, 'INVALID')
+ }.get(int(g['gc']['state']), 'INVALID')
def jit_state(g):
@@ -505,31 +400,29 @@ def jit_state(g):
0x13: 'END',
0x14: 'ASM',
0x15: 'ERR',
- }.get(J(g).state, 'INVALID')
+ }.get(int(J(g).state), 'INVALID')
def strx64(val):
return re.sub('L?$', '',
- hex(int(val) & 0xFFFFFFFFFFFFFFFF))
+ hex(int(cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF)))
def funcproto(func):
assert func.ffid == 0
- proto_size = sizeof('GCproto')
- value = cast('uintptr_t', vtou64(mref('char *', func.pc)) - proto_size)
- return cast(GCprotoPtr, value)
+ return cast('GCproto *', mref('char *', func.pc) - sizeof('GCproto'))
def strdata(obj):
try:
- ptr = cast('char *', obj + 1)
- return ptr.summary
+ ptr = cast('char *', cast('GCstr *', obj) + 1)
+ return ptr.sbvalue.summary
except UnicodeEncodeError:
return "<luajit-lldb: error occurred while rendering non-ascii slot>"
def itype(o):
- return tou32(o.it64 >> 47) if LJ_GC64 else o.it
+ return tou32(o['it64'] >> 47) if LJ_GC64 else o['it']
def tvisint(o):
@@ -538,7 +431,7 @@ def tvisint(o):
def tvislightud(o):
if LJ_64 and not LJ_GC64:
- return (vtoi(cast('int32_t', itype(o))) >> 15) == -2
+ return (int(cast('int32_t', itype(o))) >> 15) == -2
else:
return itype(o) == LJ_T['LIGHTUD']
@@ -560,80 +453,80 @@ def dump_lj_ttrue(tv):
def dump_lj_tlightud(tv):
- return 'light userdata @ {}'.format(strx64(gcval(tv.gcr)))
+ return 'light userdata @ {}'.format(strx64(gcval(tv['gcr'])))
def dump_lj_tstr(tv):
return 'string {body} @ {address}'.format(
- body=strdata(cast(GCstrPtr, gcval(tv.gcr))),
- address=strx64(gcval(tv.gcr))
+ body=strdata(cast('GCstr *', gcval(tv['gcr']))),
+ address=strx64(gcval(tv['gcr']))
)
def dump_lj_tupval(tv):
- return 'upvalue @ {}'.format(strx64(gcval(tv.gcr)))
+ return 'upvalue @ {}'.format(strx64(gcval(tv['gcr'])))
def dump_lj_tthread(tv):
- return 'thread @ {}'.format(strx64(gcval(tv.gcr)))
+ return 'thread @ {}'.format(strx64(gcval(tv['gcr'])))
def dump_lj_tproto(tv):
- return 'proto @ {}'.format(strx64(gcval(tv.gcr)))
+ return 'proto @ {}'.format(strx64(gcval(tv['gcr'])))
def dump_lj_tfunc(tv):
- func = cast(GCfuncCPtr, gcval(tv.gcr))
- ffid = func.ffid
+ func = cast('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=func.nupvalues,
- chunk=strdata(cast(GCstrPtr, gcval(pt.chunkname))),
- line=pt.firstline
+ nups=func['nupvalues'],
+ chunk=strdata(cast('GCstr *', gcval(pt['chunkname']))),
+ line=pt['firstline']
)
elif ffid == 1:
- return 'C function @ {}'.format(strx64(func.f))
+ return 'C function @ {}'.format(strx64(func['f']))
else:
return 'fast function #{}'.format(ffid)
def dump_lj_ttrace(tv):
- trace = cast(GCtracePtr, gcval(tv.gcr))
+ trace = cast('GCtrace *', gcval(tv['gcr']))
return 'trace {traceno} @ {addr}'.format(
- traceno=strx64(trace.traceno),
+ traceno=strx64(trace['traceno']),
addr=strx64(trace)
)
def dump_lj_tcdata(tv):
- return 'cdata @ {}'.format(strx64(gcval(tv.gcr)))
+ return 'cdata @ {}'.format(strx64(gcval(tv['gcr'])))
def dump_lj_ttab(tv):
- table = cast(GCtabPtr, gcval(tv.gcr))
+ table = cast('GCtab *', gcval(tv['gcr']))
return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format(
gcr=strx64(table),
- asize=table.asize,
- hmask=strx64(table.hmask),
+ asize=table['asize'],
+ hmask=strx64(table['hmask']),
)
def dump_lj_tudata(tv):
- return 'userdata @ {}'.format(strx64(gcval(tv.gcr)))
+ return 'userdata @ {}'.format(strx64(gcval(tv['gcr'])))
def dump_lj_tnumx(tv):
if tvisint(tv):
- return 'integer {}'.format(cast('int32_t', tv.i))
+ return 'integer {}'.format(cast('int32_t', tv['i']))
else:
- return 'number {}'.format(tv.n)
+ return 'number {}'.format(cast('double', tv['n']))
def dump_lj_invalid(tv):
- return 'not valid type @ {}'.format(strx64(gcval(tv.gcr)))
+ return 'not valid type @ {}'.format(strx64(gcval(tv['gcr'])))
dumpers = {
@@ -686,8 +579,8 @@ def typenames(value):
}.get(int(value), 'LJ_TINVALID')
-def dump_tvalue(tvptr):
- return dumpers.get(typenames(itypemap(tvptr)), dump_lj_invalid)(tvptr)
+def dump_tvalue(tvalue):
+ return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue)
FRAME_TYPE = 0x3
@@ -720,23 +613,17 @@ def bc_a(ins):
def frame_ftsz(framelink):
- return vtou64(cast('ptrdiff_t', framelink.ftsz if LJ_FR2
- else framelink.fr.tp.ftsz))
+ return cast('ptrdiff_t', framelink['ftsz'] if LJ_FR2
+ else framelink['fr']['tp']['ftsz'])
def frame_pc(framelink):
- return cast(BCInsPtr, frame_ftsz(framelink)) if LJ_FR2 \
- else mref(BCInsPtr, framelink.fr.tp.pcr)
+ return cast('BCIns *', frame_ftsz(framelink)) if LJ_FR2 \
+ else mref('BCIns *', framelink['fr']['tp']['pcr'])
def frame_prevl(framelink):
- # We are evaluating the `frame_pc(framelink)[-1])` with lldb's
- # 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]'))
- return framelink - (1 + LJ_FR2 + bc_a(bcins))
+ return framelink - (1 + LJ_FR2 + bc_a(frame_pc(framelink)[-1]))
def frame_ispcall(framelink):
@@ -770,14 +657,14 @@ def frame_prev(framelink):
def frame_sentinel(L):
- return mref(TValuePtr, L.stack) + LJ_FR2
+ return mref('TValue *', L['stack']) + LJ_FR2
# 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
+ frametop = L['top']
+ framelink = L['base'] - 1
framelink_sentinel = frame_sentinel(L)
while True:
yield framelink, frametop
@@ -788,14 +675,8 @@ def frames(L):
def dump_framelink_slot_address(fr):
- return '{start:{padding}}:{end:{padding}}'.format(
- start=hex(int(fr - 1)),
- end=hex(int(fr)),
- padding=len(PADDING),
- ) if LJ_FR2 else '{addr:{padding}}'.format(
- addr=hex(int(fr)),
- padding=len(PADDING),
- )
+ return '{}:{}'.format(fr - 1, fr) if LJ_FR2 \
+ else '{}'.format(fr) + PADDING
def dump_framelink(L, fr):
@@ -809,30 +690,30 @@ def dump_framelink(L, fr):
frname=frametypes(int(frame_type(fr))),
p='P' if frame_typep(fr) & FRAME_P else ''
),
- d=fr - frame_prev(fr),
+ 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
+ base = base or L['base']
+ top = top or L['top']
- return '{addr:{padding}} [ {B}{T}{M}] VALUE: {value}'.format(
+ return '{addr}{padding} [ {B}{T}{M}] VALUE: {value}'.format(
addr=strx64(slot),
- padding=2 * len(PADDING) + 1,
+ padding=PADDING,
B='B' if slot == base else ' ',
T='T' if slot == top else ' ',
- M='M' if slot == mref(TValuePtr, L.maxstack) 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(TValuePtr, L.stack)
- maxstack = mref(TValuePtr, L.maxstack)
+ base = base or L['base']
+ top = top or L['top']
+ stack = mref('TValue *', L['stack'])
+ maxstack = mref('TValue *', L['maxstack'])
red = 5 + 3 * LJ_FR2
dump = [
@@ -848,19 +729,13 @@ def dump_stack(L, base=None, top=None):
dump.extend([
'{padding} Stack: {nstackslots: >5} slots {padding}'.format(
padding='-' * len(PADDING),
- nstackslots=int((maxstack - stack) >> 3),
+ nstackslots=int((tou64(maxstack) - tou64(stack)) >> 3),
),
dump_stack_slot(L, maxstack, base, top),
'{start}:{end} [ ] {nfreeslots} slots: Free stack slots'.format(
- start='{address:{padding}}'.format(
- address=strx64(top + 1),
- padding=len(PADDING),
- ),
- end='{address:{padding}}'.format(
- address=strx64(maxstack - 1),
- padding=len(PADDING),
- ),
- nfreeslots=int((maxstack - top - 8) >> 3),
+ start=strx64(top + 1),
+ end=strx64(maxstack - 1),
+ nfreeslots=int((tou64(maxstack) - tou64(top) - 8) >> 3),
),
])
@@ -905,7 +780,7 @@ Whether the type of the given address differs from the listed above, then
error message occurs.
'''
def execute(self, debugger, args, result):
- tvptr = TValuePtr(cast('TValue *', self.parse(args)))
+ tvptr = cast('TValue *', self.parse(args))
print('{}'.format(dump_tvalue(tvptr)))
@@ -984,11 +859,11 @@ the payload, size in bytes and hash.
is replaced with the corresponding error when decoding fails.
'''
def execute(self, debugger, args, result):
- string_ptr = GCstrPtr(cast('GCstr *', self.parse(args)))
+ string = cast('GCstr *', self.parse(args))
print("String: {body} [{len} bytes] with hash {hash}".format(
- body=strdata(string_ptr),
- hash=strx64(string_ptr.hash),
- len=string_ptr.len,
+ body=strdata(string),
+ hash=strx64(string['hash']),
+ len=string['len'],
))
@@ -1004,13 +879,13 @@ The command receives a GCtab address and dumps the table contents:
<hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr>
'''
def execute(self, debugger, args, result):
- t = GCtabPtr(cast('GCtab *', self.parse(args)))
- array = mref(TValuePtr, t.array)
- nodes = mref(NodePtr, t.node)
- mt = gcval(t.metatable)
+ t = cast('GCtab *', self.parse(args))
+ array = mref('TValue *', t['array'])
+ nodes = mref('Node *', t['node'])
+ mt = gcval(t['metatable'])
capacity = {
- 'apart': int(t.asize),
- 'hpart': int(t.hmask + 1) if t.hmask > 0 else 0
+ 'apart': int(t['asize']),
+ 'hpart': int(t['hmask'] + 1) if t['hmask'] > 0 else 0
}
if mt:
@@ -1031,9 +906,9 @@ The command receives a GCtab address and dumps the table contents:
node = nodes + i
print('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}'.format(
ptr=strx64(node),
- key=dump_tvalue(TValuePtr(node.key.addr)),
- val=dump_tvalue(TValuePtr(node.val.addr)),
- n=strx64(mref(NodePtr, node.next))
+ key=dump_tvalue(node['key']),
+ val=dump_tvalue(node['val']),
+ n=strx64(mref('Node *', node['next']))
))
@@ -1070,9 +945,7 @@ coroutine guest stack:
If L is omitted the main coroutine is used.
'''
def execute(self, debugger, args, result):
- lstate = self.parse(args)
- lstate_ptr = cast('lua_State *', lstate) if coro is not None else None
- print('{}'.format(dump_stack(L(lstate_ptr))))
+ print('{}'.format(dump_stack(L(self.parse(args)))))
def register_commands(debugger, commands):
@@ -1106,7 +979,7 @@ def configure(debugger):
'no debugging symbols found for libluajit')
return
- PADDING = ' ' * len(strx64((TValuePtr(L().addr))))
+ 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']
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index 6094c535..11c2492b 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -125,20 +125,22 @@ class TestCaseBase(unittest.TestCase):
self.assertRegex(self.output, self.pattern.strip())
-class TestLoad(TestCaseBase):
- extension_cmds = ''
- location = 'lj_cf_print'
- lua_script = 'print(1)'
- pattern = (
- r'lj-arch command initialized\n'
- r'lj-tv command initialized\n'
- r'lj-str command initialized\n'
- r'lj-tab command initialized\n'
- r'lj-stack command initialized\n'
- r'lj-state command initialized\n'
- r'lj-gc command initialized\n'
- r'.*is successfully loaded'
- )
+# FIXME: Skip for LLDB since it has different order.
+if not LLDB:
+ class TestLoad(TestCaseBase):
+ extension_cmds = ''
+ location = 'lj_cf_print'
+ lua_script = 'print(1)'
+ pattern = (
+ r'lj-arch command initialized\n'
+ r'lj-tv command initialized\n'
+ r'lj-str command initialized\n'
+ r'lj-tab command initialized\n'
+ r'lj-stack command initialized\n'
+ r'lj-state command initialized\n'
+ r'lj-gc command initialized\n'
+ r'.*is successfully loaded'
+ )
class TestLJArch(TestCaseBase):
@@ -281,6 +283,5 @@ class TestLJTab(TestCaseBase):
for test_cls in TestCaseBase.__subclasses__():
test_cls.test = lambda self: self.check()
-# FIXME: skip for LLDB since most commands are not working anyway.
-if __name__ == '__main__' and not LLDB:
+if __name__ == '__main__':
unittest.main(verbosity=2)
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH v2 luajit 3/6] dbg: sort initialization of commands
2026-05-19 12:39 [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 1/6] test: introduce tests for debugging extensions Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 2/6] lldb: refactor extension Sergey Kaplun via Tarantool-patches
@ 2026-05-19 12:39 ` Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 4/6] lldb: support full-range 64-bit lightuserdata Sergey Kaplun via Tarantool-patches
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-05-19 12:39 UTC (permalink / raw)
To: Mikhail Elhimov, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
This patch sorts the commands for extensions' initialization
alphabetically. Also, it fixes the typo in the lldb extension.
This allows enabling the loading test for LLDB as well.
---
src/luajit-gdb.py | 8 ++---
src/luajit_lldb.py | 8 ++---
.../debug-extension-tests.py | 30 +++++++++----------
3 files changed, 22 insertions(+), 24 deletions(-)
diff --git a/src/luajit-gdb.py b/src/luajit-gdb.py
index dab07b35..be67bf18 100644
--- a/src/luajit-gdb.py
+++ b/src/luajit-gdb.py
@@ -872,12 +872,12 @@ def init(commands):
def load(event=None):
init({
'lj-arch': LJDumpArch,
- 'lj-tv': LJDumpTValue,
- 'lj-str': LJDumpString,
- 'lj-tab': LJDumpTable,
+ 'lj-gc': LJGC,
'lj-stack': LJDumpStack,
'lj-state': LJState,
- 'lj-gc': LJGC,
+ 'lj-str': LJDumpString,
+ 'lj-tab': LJDumpTable,
+ 'lj-tv': LJDumpTValue,
})
diff --git a/src/luajit_lldb.py b/src/luajit_lldb.py
index 218cc8d1..7668a94c 100644
--- a/src/luajit_lldb.py
+++ b/src/luajit_lldb.py
@@ -958,7 +958,7 @@ def register_commands(debugger, commands):
cmd=cls.command,
)
)
- print('{cmd} command intialized'.format(cmd=cls.command))
+ print('{cmd} command initialized'.format(cmd=cls.command))
def configure(debugger):
@@ -986,12 +986,12 @@ def configure(debugger):
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-stack': LJDumpStack,
+ 'lj-state': LJState,
'lj-str': LJDumpString,
'lj-tab': LJDumpTable,
- 'lj-stack': LJDumpStack,
+ 'lj-tv': LJDumpTValue,
})
print('luajit_lldb.py is successfully loaded')
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index 11c2492b..f3ce3ced 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -125,22 +125,20 @@ class TestCaseBase(unittest.TestCase):
self.assertRegex(self.output, self.pattern.strip())
-# FIXME: Skip for LLDB since it has different order.
-if not LLDB:
- class TestLoad(TestCaseBase):
- extension_cmds = ''
- location = 'lj_cf_print'
- lua_script = 'print(1)'
- pattern = (
- r'lj-arch command initialized\n'
- r'lj-tv command initialized\n'
- r'lj-str command initialized\n'
- r'lj-tab command initialized\n'
- r'lj-stack command initialized\n'
- r'lj-state command initialized\n'
- r'lj-gc command initialized\n'
- r'.*is successfully loaded'
- )
+class TestLoad(TestCaseBase):
+ extension_cmds = ''
+ location = 'lj_cf_print'
+ lua_script = 'print(1)'
+ pattern = (
+ r'lj-arch command initialized\n'
+ r'lj-gc 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-tv command initialized\n'
+ r'.*is successfully loaded'
+ )
class TestLJArch(TestCaseBase):
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH v2 luajit 4/6] lldb: support full-range 64-bit lightuserdata
2026-05-19 12:39 [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers Sergey Kaplun via Tarantool-patches
` (2 preceding siblings ...)
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 3/6] dbg: sort initialization of commands Sergey Kaplun via Tarantool-patches
@ 2026-05-19 12:39 ` Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 5/6] dbg: generalize extension Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 6/6] ci: introduce workflow to test debugger extension Sergey Kaplun via Tarantool-patches
5 siblings, 0 replies; 7+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-05-19 12:39 UTC (permalink / raw)
To: Mikhail Elhimov, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
This commit adds the missed support for the full-range 64-bit light
userdata, which was lost during lldb extension implementation. The
corresponding entries for the test of lj-tv are sorted according to LJT*
types.
---
src/luajit_lldb.py | 20 +++++++++++++-
.../debug-extension-tests.py | 27 ++++++++++++-------
2 files changed, 36 insertions(+), 11 deletions(-)
diff --git a/src/luajit_lldb.py b/src/luajit_lldb.py
index 7668a94c..e3fe82fc 100644
--- a/src/luajit_lldb.py
+++ b/src/luajit_lldb.py
@@ -18,6 +18,12 @@ IRT_P64 = 9
LJ_GCVMASK = ((1 << 47) - 1)
LJ_TISNUM = 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
+
# Debugger specific {{{
@@ -440,6 +446,18 @@ def tvisnumber(o):
return itype(o) <= LJ_TISNUM
+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'])
+
+
def dump_lj_tnil(tv):
return 'nil'
@@ -453,7 +471,7 @@ def dump_lj_ttrue(tv):
def dump_lj_tlightud(tv):
- return 'light userdata @ {}'.format(strx64(gcval(tv['gcr'])))
+ return 'light userdata @ {}'.format(strx64(lightudV(tv)))
def dump_lj_tstr(tv):
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index f3ce3ced..2b67e151 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -211,23 +211,28 @@ class TestLJTV(TestCaseBase):
'lj-tv L->base + 9\n'
'lj-tv L->base + 10\n'
'lj-tv L->base + 11\n'
+ 'lj-tv L->base + 12\n'
+ 'lj-tv L->base + 13\n'
)
+ # Sorted in LJT order.
lua_script = (
'local ffi = require("ffi")\n'
'print(\n'
' nil,\n'
' false,\n'
' true,\n'
+ ' debug.upvalueid(print, 1), \n' # lightuserdata
' "hello",\n'
- ' {1},\n'
- ' 1,\n'
- ' 1.1,\n'
' coroutine.create(function() end),\n'
- ' ffi.new("int*"),\n'
' function() end,\n'
+ ' require,\n'
' print,\n'
- ' require\n'
+ ' ffi.new("int*"),\n'
+ ' {1},\n'
+ ' newproxy(),\n'
+ ' 1,\n'
+ ' 1.1\n'
')\n'
)
@@ -235,15 +240,17 @@ class TestLJTV(TestCaseBase):
r'nil\n'
r'false\n'
r'true\n'
+ r'light userdata @ ' + RX_ADDR + r'\n'
r'string \"hello\" @ ' + RX_ADDR + r'\n'
- r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' + RX_HASH + r'\)\n'
- r'(number|integer) .*1.*\n'
- r'number 1.1\d+\n'
r'thread @ ' + RX_ADDR + r'\n'
- r'cdata @ ' + RX_ADDR + r'\n'
r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[0-9]+\n'
- r'fast function #[0-9]+\n'
r'C function @ ' + RX_ADDR + r'\n'
+ r'fast function #[0-9]+\n'
+ r'cdata @ ' + RX_ADDR + r'\n'
+ r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' + RX_HASH + r'\)\n'
+ r'userdata @ ' + RX_ADDR + r'\n'
+ r'(number|integer) .*1.*\n'
+ r'number 1.1\d+\n'
)
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH v2 luajit 5/6] dbg: generalize extension
2026-05-19 12:39 [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers Sergey Kaplun via Tarantool-patches
` (3 preceding siblings ...)
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 4/6] lldb: support full-range 64-bit lightuserdata Sergey Kaplun via Tarantool-patches
@ 2026-05-19 12:39 ` Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 6/6] ci: introduce workflow to test debugger extension Sergey Kaplun via Tarantool-patches
5 siblings, 0 replies; 7+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-05-19 12:39 UTC (permalink / raw)
To: Mikhail Elhimov, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
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 and in its child
debugger classes with the specific implementations of required methods.
The extension auto-detects the debugger it was loaded into and selects
the correct low-level logic implementation.
The F821 ignore is removed from <.flake8rc> since there is no more need
for it.
---
.flake8rc | 5 -
src/luajit-gdb.py | 884 ------------
src/luajit_dbg.py | 1281 +++++++++++++++++
src/luajit_lldb.py | 1015 -------------
.../debug-extension-tests.py | 4 +-
5 files changed, 1283 insertions(+), 1906 deletions(-)
delete mode 100644 src/luajit-gdb.py
create mode 100644 src/luajit_dbg.py
delete mode 100644 src/luajit_lldb.py
diff --git a/.flake8rc b/.flake8rc
index 13e6178f..e69de29b 100644
--- a/.flake8rc
+++ b/.flake8rc
@@ -1,5 +0,0 @@
-[flake8]
-extend-ignore =
- # XXX: Suppress F821, since we have autogenerated names for
- # 'ptr' type complements in luajit_lldb.py.
- F821
diff --git a/src/luajit-gdb.py b/src/luajit-gdb.py
deleted file mode 100644
index be67bf18..00000000
--- a/src/luajit-gdb.py
+++ /dev/null
@@ -1,884 +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):
- if typestr in gtype_cache:
- return gtype_cache[typestr]
-
- m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr)
-
- gtype = gdb.lookup_type(typestr) if m is None \
- else gdb.lookup_type(m.group(1)).pointer()
-
- gtype_cache[typestr] = gtype
- return gtype
-
-
-def cast(typestr, val):
- return gdb.Value(val).cast(gtype(typestr))
-
-
-def lookup(symbol):
- variable, _ = gdb.lookup_symbol(symbol)
- return variable.value() if variable else None
-
-
-def parse_arg(arg):
- if not arg:
- return None
-
- ret = gdb.parse_and_eval(arg)
-
- if not ret:
- raise gdb.GdbError('table argument empty')
-
- return ret
-
-
-def tou64(val):
- return cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF
-
-
-def tou32(val):
- return cast('uint32_t', val) & 0xFFFFFFFF
-
-
-def i2notu32(val):
- return ~int(val) & 0xFFFFFFFF
-
-
-def strx64(val):
- return re.sub('L?$', '',
- hex(int(cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF)))
-
-
-# Types {{{
-
-
-LJ_T = {
- 'NIL': i2notu32(0),
- 'FALSE': i2notu32(1),
- 'TRUE': i2notu32(2),
- 'LIGHTUD': i2notu32(3),
- 'STR': i2notu32(4),
- 'UPVAL': i2notu32(5),
- 'THREAD': i2notu32(6),
- 'PROTO': i2notu32(7),
- 'FUNC': i2notu32(8),
- 'TRACE': i2notu32(9),
- 'CDATA': i2notu32(10),
- 'TAB': i2notu32(11),
- 'UDATA': i2notu32(12),
- 'NUMX': i2notu32(13),
-}
-
-
-def typenames(value):
- return {
- LJ_T[k]: 'LJ_T' + k for k in LJ_T.keys()
- }.get(int(value), 'LJ_TINVALID')
-
-
-# }}}
-
-# Frames {{{
-
-
-FRAME_TYPE = 0x3
-FRAME_P = 0x4
-FRAME_TYPEP = FRAME_TYPE | FRAME_P
-
-FRAME = {
- 'LUA': 0x0,
- 'C': 0x1,
- 'CONT': 0x2,
- 'VARG': 0x3,
- 'LUAP': 0x4,
- 'CP': 0x5,
- 'PCALL': 0x6,
- 'PCALLH': 0x7,
-}
-
-
-def frametypes(ft):
- return {
- FRAME['LUA']: 'L',
- FRAME['C']: 'C',
- FRAME['CONT']: 'M',
- FRAME['VARG']: 'V',
- }.get(ft, '?')
-
-
-def bc_a(ins):
- return (ins >> 8) & 0xff
-
-
-def frame_ftsz(framelink):
- return cast('ptrdiff_t', framelink['ftsz'] if LJ_FR2
- else framelink['fr']['tp']['ftsz'])
-
-
-def frame_pc(framelink):
- return cast('BCIns *', frame_ftsz(framelink)) if LJ_FR2 \
- else mref('BCIns *', framelink['fr']['tp']['pcr'])
-
-
-def frame_prevl(framelink):
- return framelink - (1 + LJ_FR2 + bc_a(frame_pc(framelink)[-1]))
-
-
-def frame_ispcall(framelink):
- return (frame_ftsz(framelink) & FRAME['PCALL']) == FRAME['PCALL']
-
-
-def frame_sized(framelink):
- return (frame_ftsz(framelink) & ~FRAME_TYPEP)
-
-
-def frame_prevd(framelink):
- return cast('TValue *', cast('char *', framelink) - frame_sized(framelink))
-
-
-def frame_type(framelink):
- return frame_ftsz(framelink) & FRAME_TYPE
-
-
-def frame_typep(framelink):
- return frame_ftsz(framelink) & FRAME_TYPEP
-
-
-def frame_islua(framelink):
- return frametypes(int(frame_type(framelink))) == 'L' \
- and int(frame_ftsz(framelink)) > 0
-
-
-def frame_prev(framelink):
- return frame_prevl(framelink) if frame_islua(framelink) \
- else frame_prevd(framelink)
-
-
-def frame_sentinel(L):
- return mref('TValue *', L['stack']) + LJ_FR2
-
-
-# }}}
-
-# Const {{{
-
-
-LJ_64 = None
-LJ_GC64 = None
-LJ_FR2 = None
-LJ_DUALNUM = None
-
-LJ_GCVMASK = ((1 << 47) - 1)
-LJ_TISNUM = None
-PADDING = None
-
-# These constants are meaningful only for 'LJ_64' mode.
-LJ_LIGHTUD_BITS_SEG = 8
-LJ_LIGHTUD_BITS_LO = 47 - LJ_LIGHTUD_BITS_SEG
-LIGHTUD_SEG_MASK = (1 << LJ_LIGHTUD_BITS_SEG) - 1
-LIGHTUD_LO_MASK = (1 << LJ_LIGHTUD_BITS_LO) - 1
-
-
-# }}}
-
-
-def itype(o):
- return cast('uint32_t', o['it64'] >> 47) if LJ_GC64 else o['it']
-
-
-def mref(typename, obj):
- return cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32'])
-
-
-def gcref(obj):
- return cast('GCobj *', obj['gcptr64'] if LJ_GC64
- else cast('uintptr_t', obj['gcptr32']))
-
-
-def gcval(obj):
- return cast('GCobj *', obj['gcptr64'] & LJ_GCVMASK if LJ_GC64
- else cast('uintptr_t', obj['gcptr32']))
-
-
-def gcnext(obj):
- return gcref(obj)['gch']['nextgc']
-
-
-def L(L=None):
- # lookup a symbol for the main coroutine considering the host app
- # XXX Fragile: though the loop initialization looks like a crap but it
- # respects both Python 2 and Python 3.
- for lstate in [L] + list(map(lambda main: lookup(main), (
- # LuaJIT main coro (see luajit/src/luajit.c)
- 'globalL',
- # Tarantool main coro (see tarantool/src/lua/init.h)
- 'tarantool_L',
- # TODO: Add more
- ))):
- if lstate:
- return cast('lua_State *', lstate)
-
-
-def G(L):
- return mref('global_State *', L['glref'])
-
-
-def J(g):
- typeGG = gtype('GG_State')
-
- return cast('jit_State *', int(cast('char *', g))
- - int(typeGG['g'].bitpos / 8)
- + int(typeGG['J'].bitpos / 8))
-
-
-def vm_state(g):
- return {
- i2notu32(0): 'INTERP',
- i2notu32(1): 'LFUNC',
- i2notu32(2): 'FFUNC',
- i2notu32(3): 'CFUNC',
- i2notu32(4): 'GC',
- i2notu32(5): 'EXIT',
- i2notu32(6): 'RECORD',
- i2notu32(7): 'OPT',
- i2notu32(8): 'ASM',
- }.get(int(tou32(g['vmstate'])), 'TRACE')
-
-
-def gc_state(g):
- return {
- 0: 'PAUSE',
- 1: 'PROPAGATE',
- 2: 'ATOMIC',
- 3: 'SWEEPSTRING',
- 4: 'SWEEP',
- 5: 'FINALIZE',
- 6: 'LAST',
- }.get(int(g['gc']['state']), 'INVALID')
-
-
-def jit_state(g):
- return {
- 0: 'IDLE',
- 0x10: 'ACTIVE',
- 0x11: 'RECORD',
- 0x12: 'START',
- 0x13: 'END',
- 0x14: 'ASM',
- 0x15: 'ERR',
- }.get(int(J(g)['state']), 'INVALID')
-
-
-def tvisint(o):
- return LJ_DUALNUM and itype(o) == LJ_TISNUM
-
-
-def tvisnumber(o):
- return itype(o) <= LJ_TISNUM
-
-
-def tvislightud(o):
- if LJ_64 and not LJ_GC64:
- return (cast('int32_t', itype(o)) >> 15) == -2
- else:
- return itype(o) == LJ_T['LIGHTUD']
-
-
-def strdata(obj):
- # String is printed with pointer to it, thanks to gdb. Just strip it.
- try:
- return str(cast('char *', cast('GCstr *', obj) + 1))[len(PADDING):]
- except UnicodeEncodeError:
- return "<luajit-gdb: error occurred while rendering non-ascii slot>"
-
-
-def itypemap(o):
- if LJ_64 and not LJ_GC64:
- return LJ_T['NUMX'] if tvisnumber(o) \
- else LJ_T['LIGHTUD'] if tvislightud(o) \
- else itype(o)
- else:
- return LJ_T['NUMX'] if tvisnumber(o) else itype(o)
-
-
-def funcproto(func):
- assert func['ffid'] == 0
-
- return cast('GCproto *',
- mref('char *', func['pc']) - gdb.lookup_type('GCproto').sizeof)
-
-
-def gclistlen(root, end=0x0):
- count = 0
- while (gcref(root) != end):
- count += 1
- root = gcnext(root)
- return count
-
-
-def gcringlen(root):
- if not gcref(root):
- return 0
- elif gcref(root) == gcref(gcnext(root)):
- return 1
- else:
- return 1 + gclistlen(gcnext(root), gcref(root))
-
-
-gclen = {
- 'root': gclistlen,
- 'gray': gclistlen,
- 'grayagain': gclistlen,
- 'weak': gclistlen,
- # XXX: gc.mmudata is a ring-list.
- 'mmudata': gcringlen,
-}
-
-
-# The generator that implements frame iterator.
-# Every frame is represented as a tuple of framelink and frametop.
-def frames(L):
- frametop = L['top']
- framelink = L['base'] - 1
- framelink_sentinel = frame_sentinel(L)
- while True:
- yield framelink, frametop
- frametop = framelink - (1 + LJ_FR2)
- if framelink <= framelink_sentinel:
- break
- framelink = frame_prev(framelink)
-
-
-def lightudV(tv):
- if LJ_64:
- u = int(tv['u64'])
- # lightudseg macro expanded.
- seg = (u >> LJ_LIGHTUD_BITS_LO) & LIGHTUD_SEG_MASK
- segmap = mref('uint32_t *', G(L(None))['gc']['lightudseg'])
- # lightudlo macro expanded.
- return (int(segmap[seg]) << 32) | (u & LIGHTUD_LO_MASK)
- else:
- return gcval(tv['gcr'])
-
-
-# Dumpers {{{
-
-
-def dump_lj_tnil(tv):
- return 'nil'
-
-
-def dump_lj_tfalse(tv):
- return 'false'
-
-
-def dump_lj_ttrue(tv):
- return 'true'
-
-
-def dump_lj_tlightud(tv):
- return 'light userdata @ {}'.format(strx64(lightudV(tv)))
-
-
-def dump_lj_tstr(tv):
- return 'string {body} @ {address}'.format(
- body=strdata(gcval(tv['gcr'])),
- address=strx64(gcval(tv['gcr']))
- )
-
-
-def dump_lj_tupval(tv):
- return 'upvalue @ {}'.format(strx64(gcval(tv['gcr'])))
-
-
-def dump_lj_tthread(tv):
- return 'thread @ {}'.format(strx64(gcval(tv['gcr'])))
-
-
-def dump_lj_tproto(tv):
- return 'proto @ {}'.format(strx64(gcval(tv['gcr'])))
-
-
-def dump_lj_tfunc(tv):
- func = cast('struct GCfuncC *', gcval(tv['gcr']))
- ffid = func['ffid']
-
- if ffid == 0:
- pt = funcproto(func)
- return 'Lua function @ {addr}, {nups} upvalues, {chunk}:{line}'.format(
- addr=strx64(func),
- nups=int(func['nupvalues']),
- chunk=strdata(cast('GCstr *', gcval(pt['chunkname']))),
- line=pt['firstline']
- )
- elif ffid == 1:
- return 'C function @ {}'.format(strx64(func['f']))
- else:
- return 'fast function #{}'.format(int(ffid))
-
-
-def dump_lj_ttrace(tv):
- trace = cast('struct GCtrace *', gcval(tv['gcr']))
- return 'trace {traceno} @ {addr}'.format(
- traceno=strx64(trace['traceno']),
- addr=strx64(trace)
- )
-
-
-def dump_lj_tcdata(tv):
- return 'cdata @ {}'.format(strx64(gcval(tv['gcr'])))
-
-
-def dump_lj_ttab(tv):
- table = cast('GCtab *', gcval(tv['gcr']))
- return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format(
- gcr=strx64(table),
- asize=table['asize'],
- hmask=strx64(table['hmask']),
- )
-
-
-def dump_lj_tudata(tv):
- return 'userdata @ {}'.format(strx64(gcval(tv['gcr'])))
-
-
-def dump_lj_tnumx(tv):
- if tvisint(tv):
- return 'integer {}'.format(cast('int32_t', tv['i']))
- else:
- return 'number {}'.format(cast('double', tv['n']))
-
-
-def dump_lj_invalid(tv):
- return 'not valid type @ {}'.format(strx64(gcval(tv['gcr'])))
-
-
-# }}}
-
-
-dumpers = {
- 'LJ_TNIL': dump_lj_tnil,
- 'LJ_TFALSE': dump_lj_tfalse,
- 'LJ_TTRUE': dump_lj_ttrue,
- 'LJ_TLIGHTUD': dump_lj_tlightud,
- 'LJ_TSTR': dump_lj_tstr,
- 'LJ_TUPVAL': dump_lj_tupval,
- 'LJ_TTHREAD': dump_lj_tthread,
- 'LJ_TPROTO': dump_lj_tproto,
- 'LJ_TFUNC': dump_lj_tfunc,
- 'LJ_TTRACE': dump_lj_ttrace,
- 'LJ_TCDATA': dump_lj_tcdata,
- 'LJ_TTAB': dump_lj_ttab,
- 'LJ_TUDATA': dump_lj_tudata,
- 'LJ_TNUMX': dump_lj_tnumx,
-}
-
-
-def dump_tvalue(tvalue):
- return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue)
-
-
-def dump_framelink_slot_address(fr):
- return '{}:{}'.format(fr - 1, fr) if LJ_FR2 \
- else '{}'.format(fr) + PADDING
-
-
-def dump_framelink(L, fr):
- if fr == frame_sentinel(L):
- return '{addr} [S ] FRAME: dummy L'.format(
- addr=dump_framelink_slot_address(fr),
- )
- return '{addr} [ ] FRAME: [{pp}] delta={d}, {f}'.format(
- addr=dump_framelink_slot_address(fr),
- pp='PP' if frame_ispcall(fr) else '{frname}{p}'.format(
- frname=frametypes(int(frame_type(fr))),
- p='P' if frame_typep(fr) & FRAME_P else ''
- ),
- d=cast('TValue *', fr) - cast('TValue *', frame_prev(fr)),
- f=dump_lj_tfunc(fr - LJ_FR2),
- )
-
-
-def dump_stack_slot(L, slot, base=None, top=None):
- base = base or L['base']
- top = top or L['top']
-
- return '{addr}{padding} [ {B}{T}{M}] VALUE: {value}'.format(
- addr=strx64(slot),
- padding=PADDING,
- B='B' if slot == base else ' ',
- T='T' if slot == top else ' ',
- M='M' if slot == mref('TValue *', L['maxstack']) else ' ',
- value=dump_tvalue(slot),
- )
-
-
-def dump_stack(L, base=None, top=None):
- base = base or L['base']
- top = top or L['top']
- stack = mref('TValue *', L['stack'])
- maxstack = mref('TValue *', L['maxstack'])
- red = 5 + 3 * LJ_FR2
-
- dump = [
- '{padding} Red zone: {nredslots: >2} slots {padding}'.format(
- padding='-' * len(PADDING),
- nredslots=red,
- ),
- ]
- dump.extend([
- dump_stack_slot(L, maxstack + offset, base, top)
- for offset in range(red, 0, -1) # noqa: E131
- ])
- dump.extend([
- '{padding} Stack: {nstackslots: >5} slots {padding}'.format(
- padding='-' * len(PADDING),
- nstackslots=int((tou64(maxstack) - tou64(stack)) >> 3),
- ),
- dump_stack_slot(L, maxstack, base, top),
- '{start}:{end} [ ] {nfreeslots} slots: Free stack slots'.format(
- start=strx64(top + 1),
- end=strx64(maxstack - 1),
- nfreeslots=int((tou64(maxstack) - tou64(top) - 8) >> 3),
- ),
- ])
-
- for framelink, frametop in frames(L):
- # Dump all data slots in the (framelink, top) interval.
- dump.extend([
- dump_stack_slot(L, framelink + offset, base, top)
- for offset in range(frametop - framelink, 0, -1) # noqa: E131
- ])
- # Dump frame slot (2 slots in case of GC64).
- dump.append(dump_framelink(L, framelink))
-
- return '\n'.join(dump)
-
-
-def dump_gc(g):
- gc = g['gc']
- stats = ['{key}: {value}'.format(key=f, value=gc[f]) for f in (
- 'total', 'threshold', 'debt', 'estimate', 'stepmul', 'pause'
- )]
-
- stats += ['sweepstr: {sweepstr}/{strmask}'.format(
- sweepstr=gc['sweepstr'],
- # String hash mask (size of hash table - 1).
- strmask=g['strmask'] + 1,
- )]
-
- stats += ['{key}: {number} objects'.format(
- key=stat,
- number=handler(gc[stat])
- ) for stat, handler in gclen.items()]
-
- return '\n'.join(map(lambda s: '\t' + s, stats))
-
-
-class LJBase(gdb.Command):
-
- def __init__(self, name):
- # XXX Fragile: though the command initialization looks like a crap but
- # it respects both Python 2 and Python 3.
- gdb.Command.__init__(self, name, gdb.COMMAND_DATA)
- gdb.write('{} command initialized\n'.format(name))
-
-
-class LJDumpArch(LJBase):
- '''
-lj-arch
-
-The command requires no args and dumps values of LJ_64 and LJ_GC64
-compile-time flags. These values define the sizes of host and GC
-pointers respectively.
- '''
-
- def invoke(self, arg, from_tty):
- gdb.write(
- 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}\n'
- .format(
- LJ_64=LJ_64,
- LJ_GC64=LJ_GC64,
- LJ_DUALNUM=LJ_DUALNUM
- )
- )
-
-
-class LJDumpTValue(LJBase):
- '''
-lj-tv <TValue *>
-
-The command receives a pointer to <tv> (TValue address) and dumps
-the type and some info related to it.
-
-* LJ_TNIL: nil
-* LJ_TFALSE: false
-* LJ_TTRUE: true
-* LJ_TLIGHTUD: light userdata @ <gcr>
-* LJ_TSTR: string <string payload> @ <gcr>
-* LJ_TUPVAL: upvalue @ <gcr>
-* LJ_TTHREAD: thread @ <gcr>
-* LJ_TPROTO: proto @ <gcr>
-* LJ_TFUNC: <LFUNC|CFUNC|FFUNC>
- <LFUNC>: Lua function @ <gcr>, <nupvals> upvalues, <chunk:line>
- <CFUNC>: C function <mcode address>
- <FFUNC>: fast function #<ffid>
-* LJ_TTRACE: trace <traceno> @ <gcr>
-* LJ_TCDATA: cdata @ <gcr>
-* LJ_TTAB: table @ <gcr> (asize: <asize>, hmask: <hmask>)
-* LJ_TUDATA: userdata @ <gcr>
-* LJ_TNUMX: number <numeric payload>
-
-Whether the type of the given address differs from the listed above, then
-error message occurs.
- '''
-
- def invoke(self, arg, from_tty):
- tv = cast('TValue *', parse_arg(arg))
- gdb.write('{}\n'.format(dump_tvalue(tv)))
-
-
-class LJDumpString(LJBase):
- '''
-lj-str <GCstr *>
-
-The command receives a <gcr> of the corresponding GCstr object and dumps
-the payload, size in bytes and hash.
-
-*Caveat*: Since Python 2 provides no native Unicode support, the payload
-is replaced with the corresponding error when decoding fails.
- '''
-
- def invoke(self, arg, from_tty):
- string = cast('GCstr *', parse_arg(arg))
- gdb.write("String: {body} [{len} bytes] with hash {hash}\n".format(
- body=strdata(string),
- hash=strx64(string['hash']),
- len=string['len'],
- ))
-
-
-class LJDumpTable(LJBase):
- '''
-lj-tab <GCtab *>
-
-The command receives a GCtab address and dumps the table contents:
-* Metatable address whether the one is set
-* Array part <asize> slots:
- <aslot ptr>: [<index>]: <tv>
-* Hash part <hsize> nodes:
- <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr>
- '''
-
- def invoke(self, arg, from_tty):
- t = cast('GCtab *', parse_arg(arg))
- array = mref('TValue *', t['array'])
- nodes = mref('struct Node *', t['node'])
- mt = gcval(t['metatable'])
- capacity = {
- 'apart': int(t['asize']),
- 'hpart': int(t['hmask'] + 1) if t['hmask'] > 0 else 0
- }
-
- if mt != 0:
- gdb.write('Metatable detected: {}\n'.format(strx64(mt)))
-
- gdb.write('Array part: {} slots\n'.format(capacity['apart']))
- for i in range(capacity['apart']):
- slot = array + i
- gdb.write('{ptr}: [{index}]: {value}\n'.format(
- ptr=slot,
- index=i,
- value=dump_tvalue(slot)
- ))
-
- gdb.write('Hash part: {} nodes\n'.format(capacity['hpart']))
- # See hmask comment in lj_obj.h
- for i in range(capacity['hpart']):
- node = nodes + i
- gdb.write('{ptr}: {{ {key} }} => {{ {val} }}; next = {n}\n'.format(
- ptr=node,
- key=dump_tvalue(node['key']),
- val=dump_tvalue(node['val']),
- n=mref('struct Node *', node['next'])
- ))
-
-
-class LJDumpStack(LJBase):
- '''
-lj-stack [<lua_State *>]
-
-The command receives a lua_State address and dumps the given Lua
-coroutine guest stack:
-
-<slot ptr> [<slot attributes>] <VALUE|FRAME>
-
-* <slot ptr>: guest stack slot address
-* <slot attributes>:
- - S: Bottom of the stack (the slot L->stack points to)
- - B: Base of the current guest frame (the slot L->base points to)
- - T: Top of the current guest frame (the slot L->top points to)
- - M: Last slot of the stack (the slot L->maxstack points to)
-* <VALUE>: see help lj-tv for more info
-* <FRAME>: framelink slot differs from the value slot: it contains info
- related to the function being executed within this guest frame, its
- type and link to the parent guest frame
- [<frame type>] delta=<slots in frame>, <lj-tv for LJ_TFUNC slot>
- - <frame type>:
- + L: VM performs a call as a result of bytecode execution
- + C: VM performs a call as a result of lj_vm_call
- + M: VM performs a call to a metamethod as a result of bytecode
- execution
- + V: Variable-length frame for storing arguments of a variadic
- function
- + CP: Protected C frame
- + PP: VM performs a call as a result of executinig pcall or xpcall
-
-If L is omitted the main coroutine is used.
- '''
-
- def invoke(self, arg, from_tty):
- gdb.write('{}\n'.format(dump_stack(L(parse_arg(arg)))))
-
-
-class LJState(LJBase):
- '''
-lj-state
-The command requires no args and dumps current VM and GC states
-* VM state: <INTERP|C|GC|EXIT|RECORD|OPT|ASM|TRACE>
-* GC state: <PAUSE|PROPAGATE|ATOMIC|SWEEPSTRING|SWEEP|FINALIZE|LAST>
-* JIT state: <IDLE|ACTIVE|RECORD|START|END|ASM|ERR>
- '''
-
- def invoke(self, arg, from_tty):
- g = G(L(None))
- gdb.write('{}\n'.format('\n'.join(
- map(lambda t: '{} state: {}'.format(*t), {
- 'VM': vm_state(g),
- 'GC': gc_state(g),
- 'JIT': jit_state(g),
- }.items())
- )))
-
-
-class LJGC(LJBase):
- '''
-lj-gc
-
-The command requires no args and dumps current GC stats:
-* total: <total number of allocated bytes in GC area>
-* threshold: <limit when gc step is triggered>
-* debt: <how much GC is behind schedule>
-* estimate: <estimate of memory actually in use>
-* stepmul: <incremental GC step granularity>
-* pause: <pause between successive GC cycles>
-* sweepstr: <sweep position in string table>
-* root: <number of all collectable objects>
-* gray: <number of gray objects>
-* grayagain: <number of objects for atomic traversal>
-* weak: <number of weak tables (to be cleared)>
-* mmudata: <number of udata|cdata to be finalized>
- '''
-
- def invoke(self, arg, from_tty):
- g = G(L(None))
- gdb.write('GC stats: {state}\n{stats}\n'.format(
- state=gc_state(g),
- stats=dump_gc(g)
- ))
-
-
-def init(commands):
- global LJ_64, LJ_GC64, LJ_FR2, LJ_DUALNUM, LJ_TISNUM, PADDING
-
- # XXX Fragile: though connecting the callback looks like a crap but it
- # respects both Python 2 and Python 3 (see #4828).
- def connect(callback):
- if LEGACY:
- global CONNECTED
- CONNECTED = True
- gdb.events.new_objfile.connect(callback)
-
- # XXX Fragile: though disconnecting the callback looks like a crap but it
- # respects both Python 2 and Python 3 (see #4828).
- def disconnect(callback):
- if LEGACY:
- global CONNECTED
- if not CONNECTED:
- return
- CONNECTED = False
- gdb.events.new_objfile.disconnect(callback)
-
- try:
- # Try to remove the callback at first to not append duplicates to
- # gdb.events.new_objfile internal list.
- disconnect(load)
- except Exception:
- # Callback is not connected.
- pass
-
- try:
- # Detect whether libluajit objfile is loaded.
- gdb.parse_and_eval('luaJIT_setmode')
- except Exception:
- gdb.write('luajit-gdb.py initialization is postponed '
- 'until libluajit objfile is loaded\n')
- # Add a callback to be executed when the next objfile is loaded.
- connect(load)
- return
-
- try:
- LJ_64 = str(gdb.parse_and_eval('IRT_PTR')) == 'IRT_P64'
- LJ_FR2 = LJ_GC64 = str(gdb.parse_and_eval('IRT_PGC')) == 'IRT_P64'
- LJ_DUALNUM = gdb.lookup_global_symbol('lj_lib_checknumber') is not None
- except Exception:
- gdb.write('luajit-gdb.py failed to load: '
- 'no debugging symbols found for libluajit\n')
- return
-
- for name, command in commands.items():
- command(name)
-
- PADDING = ' ' * len(':' + hex((1 << (47 if LJ_GC64 else 32)) - 1))
- LJ_TISNUM = 0xfffeffff if LJ_64 and not LJ_GC64 else LJ_T['NUMX']
-
- gdb.write('luajit-gdb.py is successfully loaded\n')
-
-
-def load(event=None):
- init({
- 'lj-arch': LJDumpArch,
- 'lj-gc': LJGC,
- 'lj-stack': LJDumpStack,
- 'lj-state': LJState,
- 'lj-str': LJDumpString,
- 'lj-tab': LJDumpTable,
- 'lj-tv': LJDumpTValue,
- })
-
-
-load(None)
diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
new file mode 100644
index 00000000..164f0955
--- /dev/null
+++ b/src/luajit_dbg.py
@@ -0,0 +1,1281 @@
+# Debug extension for LuaJIT post-mortem analysis.
+# To use in GDB:
+# `source <path-to-repo>/src/luajit_dbg.py'
+# To use in LLDB:
+# `command script import <path-to-repo>/src/luajit_dbg.py'
+
+import abc
+import re
+import struct
+import sys
+
+from importlib import import_module
+
+# Make the script compatible with ancient Python.
+LEGACY = re.match(r'^2\.', sys.version)
+
+if LEGACY:
+ int = long # noqa: F821
+ range = xrange # noqa: F821
+
+
+# Debugger. ######################################################
+
+
+lldb = None
+gdb = None
+
+# XXX: While the `gdb` library is only available inside a debug
+# session, the `lldb` library can be loaded in any Python script.
+# To address that, we need to perform an additional check to
+# ensure a debug session is actually running.
+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)
+ if healthcheck(lib):
+ globals()[name] = lib
+ break
+ except Exception:
+ continue
+
+assert (not not lldb) != (not not gdb), 'Debugger must be either LLDB or GDB.'
+
+
+class Debugger(object):
+ def __init__(self):
+ self.dbgtype_cache = {}
+
+ def __new__(self):
+ if gdb:
+ self.GDB = True
+ return super(Debugger, self).__new__(_GDBDebugger)
+ elif lldb:
+ self.LLDB = True
+ return super(Debugger, self).__new__(_LLDBDebugger)
+
+ def configure(self):
+ global PADDING, LJ_TISNUM
+ if not self.check_libluajit():
+ return False
+ try:
+ self.init_luajit_arch()
+ 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']
+ except Exception:
+ self.write('luajit_dbg.py failed to load: '
+ 'no debugging symbols found for libluajit\n')
+ return False
+ return True
+
+ def initialize_extension(self, commands):
+ if self.configure():
+ for name, command in commands.items():
+ self.register_command(command, name)
+ self.write('{} command initialized\n'.format(name))
+ self.write('LuaJIT debug extension is successfully loaded\n')
+
+ @abc.abstractmethod
+ def cast(self, typestr, val):
+ '''Cast the value to the required C type.'''
+ pass
+
+ @abc.abstractmethod
+ def sizeof(self, typestr):
+ '''Return the size of the given type in bytes.'''
+ pass
+
+ @abc.abstractmethod
+ def offsetof(self, typestr, fieldstr):
+ '''Return the offset of the given field in the type in bytes.'''
+ pass
+
+ @abc.abstractmethod
+ def cstr(self, strptr):
+ '''Return the content of the string by the given pointer.'''
+ pass
+
+ @abc.abstractmethod
+ def lookup_global(self, symbol):
+ '''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 write(self, msg):
+ '''Print the message.'''
+ pass
+
+ @abc.abstractmethod
+ def check_libluajit(self):
+ '''Check that libluajit is loaded.
+ Check that the object file with libluajit symbols is loaded.
+ Postpone loading of the extension if needed.
+ '''
+ pass
+
+ @abc.abstractmethod
+ def init_luajit_arch(self):
+ '''Initialize LuaJIT architecture-specific globals.
+ Initialize build-dependent global constants.
+ If no debugging symbols are found raise an error.
+ '''
+ pass
+
+ @abc.abstractmethod
+ def register_command(self, command, name):
+ '''Register the command with the corresponding name.'''
+ pass
+
+ @abc.abstractproperty
+ def LJBase(self):
+ '''Base command class.
+ Provides the base class for the extension commands.
+ '''
+
+
+class _GDBDebugger(Debugger):
+ def _dbgtype(self, typestr):
+ if typestr in self.dbgtype_cache:
+ return self.dbgtype_cache[typestr]
+
+ m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr)
+
+ dbgtype = gdb.lookup_type(typestr) if m is None \
+ else gdb.lookup_type(m.group(1)).pointer()
+
+ self.dbgtype_cache[typestr] = dbgtype
+ return dbgtype
+
+ def __init__(self):
+ super(_GDBDebugger, self).__init__()
+ self.CONNECTED = False
+
+ def cast(self, typestr, val):
+ return gdb.Value(val).cast(self._dbgtype(typestr))
+
+ def sizeof(self, typestr):
+ return self._dbgtype(typestr).sizeof
+
+ def offsetof(self, typestr, fieldstr):
+ return int(self._dbgtype(typestr)[fieldstr].bitpos / 8)
+
+ def cstr(self, strptr):
+ # A string is printed with a pointer to it. Just strip it.
+ return re.sub(r'^0x[a-f0-9]+\s+(?=")', '', str(strptr))
+
+ def lookup_global(self, symbol):
+ variable, _ = gdb.lookup_symbol(symbol)
+ return variable.value() if variable else None
+
+ def eval(self, command):
+ if not command:
+ return None
+
+ ret = gdb.parse_and_eval(command)
+ if not ret:
+ raise gdb.GdbError('table argument empty')
+ return ret
+
+ def write(self, msg):
+ gdb.write(msg)
+
+ def check_libluajit(self):
+ # XXX Fragile: Though connecting the callback looks bad,
+ # it respects both Python 2 and Python 3 (see #4828).
+ def connect(callback):
+ if LEGACY:
+ self.CONNECTED = True
+ gdb.events.new_objfile.connect(callback)
+
+ # XXX Fragile: Though disconnecting the callback looks
+ # bad, it respects both Python 2 and Python 3 (see #4828).
+ def disconnect(callback):
+ if LEGACY:
+ if not self.CONNECTED:
+ return
+ self.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_dbg.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 False
+ return True
+
+ def init_luajit_arch(self):
+ global LJ_64, LJ_DUALNUM, LJ_FR2, LJ_GC64
+ LJ_64 = str(gdb.parse_and_eval('IRT_PTR')) == 'IRT_P64'
+ LJ_DUALNUM = gdb.lookup_global_symbol('lj_lib_checknumber') is not None
+ LJ_FR2 = LJ_GC64 = str(gdb.parse_and_eval('IRT_PGC')) == 'IRT_P64'
+
+ def register_command(self, command, name):
+ command(name)
+
+ class LJBase(gdb and gdb.Command or object):
+ def __init__(ljbase, name):
+ # XXX Fragile: Though the command initialization looks
+ # bad, it respects both Python 2 and Python 3.
+ gdb.Command.__init__(ljbase, name, gdb.COMMAND_DATA)
+
+ def invoke(ljbase, args, from_tty):
+ return ljbase.execute(args)
+
+ @abc.abstractmethod
+ def execute(ljbase, args):
+ '''Implementation of the command.
+ Subclasses override this method to implement the logic of a given
+ command, e.g. printing a stack.
+ '''
+
+ LJBase = LJBase
+
+
+class _LLDBDebugger(Debugger):
+ def _lldb_tp_isfp(self, tp):
+ return tp.GetBasicType() in [
+ lldb.eBasicTypeFloat,
+ lldb.eBasicTypeDouble,
+ lldb.eBasicTypeLongDouble
+ ]
+
+ def _lldb_value_from_raw(self, raw_value, size, tp):
+ isfp = self._lldb_tp_isfp(tp)
+ pack_flag = '<d' if isfp else '<Q'
+ raw_data = struct.pack(pack_flag, raw_value)
+ sbdata = lldb.SBData()
+ sbdata.SetData(
+ lldb.SBError(),
+ raw_data,
+ lldb.eByteOrderLittle,
+ size
+ )
+ sbval_res = self.target.CreateValueFromData(
+ # XXX: Name is required, let's make it meaningful.
+ '({tp}){val}'.format(
+ tp=tp.name,
+ val=raw_value if isfp else hex(raw_value)
+ ),
+ sbdata,
+ tp
+ )
+ return lldb.value(sbval_res)
+
+ def __init__(self):
+ def lldb__add__(lldbval, other):
+ other = int(other)
+ sbvalue = lldbval.sbvalue
+ if sbvalue.TypeIsPointerType():
+ tp = sbvalue.GetType()
+ sz = sbvalue.deref.size
+ addr = sbvalue.GetValueAsUnsigned() + other * sz
+ return self._lldb_value_from_raw(
+ addr, sbvalue.GetByteSize(), tp
+ )
+ else:
+ return int(lldbval) + other
+
+ def lldb__bool__(lldbval):
+ return int(lldbval) != 0
+
+ def lldb__ge__(lldbval, other):
+ return int(lldbval) >= int(other)
+
+ def lldb__getitem__(lldbval, key):
+ if type(key) is lldb.value:
+ key = int(key)
+ if type(key) is int:
+ # Allow array access.
+ return lldb.value(
+ lldbval.sbvalue.GetValueForExpressionPath('[%i]' % key)
+ )
+ elif type(key) is str:
+ return lldb.value(lldbval.sbvalue.GetChildMemberWithName(key))
+ raise Exception(TypeError('No item of type %s' % str(type(key))))
+
+ def lldb__gt__(lldbval, other):
+ return int(lldbval) > int(other)
+
+ def lldb__le__(lldbval, other):
+ return int(lldbval) <= int(other)
+
+ def lldb__lt__(lldbval, other):
+ return int(lldbval) < int(other)
+
+ def lldb__str__(lldbval):
+ # Instead of default GetSummary.
+ if not lldbval.sbvalue.TypeIsPointerType():
+ tp = lldbval.sbvalue.GetType()
+ is_float = self._lldb_tp_isfp(tp)
+ if is_float:
+ return lldbval.sbvalue.GetValue()
+ else:
+ return str(int(lldbval))
+
+ s = lldbval.sbvalue.GetValue()
+ if s[:2] == '0x':
+ # Strip useless leading zeros.
+ res = s[2:].lstrip('0')
+ return '0x' + (res if res else '0')
+ return s
+
+ def lldb__sub__(lldbval, other):
+ if type(other) is not lldb.value or \
+ type(other) is lldb.value and \
+ not other.sbvalue.TypeIsPointerType():
+ other = int(other)
+ if type(other) is int:
+ return lldb__add__(lldbval, -other)
+ elif lldbval.sbvalue.TypeIsPointerType():
+ sbval = lldbval.sbvalue
+ osbval = other.sbvalue
+ lldbval_tp = sbval.GetType()
+ other_tp = osbval.GetType()
+ # Subtract pointers of the same size only.
+ elsz = lldbval_tp.GetDereferencedType().size
+ if other_tp.GetDereferencedType().size != elsz:
+ raise Exception(
+ 'Attempt to substruct {otp} from {stp}'.format(
+ stp=lldbval_tp.name,
+ otp=other_tp.name
+ )
+ )
+ diff = sbval.GetValueAsUnsigned() - osbval.GetValueAsUnsigned()
+ return int(diff / elsz)
+ else:
+ return int(lldbval) - int(other)
+
+ super(_LLDBDebugger, self).__init__()
+ self.target = lldb.debugger.GetSelectedTarget()
+ # Monkey-patch the lldb.value class.
+ lldb.value.__add__ = lldb__add__
+ lldb.value.__bool__ = lldb__bool__
+ lldb.value.__ge__ = lldb__ge__
+ lldb.value.__getitem__ = lldb__getitem__
+ lldb.value.__gt__ = lldb__gt__
+ lldb.value.__le__ = lldb__le__
+ lldb.value.__lt__ = lldb__lt__
+ lldb.value.__str__ = lldb__str__
+ lldb.value.__sub__ = lldb__sub__
+
+ def lldb_major_version():
+ version_string = lldb.SBDebugger.GetVersionString()
+ match = re.search(r'(\d+)', version_string)
+ if match:
+ return int(match.group(1))
+ return None
+
+ # Needed for features detection.
+ self.version = lldb_major_version()
+
+ def _dbgtype(self, typestr):
+ if typestr in self.dbgtype_cache:
+ return self.dbgtype_cache[typestr]
+
+ m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr)
+
+ dbgtype = self.target.FindFirstType(typestr) if m is None \
+ else self.target.FindFirstType(m.group(1)).GetPointerType()
+
+ self.dbgtype_cache[typestr] = dbgtype
+ return dbgtype
+
+ def cast(self, typestr, val):
+ if isinstance(val, lldb.value):
+ val = val.sbvalue
+ elif type(val) is int:
+ tp = self._dbgtype(typestr)
+ return self._lldb_value_from_raw(val, tp.GetByteSize(), tp)
+ elif not isinstance(val, lldb.SBValue):
+ raise Exception(
+ 'Unexpected cast from type: {t}.'.format(t=type(val))
+ )
+
+ # XXX: Simply SBValue.Cast() works incorrectly since it
+ # may take the 8 bytes of memory instead of 4, before the
+ # cast. Construct the value on the fly.
+ tp = self._dbgtype(typestr)
+ is_fp = self._lldb_tp_isfp(tp)
+ rawval = float(val.GetValue()) if is_fp else val.GetValueAsUnsigned()
+ return self._lldb_value_from_raw(rawval, val.GetByteSize(), tp)
+
+ def sizeof(self, typestr):
+ return self._dbgtype(typestr).GetByteSize()
+
+ def offsetof(self, typestr, fieldstr):
+ def _type_member(type_obj, name):
+ return next((x for x in type_obj.members if x.name == name), None)
+
+ type_obj = self._dbgtype(typestr)
+ member = _type_member(type_obj, fieldstr)
+ assert member is not None, 'There is no field {f} in {t}'.format(
+ f=fieldstr,
+ t=typestr,
+ )
+ return member.GetOffsetInBytes()
+
+ def cstr(self, strptr):
+ return strptr.sbvalue.summary
+
+ def lookup_global(self, symbol):
+ return self.target.FindFirstGlobalVariable(symbol)
+
+ def eval(self, command):
+ if not command:
+ return None
+
+ process = self.target.GetProcess()
+ thread = process.GetSelectedThread()
+ frame = thread.GetSelectedFrame()
+ ret = frame.EvaluateExpression(command)
+ return ret
+
+ def write(self, msg):
+ sys.stdout.write(msg)
+
+ def check_libluajit(self):
+ # TODO: Implement postpone loading for LLDB too.
+ return True
+
+ def init_luajit_arch(self):
+ global LJ_64, LJ_DUALNUM, LJ_FR2, LJ_GC64
+ IRT_P64 = 9
+ module = self.target.modules[0]
+ LJ_DUALNUM = module.FindSymbol('lj_lib_checknumber') is not None
+ irtype_enum = self.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
+
+ def register_command(self, command, name):
+ command.name = name
+ lldb.debugger.HandleCommand(
+ 'command script add {o} --class luajit_dbg.{cls} {cmd}'.format(
+ o='--overwrite' if self.version >= 14 else '',
+ cls=command.__name__,
+ cmd=name,
+ )
+ )
+
+ class LJBase(object):
+ # Ignore given parameters by LLDB.
+ def __init__(ljbase, debugger, unused):
+ pass
+
+ def get_short_help(ljbase):
+ return ljbase.__doc__.splitlines()[1]
+
+ def get_long_help(ljbase):
+ return ljbase.__doc__
+
+ def __call__(ljbase, debugger, args, exe_ctx, result):
+ try:
+ ljbase.execute(args)
+ except Exception as e:
+ msg = 'Failed to execute command `{}`: {}'.format(
+ ljbase.name,
+ e
+ )
+ result.SetError(msg)
+
+ @abc.abstractmethod
+ def execute(ljbase, args):
+ '''Implementation of the command.
+ Subclasses override this method to implement the logic of a given
+ command, e.g. printing a stack. Any unhandled exception will be
+ automatically transformed into proper errors.
+ '''
+
+ LJBase = LJBase
+
+
+dbg = Debugger()
+
+
+# LuaJIT. ########################################################
+
+
+# Constants.
+
+
+LJ_64 = None
+LJ_DUALNUM = None
+LJ_FR2 = None
+LJ_GC64 = 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
+
+
+# Simple converters.
+
+
+def tou64(val):
+ return dbg.cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF
+
+
+def tou32(val):
+ return int(val) & 0xFFFFFFFF
+
+
+def i2notu32(val):
+ return ~int(val) & 0xFFFFFFFF
+
+
+def strx64(val):
+ return re.sub('L?$', '', hex(int(tou64(val))))
+
+
+# Types and TValues.
+
+
+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')
+
+
+def itype(o):
+ return tou32(o['it64'] >> 47) if LJ_GC64 else o['it']
+
+
+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 (dbg.cast('int32_t', itype(o)) >> 15) == -2
+ else:
+ return itype(o) == LJ_T['LIGHTUD']
+
+
+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)
+
+
+# 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 dbg.cast('ptrdiff_t', framelink['ftsz'] if LJ_FR2
+ else framelink['fr']['tp']['ftsz'])
+
+
+def frame_pc(framelink):
+ return dbg.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 dbg.cast('TValue *',
+ dbg.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
+
+
+# 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)
+
+
+# LuaJIT macro implementations and structure access.
+
+
+def mref(typename, obj):
+ return dbg.cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32'])
+
+
+def gcref(obj):
+ return dbg.cast('GCobj *', obj['gcptr64'] if LJ_GC64
+ else dbg.cast('uintptr_t', obj['gcptr32']))
+
+
+def gcval(obj):
+ return dbg.cast('GCobj *', obj['gcptr64'] & LJ_GCVMASK if LJ_GC64
+ else dbg.cast('uintptr_t', obj['gcptr32']))
+
+
+def gcnext(obj):
+ return gcref(obj)['gch']['nextgc']
+
+
+def L(L=None):
+ # Look up a symbol for the main coroutine considering the host
+ # application.
+ # XXX Fragile: Though the loop initialization looks bad, it
+ # respects both Python 2 and Python 3.
+ for lstate in [L] + list(map(lambda main: dbg.lookup_global(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 dbg.cast('lua_State *', lstate)
+
+
+def G(L):
+ return mref('global_State *', L['glref'])
+
+
+def J(g):
+ g_offset = dbg.offsetof('GG_State', 'g')
+ J_offset = dbg.offsetof('GG_State', 'J')
+ return dbg.cast('jit_State *', dbg.cast('char *', g) - g_offset + J_offset)
+
+
+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 strdata(obj):
+ try:
+ return dbg.cstr(dbg.cast('char *', dbg.cast('GCstr *', obj) + 1))
+ except UnicodeEncodeError:
+ return "<luajit_dbg: error occurred while rendering non-ascii slot>"
+
+
+def funcproto(func):
+ assert func['ffid'] == 0, 'Attempt to take a prototype of non-Lua function'
+ return dbg.cast('GCproto *',
+ mref('char *', func['pc']) - dbg.sizeof('GCproto'))
+
+
+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,
+}
+
+
+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 = dbg.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(dbg.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 = dbg.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 = dbg.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(dbg.cast('int32_t', tv['i']))
+ else:
+ return 'number {}'.format(dbg.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=dbg.cast('TValue *', fr) - dbg.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 + 3 * 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))
+
+
+# Extension commands. ############################################
+
+
+class LJDumpArch(dbg.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 execute(self, arg):
+ dbg.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 LJGC(dbg.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 execute(self, arg):
+ g = G(L(None))
+ dbg.write('GC stats: {state}\n{stats}\n'.format(
+ state=gc_state(g),
+ stats=dump_gc(g)
+ ))
+
+
+class LJDumpStack(dbg.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 a 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 executing pcall or xpcall
+
+If L is omitted, the main coroutine is used.
+ '''
+
+ def execute(self, arg):
+ dbg.write('{}\n'.format(dump_stack(L(dbg.eval(arg)))))
+
+
+class LJState(dbg.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 execute(self, arg):
+ g = G(L(None))
+ dbg.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 LJDumpString(dbg.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 execute(self, arg):
+ string = dbg.cast('GCstr *', dbg.eval(arg))
+ dbg.write("String: {body} [{len} bytes] with hash {hash}\n".format(
+ body=strdata(string),
+ hash=strx64(string['hash']),
+ len=string['len'],
+ ))
+
+
+class LJDumpTable(dbg.LJBase):
+ '''
+lj-tab <GCtab *>
+
+The command receives a GCtab address and dumps the table contents:
+* Metatable address whether the one is set
+* Array part <asize> slots:
+ <aslot ptr>: [<index>]: <tv>
+* Hash part <hsize> nodes:
+ <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr>
+ '''
+
+ def execute(self, arg):
+ t = dbg.cast('GCtab *', dbg.eval(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:
+ dbg.write('Metatable detected: {}\n'.format(strx64(mt)))
+
+ dbg.write('Array part: {} slots\n'.format(capacity['apart']))
+ for i in range(capacity['apart']):
+ slot = array + i
+ dbg.write('{ptr}: [{index}]: {value}\n'.format(
+ ptr=slot,
+ index=i,
+ value=dump_tvalue(slot)
+ ))
+
+ dbg.write('Hash part: {} nodes\n'.format(capacity['hpart']))
+ # See hmask comment in lj_obj.h
+ for i in range(capacity['hpart']):
+ node = nodes + i
+ dbg.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 LJDumpTValue(dbg.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|integer> <numeric payload>
+
+Whether the type of the given address differs from the listed above, then
+error message occurs.
+ '''
+
+ def execute(self, arg):
+ tv = dbg.cast('TValue *', dbg.eval(arg))
+ dbg.write('{}\n'.format(dump_tvalue(tv)))
+
+
+def load(event=None):
+ dbg.initialize_extension({
+ 'lj-arch': LJDumpArch,
+ 'lj-gc': LJGC,
+ 'lj-stack': LJDumpStack,
+ 'lj-state': LJState,
+ 'lj-str': LJDumpString,
+ 'lj-tab': LJDumpTable,
+ 'lj-tv': LJDumpTValue,
+ })
+
+
+if gdb:
+ load()
+elif lldb:
+ def __lldb_init_module(debugger, internal_dictionary):
+ load()
diff --git a/src/luajit_lldb.py b/src/luajit_lldb.py
deleted file mode 100644
index e3fe82fc..00000000
--- a/src/luajit_lldb.py
+++ /dev/null
@@ -1,1015 +0,0 @@
-# LLDB extension for LuaJIT post-mortem analysis.
-# To use, just put 'command script import <path-to-repo>/src/luajit_lldb.py'
-# in lldb.
-
-import abc
-import re
-import lldb
-import struct
-
-LJ_64 = None
-LJ_GC64 = None
-LJ_FR2 = None
-LJ_DUALNUM = None
-PADDING = None
-
-# Constants
-IRT_P64 = 9
-LJ_GCVMASK = ((1 << 47) - 1)
-LJ_TISNUM = 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
-
-# Debugger specific {{{
-
-
-# Global
-target = None
-
-
-def lldb_tp_isfp(tp):
- return tp.GetBasicType() in [
- lldb.eBasicTypeFloat,
- lldb.eBasicTypeDouble,
- lldb.eBasicTypeLongDouble
- ]
-
-
-def lldb_value_from_raw(raw_value, size, tp):
- isfp = lldb_tp_isfp(tp)
- pack_flag = '<d' if isfp else '<Q'
- raw_data = struct.pack(pack_flag, raw_value)
- sbdata = lldb.SBData()
- sbdata.SetData(
- lldb.SBError(),
- raw_data,
- lldb.eByteOrderLittle,
- size
- )
- sbval_res = target.CreateValueFromData(
- # XXX: The name is required. Let's make it meaningful.
- '({tp}){val}'.format(
- tp=tp.name,
- val=raw_value if isfp else hex(raw_value)
- ),
- sbdata,
- tp
- )
- return lldb.value(sbval_res)
-
-
-def lldb__add__(self, other):
- other = int(other)
- sbvalue = self.sbvalue
- if sbvalue.TypeIsPointerType():
- tp = sbvalue.GetType()
- sz = sbvalue.deref.size
- addr = sbvalue.GetValueAsUnsigned() + other * sz
- return lldb_value_from_raw(addr, sbvalue.GetByteSize(), tp)
- else:
- return int(self) + other
-
-
-def lldb__bool__(self):
- return int(self) != 0
-
-
-def lldb__ge__(self, other):
- return int(self) >= int(other)
-
-
-def lldb__getitem__(self, key):
- if type(key) is lldb.value:
- key = int(key)
- if type(key) is int:
- # Allow array access.
- return lldb.value(self.sbvalue.GetValueForExpressionPath('[%i]' % key))
- elif type(key) is str:
- return lldb.value(self.sbvalue.GetChildMemberWithName(key))
- raise Exception(TypeError('No item of type %s' % str(type(key))))
-
-
-def lldb__gt__(self, other):
- return int(self) > int(other)
-
-
-def lldb__le__(self, other):
- return int(self) <= int(other)
-
-
-def lldb__lt__(self, other):
- return int(self) < int(other)
-
-
-def lldb__str__(self):
- # Instead of default GetSummary.
- if not self.sbvalue.TypeIsPointerType():
- tp = self.sbvalue.GetType()
- is_float = lldb_tp_isfp(tp)
- if is_float:
- return self.sbvalue.GetValue()
- else:
- return str(int(self))
-
- s = self.sbvalue.GetValue()
- if s[:2] == '0x':
- # Strip useless leading zeros.
- res = s[2:].lstrip('0')
- return '0x' + (res if res else '0')
- return s
-
-
-def lldb__sub__(self, other):
- if type(other) is not lldb.value or \
- type(other) is lldb.value and not other.sbvalue.TypeIsPointerType():
- other = int(other)
- if type(other) is int:
- return lldb__add__(self, -other)
- elif self.sbvalue.TypeIsPointerType():
- ssbval = self.sbvalue
- osbval = other.sbvalue
- self_tp = ssbval.GetType()
- other_tp = osbval.GetType()
- # Subtract pointers of the same size only.
- elsz = self_tp.GetDereferencedType().size
- if other_tp.GetDereferencedType().size != elsz:
- raise Exception('Attempt to substruct {otp} from {stp}'.format(
- stp=self_tp.name,
- otp=other_tp.name
- ))
- diff = ssbval.GetValueAsUnsigned() - osbval.GetValueAsUnsigned()
- return int(diff / elsz)
- else:
- return int(self) - int(other)
-
-
-# Monkey-patch the lldb.value class.
-lldb.value.__add__ = lldb__add__
-lldb.value.__bool__ = lldb__bool__
-lldb.value.__ge__ = lldb__ge__
-lldb.value.__getitem__ = lldb__getitem__
-lldb.value.__gt__ = lldb__gt__
-lldb.value.__le__ = lldb__le__
-lldb.value.__lt__ = lldb__lt__
-lldb.value.__str__ = lldb__str__
-lldb.value.__sub__ = lldb__sub__
-
-
-class Command(object):
- def __init__(self, debugger, unused):
- pass
-
- def get_short_help(self):
- return self.__doc__.splitlines()[0]
-
- def get_long_help(self):
- return self.__doc__
-
- def __call__(self, debugger, command, exe_ctx, result):
- try:
- self.execute(debugger, command, result)
- 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
-
- @abc.abstractproperty
- def command(self):
- """Command name.
- This name will be used by LLDB in order to unique/ly identify an
- implementation that should be executed when a command is run
- in the REPL.
- """
-
- @abc.abstractmethod
- def execute(self, debugger, args, result):
- """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
- communicated back via the provided result object, so that it's
- properly routed to LLDB frontend. Any unhandled exception will be
- automatically transformed into proper errors.
- """
-
-
-gtype_cache = {}
-
-
-def gtype(typestr):
- if typestr in gtype_cache:
- return gtype_cache[typestr]
-
- m = re.match(r'((?:(?:struct|union) )?\S*)\s*[*]', typestr)
-
- gtype = target.FindFirstType(typestr) if m is None \
- else target.FindFirstType(m.group(1)).GetPointerType()
-
- gtype_cache[typestr] = gtype
- return gtype
-
-
-def cast(typestr, val):
- if isinstance(val, lldb.value):
- val = val.sbvalue
- elif type(val) is int:
- tp = gtype(typestr)
- return lldb_value_from_raw(val, tp.GetByteSize(), tp)
- elif not isinstance(val, lldb.SBValue):
- raise Exception('unexpected cast from type: {t}'.format(t=type(val)))
-
- # XXX: Simply SBValue.Cast() works incorrectly since it may
- # take the 8 bytes of memory instead of 4, before the cast.
- # Construct the value on the fly.
- tp = gtype(typestr)
- is_fp = lldb_tp_isfp(tp)
- rawval = float(val.GetValue()) if is_fp else val.GetValueAsUnsigned()
- return lldb_value_from_raw(rawval, val.GetByteSize(), tp)
-
-
-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 offsetof(typename, membername):
- type_obj = gtype(typename)
- member = type_member(type_obj, membername)
- assert member is not None
- return member.GetOffsetInBytes()
-
-
-def sizeof(typename):
- type_obj = gtype(typename)
- return type_obj.GetByteSize()
-
-
-def tou64(val):
- return cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF
-
-
-def dbg_eval(expr):
- process = target.GetProcess()
- thread = process.GetSelectedThread()
- frame = thread.GetSelectedFrame()
- return frame.EvaluateExpression(expr)
-
-
-# }}} Debugger specific
-
-
-def gcval(obj):
- return cast('GCobj *', obj['gcptr64'] & LJ_GCVMASK if LJ_GC64
- else cast('uintptr_t', obj['gcptr32']))
-
-
-def gcref(obj):
- return cast('GCobj *', obj['gcptr64'] if LJ_GC64
- else cast('uintptr_t', obj['gcptr32']))
-
-
-def gcnext(obj):
- return gcref(obj)['gch']['nextgc']
-
-
-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,
-}
-
-
-def dump_gc(g):
- gc = g['gc']
- stats = ['{key}: {value}'.format(key=f, value=getattr(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(getattr(gc, stat))
- ) for stat, handler in gclen.items()]
- return '\n'.join(map(lambda s: '\t' + s, stats))
-
-
-def mref(typename, obj):
- return cast(typename, obj['ptr64'] if LJ_GC64 else obj['ptr32'])
-
-
-def J(g):
- g_offset = offsetof('GG_State', 'g')
- J_offset = offsetof('GG_State', 'J')
- return cast('jit_State *', (cast('char *', g) - g_offset + J_offset))
-
-
-def G(L):
- return mref('global_State *', L['glref'])
-
-
-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), (
- # 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 tou32(val):
- return val & 0xFFFFFFFF
-
-
-def i2notu32(val):
- return ~int(val) & 0xFFFFFFFF
-
-
-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 strx64(val):
- return re.sub('L?$', '',
- hex(int(cast('uint64_t', val) & 0xFFFFFFFFFFFFFFFF)))
-
-
-def funcproto(func):
- assert func.ffid == 0
- return cast('GCproto *', mref('char *', func.pc) - sizeof('GCproto'))
-
-
-def strdata(obj):
- try:
- ptr = cast('char *', cast('GCstr *', obj) + 1)
- return ptr.sbvalue.summary
- except UnicodeEncodeError:
- return "<luajit-lldb: error occurred while rendering non-ascii slot>"
-
-
-def itype(o):
- return tou32(o['it64'] >> 47) if LJ_GC64 else o['it']
-
-
-def tvisint(o):
- return LJ_DUALNUM and itype(o) == LJ_TISNUM
-
-
-def tvislightud(o):
- if LJ_64 and not LJ_GC64:
- return (int(cast('int32_t', itype(o))) >> 15) == -2
- else:
- return itype(o) == LJ_T['LIGHTUD']
-
-
-def tvisnumber(o):
- return itype(o) <= LJ_TISNUM
-
-
-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'])
-
-
-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(cast('GCstr *', 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('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=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(ffid)
-
-
-def dump_lj_ttrace(tv):
- trace = cast('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,
-}
-
-
-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 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 typenames(value):
- return {
- LJ_T[k]: 'LJ_T' + k for k in LJ_T.keys()
- }.get(int(value), 'LJ_TINVALID')
-
-
-def dump_tvalue(tvalue):
- return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue)
-
-
-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 framelink - int(frame_sized(framelink) / sizeof('TValue'))
-
-
-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(frame_type(framelink)) == 'L' \
- and 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
-
-
-# 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 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 + 3 * 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)
-
-
-class LJDumpTValue(Command):
- '''
-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 execute(self, debugger, args, result):
- tvptr = cast('TValue *', self.parse(args))
- print('{}'.format(dump_tvalue(tvptr)))
-
-
-class LJState(Command):
- '''
-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 execute(self, debugger, args, result):
- g = G(L(None))
- print('{}'.format('\n'.join(
- map(lambda t: '{} state: {}'.format(*t), {
- 'VM': vm_state(g),
- 'GC': gc_state(g),
- 'JIT': jit_state(g),
- }.items())
- )))
-
-
-class LJDumpArch(Command):
- '''
-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 execute(self, debugger, args, result):
- print(
- 'LJ_64: {LJ_64}, LJ_GC64: {LJ_GC64}, LJ_DUALNUM: {LJ_DUALNUM}'
- .format(
- LJ_64=LJ_64,
- LJ_GC64=LJ_GC64,
- LJ_DUALNUM=LJ_DUALNUM
- )
- )
-
-
-class LJGC(Command):
- '''
-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 execute(self, debugger, args, result):
- g = G(L(None))
- print('GC stats: {state}\n{stats}'.format(
- state=gc_state(g),
- stats=dump_gc(g)
- ))
-
-
-class LJDumpString(Command):
- '''
-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 execute(self, debugger, args, result):
- string = cast('GCstr *', self.parse(args))
- print("String: {body} [{len} bytes] with hash {hash}".format(
- body=strdata(string),
- hash=strx64(string['hash']),
- len=string['len'],
- ))
-
-
-class LJDumpTable(Command):
- '''
-lj-tab <GCtab *>
-
-The command receives a GCtab address and dumps the table contents:
-* Metatable address whether the one is set
-* Array part <asize> slots:
- <aslot ptr>: [<index>]: <tv>
-* Hash part <hsize> nodes:
- <hnode ptr>: { <tv> } => { <tv> }; next = <next hnode ptr>
- '''
- def execute(self, debugger, args, result):
- t = cast('GCtab *', self.parse(args))
- array = mref('TValue *', t['array'])
- nodes = mref('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:
- print('Metatable detected: {}'.format(strx64(mt)))
-
- print('Array part: {} slots'.format(capacity['apart']))
- for i in range(capacity['apart']):
- slot = array + i
- print('{ptr}: [{index}]: {value}'.format(
- ptr=strx64(slot),
- index=i,
- value=dump_tvalue(slot)
- ))
-
- print('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(
- ptr=strx64(node),
- key=dump_tvalue(node['key']),
- val=dump_tvalue(node['val']),
- n=strx64(mref('Node *', node['next']))
- ))
-
-
-class LJDumpStack(Command):
- '''
-lj-stack [<lua_State *>]
-
-The command receives a lua_State address and dumps the given Lua
-coroutine guest stack:
-
-<slot ptr> [<slot attributes>] <VALUE|FRAME>
-
-* <slot ptr>: guest stack slot address
-* <slot attributes>:
- - S: Bottom of the stack (the slot L->stack points to)
- - B: Base of the current guest frame (the slot L->base points to)
- - T: Top of the current guest frame (the slot L->top points to)
- - M: Last slot of the stack (the slot L->maxstack points to)
-* <VALUE>: see help lj-tv for more info
-* <FRAME>: framelink slot differs from the value slot: it contains info
- related to the function being executed within this guest frame, its
- type and link to the parent guest frame
- [<frame type>] delta=<slots in frame>, <lj-tv for LJ_TFUNC slot>
- - <frame type>:
- + L: VM performs a call as a result of bytecode execution
- + C: VM performs a call as a result of lj_vm_call
- + M: VM performs a call to a metamethod as a result of bytecode
- execution
- + V: Variable-length frame for storing arguments of a variadic
- function
- + CP: Protected C frame
- + PP: VM performs a call as a result of executinig pcall or xpcall
-
-If L is omitted the main coroutine is used.
- '''
- def execute(self, debugger, args, result):
- print('{}'.format(dump_stack(L(self.parse(args)))))
-
-
-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 initialized'.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
-
- 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
- except Exception:
- print('luajit_lldb.py failed to load: '
- 'no debugging symbols found for libluajit')
- return
-
- 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']
-
-
-def __lldb_init_module(debugger, internal_dict):
- configure(debugger)
- register_commands(debugger, {
- 'lj-arch': LJDumpArch,
- 'lj-gc': LJGC,
- 'lj-stack': LJDumpStack,
- 'lj-state': LJState,
- 'lj-str': LJDumpString,
- 'lj-tab': LJDumpTable,
- 'lj-tv': LJDumpTValue,
- })
- print('luajit_lldb.py is successfully loaded')
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index 2b67e151..30a2c478 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -16,7 +16,7 @@ LUAJIT_BINARY = os.environ['LUAJIT_TEST_BINARY']
EXTENSION_PATH = os.environ['DEBUGGER_EXTENSION_PATH']
DEBUGGER = os.environ['DEBUGGER_COMMAND']
LLDB = 'lldb' in DEBUGGER
-EXTENSION = EXTENSION_PATH + ('/luajit_lldb.py' if LLDB else '/luajit-gdb.py')
+EXTENSION = EXTENSION_PATH + '/luajit_dbg.py'
TIMEOUT = 10
# Don't run any initialization scripts.
@@ -137,7 +137,7 @@ class TestLoad(TestCaseBase):
r'lj-str command initialized\n'
r'lj-tab command initialized\n'
r'lj-tv command initialized\n'
- r'.*is successfully loaded'
+ r'LuaJIT debug extension is successfully loaded'
)
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [Tarantool-patches] [PATCH v2 luajit 6/6] ci: introduce workflow to test debugger extension
2026-05-19 12:39 [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers Sergey Kaplun via Tarantool-patches
` (4 preceding siblings ...)
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 5/6] dbg: generalize extension Sergey Kaplun via Tarantool-patches
@ 2026-05-19 12:39 ` Sergey Kaplun via Tarantool-patches
5 siblings, 0 replies; 7+ messages in thread
From: Sergey Kaplun via Tarantool-patches @ 2026-05-19 12:39 UTC (permalink / raw)
To: Mikhail Elhimov, Sergey Bronnikov, Evgeniy Temirgaleev; +Cc: tarantool-patches
The debugger extension test is run for both LLDB and GDB for GC64 and
non-GC64 builds for arm64 and x86_64.
---
.github/actions/setup-debuggers/README.md | 13 +++++
.github/actions/setup-debuggers/action.yml | 12 +++++
.github/workflows/debuggers.yml | 61 ++++++++++++++++++++++
3 files changed, 86 insertions(+)
create mode 100644 .github/actions/setup-debuggers/README.md
create mode 100644 .github/actions/setup-debuggers/action.yml
create mode 100644 .github/workflows/debuggers.yml
diff --git a/.github/actions/setup-debuggers/README.md b/.github/actions/setup-debuggers/README.md
new file mode 100644
index 00000000..7c542ddb
--- /dev/null
+++ b/.github/actions/setup-debuggers/README.md
@@ -0,0 +1,13 @@
+# Setup environment for testing debugger extension on Linux
+
+Action setups the environment on Linux runners (install requirements, setup the
+workflow environment, etc.) for testing the python debugger extension for
+various debuggers.
+
+## How to use Github Action from Github workflow
+
+Add the following code to the running steps before LuaJIT configuration:
+```
+- uses: ./.github/actions/setup-debuggers
+ if: ${{ matrix.OS == 'Linux' }}
+```
diff --git a/.github/actions/setup-debuggers/action.yml b/.github/actions/setup-debuggers/action.yml
new file mode 100644
index 00000000..c34d4502
--- /dev/null
+++ b/.github/actions/setup-debuggers/action.yml
@@ -0,0 +1,12 @@
+name: Setup CI environment for testing the debugger extension
+description: The Linux machine setup for tests of the debugger extension
+runs:
+ using: composite
+ steps:
+ - name: Setup CI environment (Linux)
+ uses: ./.github/actions/setup-linux
+ - name: Install dependencies for the tests
+ run: |
+ apt -y update
+ apt install -y python3 gdb lldb
+ shell: bash
diff --git a/.github/workflows/debuggers.yml b/.github/workflows/debuggers.yml
new file mode 100644
index 00000000..489c2bed
--- /dev/null
+++ b/.github/workflows/debuggers.yml
@@ -0,0 +1,61 @@
+name: Debuggers
+
+on:
+ push:
+ branches-ignore:
+ - '**-notest'
+ - 'upstream-**'
+ tags-ignore:
+ - '**'
+
+concurrency:
+ # An update of a developer branch cancels the previously
+ # scheduled workflow run for this branch. However, the default
+ # branch, and long-term branch (tarantool/release/2.11,
+ # tarantool/release/2.10, etc) workflow runs are never canceled.
+ #
+ # We use a trick here: define the concurrency group as 'workflow
+ # run ID' + # 'workflow run attempt' because it is a unique
+ # combination for any run. So it effectively discards grouping.
+ #
+ # XXX: we cannot use `github.sha` as a unique identifier because
+ # pushing a tag may cancel a run that works on a branch push
+ # event.
+ group: ${{ startsWith(github.ref, 'refs/heads/tarantool/')
+ && format('{0}-{1}', github.run_id, github.run_attempt)
+ || format('{0}-{1}', github.workflow, github.ref) }}
+ cancel-in-progress: true
+
+jobs:
+ test-debuggers:
+ strategy:
+ fail-fast: false
+ matrix:
+ ARCH: [ARM64, x86_64]
+ GC64: [ON, OFF]
+ exclude:
+ - ARCH: ARM64
+ GC64: OFF
+ runs-on: [self-hosted, regular, Linux, '${{ matrix.ARCH }}']
+ name: >
+ LuaJIT
+ (${{ matrix.ARCH }})
+ GC64:${{ matrix.GC64 }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ - name: setup debuggers
+ uses: ./.github/actions/setup-debuggers
+ - name: configure
+ run: >
+ cmake -S . -B ${{ env.BUILDDIR }}
+ -DCMAKE_BUILD_TYPE=Debug
+ -DLUAJIT_ENABLE_GC64=${{ matrix.GC64 }}
+ - name: build
+ run: cmake --build . --parallel
+ working-directory: ${{ env.BUILDDIR }}
+ - name: test
+ run: cmake --build . --parallel --target tarantool-debugger-tests
+ working-directory: ${{ env.BUILDDIR }}
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-05-19 12:43 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-19 12:39 [Tarantool-patches] [PATCH v2 luajit 0/6] Unified extension for debuggers Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 1/6] test: introduce tests for debugging extensions Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 2/6] lldb: refactor extension Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 3/6] dbg: sort initialization of commands Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 4/6] lldb: support full-range 64-bit lightuserdata Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 5/6] dbg: generalize extension Sergey Kaplun via Tarantool-patches
2026-05-19 12:39 ` [Tarantool-patches] [PATCH v2 luajit 6/6] ci: introduce workflow to test debugger extension Sergey Kaplun 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