From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from [87.239.111.99] (localhost [127.0.0.1]) by dev.tarantool.org (Postfix) with ESMTP id 7FB2F6EFEE; Fri, 5 Jun 2026 18:02:32 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 7FB2F6EFEE DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1780671752; bh=EIM419kY9wGtBymr+W2OxiMrMqZ558/8eS45ZqOpSr8=; h=Date:To:Cc:References:In-Reply-To:Subject:List-Id: List-Unsubscribe:List-Archive:List-Post:List-Help:List-Subscribe: From:Reply-To:From; b=L4w23BKLzHkI2B0/sStGbiaP23HBqiXFDS1EXXPdGLN8W5BcdU9WVsJ2FTuFjvo+2 p9HPBlNSdcCrMIrypZsBH0srMgdqOqgq9YvEO2+h5PyXAwz1sd0BjHuf6DIOD8/u8O eovJNPJTYZp2FHG+IoNsejTDbJAKBC7d2nhWJr1c= Received: from send34.i.mail.ru (send34.i.mail.ru [89.221.237.129]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id 8C9AB6EFEE for ; Fri, 5 Jun 2026 18:02:30 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 8C9AB6EFEE Received: by exim-smtp-85dd497b6-xqbzn with esmtpa (envelope-from ) id 1wVW45-00000000OLR-1Uf4; Fri, 05 Jun 2026 18:02:29 +0300 Content-Type: multipart/alternative; boundary="------------blm00fabqpTZ2VvfpRKorM0i" Message-ID: <298f77c0-d555-4364-a6d1-c96ab322daf4@tarantool.org> Date: Fri, 5 Jun 2026 18:02:28 +0300 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Content-Language: en-US To: Sergey Kaplun , Evgeniy Temirgaleev Cc: tarantool-patches@dev.tarantool.org References: <20260604093052.2221827-1-skaplun@tarantool.org> <20260604093052.2221827-4-skaplun@tarantool.org> In-Reply-To: <20260604093052.2221827-4-skaplun@tarantool.org> X-Mailru-Src: smtp X-4EC0790: 10 X-7564579A: B8F34718100C35BD X-77F55803: 4F1203BC0FB41BD9FAD06046D747065B5E501D0605CEA84C9D2B5AB1DD9DE79A182A05F538085040D1F95C10E618BCC73DE06ABAFEAF670535FB764CFCA94C68CF5BE8966240E96A7959C119149068A2 X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE742D9BD90C58D50E0EA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F7900637AC83A81C8FD4AD23D82A6BABE6F325AC2E85FA5F3EDFCBAA7353EFBB5533756660A56BB7F6F20B606EEFB97E6B3AB4F2FE304FC8DE69A130BFE01DE9A4383612389733CBF5DBD5E913377AFFFEAFD269176DF2183F8FC7C0A29E2F051442AF778941B15DA834481FCF19DD082D7633A0EF3E4896CB9E6436389733CBF5DBD5E9D5E8D9A59859A8B6E5E764EB5D94DBD4CC7F00164DA146DA6F5DAA56C3B73B237318B6A418E8EAB86D1867E19FE14079C09775C1D3CA48CF3D321E7403792E342EB15956EA79C166A417C69337E82CC275ECD9A6C639B01B78DA827A17800CE78DD9044B304389D4731C566533BA786AA5CC5B56E945C8DA X-C1DE0DAB: 0D63561A33F958A58B69269E70E3C6595002B1117B3ED696415372C055FA16C3E41E333F9D1358D5823CB91A9FED034534781492E4B8EEADC3194D76C41E9723BDAD6C7F3747799A X-C8649E89: 1C3962B70DF3F0AD73CAD6646DEDE191716CD42B3DD1D34CAB70F9BE574AE9C625B6776AC983F447FC0B9F89525902EE6F57B2FD27647F25E66C117BDB76D6596C0E8512AFE55819944803DB782D21051C1BB88EC7AA0DF475408297EBE571E031459D47C722DFF6B8341EE9D5BE9A0AEB09201D0DA0808CD114A2D811C57EEDA68B11273BB5893C6536EB022892E5344C41F94D744909CE2512F26BEC029E55448553D2254B8D95CD72808BE417F3B9E0E7457915DAA85F X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu53w8ahmwBjZKM/YPHZyZHvz5uv+WouB9+ObcCpyrx6l7KImUglyhkEat/+ysWwi0gdhEs0JGjl6ggRWTy1haxBpVdbIX1nthFXMZebaIdHP2ghjoIc/363UZI6Kf1ptIMVRI2994ruhLUh4pT6GXa+fA= X-DA7885C5: 05A01A2BF73B2E3EF255D290C0D534F9143603A9FAED076D09E3BF1CF2036A47AE0FA6BEF09449995B1A4C17EAA7BC4BEF2421ABFA55128DAF83EF9164C44C7E X-Mailru-Sender: 689FA8AB762F7393520AF17B8A65FDE28FB2CFEFA3D3A48C0130128ABB10CBA81F82672367909BFBEF86D5F70DA33880E41E8EF7A07863ECB274557F927329BE2DDF8182D28ACDB545BD1C3CC395C826B4A721A3011E896F X-Mras: Ok Subject: Re: [Tarantool-patches] [PATCH luajit 3/4] dbg: introduce lj-gco command X-BeenThere: tarantool-patches@dev.tarantool.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Sergey Bronnikov via Tarantool-patches Reply-To: Sergey Bronnikov Errors-To: tarantool-patches-bounces@dev.tarantool.org Sender: "Tarantool-patches" This is a multi-part message in MIME format. --------------blm00fabqpTZ2VvfpRKorM0i Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit Hi, Sergey, thanks for the patch! LGTM On 6/4/26 12:30, Sergey Kaplun wrote: > Our GDB extension already has dumpers for TValues. But sometimes it > may be useful to dump GC objects (GCobj) without stack context. This > patch adds additional wrappers around dumpers for GC objects to get the > corresponding GC object from a TValue. Also, the lj-gco command is > introduced. It allows dumping GC objects without stack context. The > output format is the same as for the lj-tv command. > > Part of tarantool/tarantool#4808 > --- > src/luajit_dbg.py | 182 ++++++++++++------ > .../debug-extension-tests.py | 83 ++++++-- > 2 files changed, 190 insertions(+), 75 deletions(-) > > diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py > index 300d65e9..f5868e61 100644 > --- a/src/luajit_dbg.py > +++ b/src/luajit_dbg.py > @@ -882,44 +882,29 @@ def lightudV(tv): > > # Dumpers. > > +# GCobj 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): > +def dump_lj_gco_str(gcobj): > return 'string {body} @ {address}'.format( > - body=strdata(gcval(tv['gcr'])), > - address=strx64(gcval(tv['gcr'])) > + body=strdata(gcobj), > + address=strx64(gcobj) > ) > > > -def dump_lj_tupval(tv): > - return 'upvalue @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_upval(gcobj): > + return 'upvalue @ {}'.format(strx64(gcobj)) > > > -def dump_lj_tthread(tv): > - return 'thread @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_thread(gcobj): > + return 'thread @ {}'.format(strx64(gcobj)) > > > -def dump_lj_tproto(tv): > - return 'proto @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_proto(gcobj): > + return 'proto @ {}'.format(strx64(gcobj)) > > > -def dump_lj_tfunc(tv): > - func = dbg.cast('struct GCfuncC *', gcval(tv['gcr'])) > +def dump_lj_gco_func(gcobj): > + func = dbg.cast('struct GCfuncC *', gcobj) > ffid = func['ffid'] > > if ffid == 0: > @@ -936,20 +921,20 @@ def dump_lj_tfunc(tv): > return 'fast function #{}'.format(int(ffid)) > > > -def dump_lj_ttrace(tv): > - trace = dbg.cast('struct GCtrace *', gcval(tv['gcr'])) > +def dump_lj_gco_trace(gcobj): > + trace = dbg.cast('struct GCtrace *', gcobj) > 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_gco_cdata(gcobj): > + return 'cdata @ {}'.format(strx64(gcobj)) > > > -def dump_lj_ttab(tv): > - table = dbg.cast('GCtab *', gcval(tv['gcr'])) > +def dump_lj_gco_tab(gcobj): > + table = dbg.cast('GCtab *', gcobj) > return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format( > gcr=strx64(table), > asize=table['asize'], > @@ -957,41 +942,94 @@ def dump_lj_ttab(tv): > ) > > > -def dump_lj_tudata(tv): > - return 'userdata @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_udata(gcobj): > + return 'userdata @ {}'.format(strx64(gcobj)) > + > + > +def dump_lj_gco_invalid(gcobj): > + return 'not valid type @ {}'.format(strx64(gcobj)) > + > + > +# TValue dumpers > + > +def dump_lj_tv_nil(tv): > + return 'nil' > + > + > +def dump_lj_tv_false(tv): > + return 'false' > + > + > +def dump_lj_tv_true(tv): > + return 'true' > + > + > +def dump_lj_tv_lightud(tv): > + return 'light userdata @ {}'.format(strx64(lightudV(tv))) > + > + > +# Generate wrappers for TValues containing GCobj. > +gco_fn_dumpers = [ > + fn for fn in globals().keys() if fn.startswith('dump_lj_gco') > +] > +for fn_name in gco_fn_dumpers: > + wrapped_fn_name = fn_name.replace('gco', 'tv') > + # Lambda takes `fn_name` as a reference, so the additional > + # lambda is needed to fixate the correct wrapper. > + globals()[wrapped_fn_name] = (lambda f: ( > + lambda tv: globals()[f](gcval(tv['gcr'])) > + ))(fn_name) > > > -def dump_lj_tnumx(tv): > +def dump_lj_tv_numx(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, > +gco_dumpers = { > + 'LJ_TSTR': dump_lj_gco_str, > + 'LJ_TUPVAL': dump_lj_gco_upval, > + 'LJ_TTHREAD': dump_lj_gco_thread, > + 'LJ_TPROTO': dump_lj_gco_proto, > + 'LJ_TFUNC': dump_lj_gco_func, > + 'LJ_TTRACE': dump_lj_gco_trace, > + 'LJ_TCDATA': dump_lj_gco_cdata, > + 'LJ_TTAB': dump_lj_gco_tab, > + 'LJ_TUDATA': dump_lj_gco_udata, > } > > > +tv_dumpers = { > + 'LJ_TNIL': dump_lj_tv_nil, > + 'LJ_TFALSE': dump_lj_tv_false, > + 'LJ_TTRUE': dump_lj_tv_true, > + 'LJ_TLIGHTUD': dump_lj_tv_lightud, > + 'LJ_TSTR': dump_lj_tv_str, # noqa: F821 # Generated. > + 'LJ_TUPVAL': dump_lj_tv_upval, # noqa: F821 # Generated. > + 'LJ_TTHREAD': dump_lj_tv_thread, # noqa: F821 # Generated. > + 'LJ_TPROTO': dump_lj_tv_proto, # noqa: F821 # Generated. > + 'LJ_TFUNC': dump_lj_tv_func, # noqa: F821 # Generated. > + 'LJ_TTRACE': dump_lj_tv_trace, # noqa: F821 # Generated. > + 'LJ_TCDATA': dump_lj_tv_cdata, # noqa: F821 # Generated. > + 'LJ_TTAB': dump_lj_tv_tab, # noqa: F821 # Generated. > + 'LJ_TUDATA': dump_lj_tv_udata, # noqa: F821 # Generated. > + 'LJ_TNUMX': dump_lj_tv_numx, > +} > + > + > +def dump_gcobj(gcobj): > + return gco_dumpers.get( > + typenames(i2notu32(gcobj['gch']['gct'])), dump_lj_gco_invalid > + )(gcobj) > + > + > def dump_tvalue(tvalue): > - return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue) > + return tv_dumpers.get( > + typenames(itypemap(tvalue)), > + dump_lj_tv_invalid # noqa: F821 # Generated. > + )(tvalue) > > > def dump_framelink_slot_address(fr): > @@ -1011,7 +1049,7 @@ def dump_framelink(L, 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), > + f=dump_lj_tv_func(fr - LJ_FR2), # noqa: F821 # Generated. > ) > > > @@ -1141,6 +1179,35 @@ The command requires no args and dumps current GC stats: > )) > > > +class LJDumpGCobj(dbg.LJBase): > + ''' > +lj-gco > + > +The command receives a pointer to (GCobj address) and dumps > +the type and some info related to it. > + > +* LJ_TSTR: string @ > +* LJ_TUPVAL: upvalue @ > +* LJ_TTHREAD: thread @ > +* LJ_TPROTO: proto @ > +* LJ_TFUNC: > + : Lua function @ , upvalues, > + : C function > + : fast function # > +* LJ_TTRACE: trace @ > +* LJ_TCDATA: cdata @ > +* LJ_TTAB: table @ (asize: , hmask: ) > +* LJ_TUDATA: userdata @ > + > +Whether the type of the given address differs from the listed above, then > +error message occurs. > + ''' > + > + def execute(self, arg): > + gcobj = dbg.cast('GCobj *', dbg.eval(arg)) > + dbg.write('{}\n'.format(dump_gcobj(gcobj))) > + > + > class LJDumpStack(dbg.LJBase): > ''' > lj-stack [] > @@ -1302,6 +1369,7 @@ def load(event=None): > dbg.initialize_extension({ > 'lj-arch': LJDumpArch, > 'lj-gc': LJGC, > + 'lj-gco': LJDumpGCobj, > 'lj-stack': LJDumpStack, > 'lj-state': LJState, > 'lj-str': LJDumpString, > diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py > index 06a118ff..7e2b5ac4 100644 > --- a/test/tarantool-debugger-tests/debug-extension-tests.py > +++ b/test/tarantool-debugger-tests/debug-extension-tests.py > @@ -138,6 +138,17 @@ class TestCaseBase(unittest.TestCase): > self.assertRegex(self.output, self.pattern.strip()) > > > +# LLDB + Clang on macOS can't produce debug info for the C-defined > +# macros. Thus, we hardcoded its value manually. > +def gcval(arg): > + if sys.platform == 'darwin': > + # Assume GC64 build only. > + LJ_GCVMASK = '(((uint64_t)1 << 47) - 1)' > + return '(((' + arg + ')->gcr).gcptr64 & ' + LJ_GCVMASK + ')' > + else: > + return 'gcval(' + arg + ')' > + > + > class TestLoad(TestCaseBase): > extension_cmds = '' > location = 'lj_cf_print' > @@ -145,6 +156,7 @@ class TestLoad(TestCaseBase): > pattern = ( > r'lj-arch command initialized\n' > r'lj-gc command initialized\n' > + r'lj-gco command initialized\n' > r'lj-stack command initialized\n' > r'lj-state command initialized\n' > r'lj-str command initialized\n' > @@ -223,6 +235,31 @@ class TestLJStackFunc(TestCaseBase): > pattern = STACK_RX > > > +# Sorted in LJT order. > +GCO_ARGS = ( > + '"hello",\n' > + 'coroutine.create(function() end),\n' > + 'function() end,\n' > + 'require,\n' > + 'print,\n' > + 'ffi.new("int*"),\n' > + '{1},\n' > + 'newproxy(),\n' > +) > + > + > +GCO_RX = ( > + r'string \"hello\" @ ' + RX_ADDR + r'\n' > + r'thread @ ' + RX_ADDR + r'\n' > + r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[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' > +) > + > + > class TestLJTV(TestCaseBase): > location = 'lj_cf_print' > extension_cmds = ( > @@ -249,15 +286,8 @@ class TestLJTV(TestCaseBase): > ' nil,\n' > ' false,\n' > ' true,\n' > - ' debug.upvalueid(print, 1), \n' # lightuserdata > - ' "hello",\n' > - ' coroutine.create(function() end),\n' > - ' function() end,\n' > - ' require,\n' > - ' print,\n' > - ' ffi.new("int*"),\n' > - ' {1},\n' > - ' newproxy(),\n' > + ' debug.upvalueid(print, 1), \n' + # lightuserdata > + GCO_ARGS + > ' 1,\n' > ' 1.1\n' > ')\n' > @@ -267,15 +297,8 @@ 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'thread @ ' + RX_ADDR + r'\n' > - r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[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'light userdata @ ' + RX_ADDR + r'\n' + > + GCO_RX + > RX_INT + r' .*1.*\n' > r'number 1.1\d+\n' > ) > @@ -312,6 +335,30 @@ class TestLJTab(TestCaseBase): > ) > > > +class TestLJGCo(TestCaseBase): > + location = 'lj_cf_print' > + extension_cmds = ( > + 'lj-gco ' + gcval('L->base + 0') + '\n' > + 'lj-gco ' + gcval('L->base + 1') + '\n' > + 'lj-gco ' + gcval('L->base + 2') + '\n' > + 'lj-gco ' + gcval('L->base + 3') + '\n' > + 'lj-gco ' + gcval('L->base + 4') + '\n' > + 'lj-gco ' + gcval('L->base + 5') + '\n' > + 'lj-gco ' + gcval('L->base + 6') + '\n' > + 'lj-gco ' + gcval('L->base + 7') + '\n' > + ) > + > + lua_script = ( > + 'local ffi = require("ffi")\n' > + 'print(\n' + > + GCO_ARGS + > + ' 1\n' # Stub for the pattern. > + ')\n' > + ) > + > + pattern = GCO_RX > + > + > for test_cls in TestCaseBase.__subclasses__(): > test_cls.test = lambda self: self.check() > --------------blm00fabqpTZ2VvfpRKorM0i Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit

Hi, Sergey,

thanks for the patch! LGTM

On 6/4/26 12:30, Sergey Kaplun wrote:
Our GDB extension already has dumpers for TValues. But sometimes it
may be useful to dump GC objects (GCobj) without stack context. This
patch adds additional wrappers around dumpers for GC objects to get the
corresponding GC object from a TValue. Also, the lj-gco command is
introduced. It allows dumping GC objects without stack context. The
output format is the same as for the lj-tv command.

Part of tarantool/tarantool#4808
---
 src/luajit_dbg.py                             | 182 ++++++++++++------
 .../debug-extension-tests.py                  |  83 ++++++--
 2 files changed, 190 insertions(+), 75 deletions(-)

diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
index 300d65e9..f5868e61 100644
--- a/src/luajit_dbg.py
+++ b/src/luajit_dbg.py
@@ -882,44 +882,29 @@ def lightudV(tv):
 
 # Dumpers.
 
+# GCobj 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):
+def dump_lj_gco_str(gcobj):
     return 'string {body} @ {address}'.format(
-        body=strdata(gcval(tv['gcr'])),
-        address=strx64(gcval(tv['gcr']))
+        body=strdata(gcobj),
+        address=strx64(gcobj)
     )
 
 
-def dump_lj_tupval(tv):
-    return 'upvalue @ {}'.format(strx64(gcval(tv['gcr'])))
+def dump_lj_gco_upval(gcobj):
+    return 'upvalue @ {}'.format(strx64(gcobj))
 
 
-def dump_lj_tthread(tv):
-    return 'thread @ {}'.format(strx64(gcval(tv['gcr'])))
+def dump_lj_gco_thread(gcobj):
+    return 'thread @ {}'.format(strx64(gcobj))
 
 
-def dump_lj_tproto(tv):
-    return 'proto @ {}'.format(strx64(gcval(tv['gcr'])))
+def dump_lj_gco_proto(gcobj):
+    return 'proto @ {}'.format(strx64(gcobj))
 
 
-def dump_lj_tfunc(tv):
-    func = dbg.cast('struct GCfuncC *', gcval(tv['gcr']))
+def dump_lj_gco_func(gcobj):
+    func = dbg.cast('struct GCfuncC *', gcobj)
     ffid = func['ffid']
 
     if ffid == 0:
@@ -936,20 +921,20 @@ def dump_lj_tfunc(tv):
         return 'fast function #{}'.format(int(ffid))
 
 
-def dump_lj_ttrace(tv):
-    trace = dbg.cast('struct GCtrace *', gcval(tv['gcr']))
+def dump_lj_gco_trace(gcobj):
+    trace = dbg.cast('struct GCtrace *', gcobj)
     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_gco_cdata(gcobj):
+    return 'cdata @ {}'.format(strx64(gcobj))
 
 
-def dump_lj_ttab(tv):
-    table = dbg.cast('GCtab *', gcval(tv['gcr']))
+def dump_lj_gco_tab(gcobj):
+    table = dbg.cast('GCtab *', gcobj)
     return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format(
         gcr=strx64(table),
         asize=table['asize'],
@@ -957,41 +942,94 @@ def dump_lj_ttab(tv):
     )
 
 
-def dump_lj_tudata(tv):
-    return 'userdata @ {}'.format(strx64(gcval(tv['gcr'])))
+def dump_lj_gco_udata(gcobj):
+    return 'userdata @ {}'.format(strx64(gcobj))
+
+
+def dump_lj_gco_invalid(gcobj):
+    return 'not valid type @ {}'.format(strx64(gcobj))
+
+
+# TValue dumpers
+
+def dump_lj_tv_nil(tv):
+    return 'nil'
+
+
+def dump_lj_tv_false(tv):
+    return 'false'
+
+
+def dump_lj_tv_true(tv):
+    return 'true'
+
+
+def dump_lj_tv_lightud(tv):
+    return 'light userdata @ {}'.format(strx64(lightudV(tv)))
+
+
+# Generate wrappers for TValues containing GCobj.
+gco_fn_dumpers = [
+    fn for fn in globals().keys() if fn.startswith('dump_lj_gco')
+]
+for fn_name in gco_fn_dumpers:
+    wrapped_fn_name = fn_name.replace('gco', 'tv')
+    # Lambda takes `fn_name` as a reference, so the additional
+    # lambda is needed to fixate the correct wrapper.
+    globals()[wrapped_fn_name] = (lambda f: (
+        lambda tv: globals()[f](gcval(tv['gcr']))
+    ))(fn_name)
 
 
-def dump_lj_tnumx(tv):
+def dump_lj_tv_numx(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,
+gco_dumpers = {
+    'LJ_TSTR':     dump_lj_gco_str,
+    'LJ_TUPVAL':   dump_lj_gco_upval,
+    'LJ_TTHREAD':  dump_lj_gco_thread,
+    'LJ_TPROTO':   dump_lj_gco_proto,
+    'LJ_TFUNC':    dump_lj_gco_func,
+    'LJ_TTRACE':   dump_lj_gco_trace,
+    'LJ_TCDATA':   dump_lj_gco_cdata,
+    'LJ_TTAB':     dump_lj_gco_tab,
+    'LJ_TUDATA':   dump_lj_gco_udata,
 }
 
 
+tv_dumpers = {
+    'LJ_TNIL':     dump_lj_tv_nil,
+    'LJ_TFALSE':   dump_lj_tv_false,
+    'LJ_TTRUE':    dump_lj_tv_true,
+    'LJ_TLIGHTUD': dump_lj_tv_lightud,
+    'LJ_TSTR':     dump_lj_tv_str,  # noqa: F821 # Generated.
+    'LJ_TUPVAL':   dump_lj_tv_upval,  # noqa: F821 # Generated.
+    'LJ_TTHREAD':  dump_lj_tv_thread,  # noqa: F821 # Generated.
+    'LJ_TPROTO':   dump_lj_tv_proto,  # noqa: F821 # Generated.
+    'LJ_TFUNC':    dump_lj_tv_func,  # noqa: F821 # Generated.
+    'LJ_TTRACE':   dump_lj_tv_trace,  # noqa: F821 # Generated.
+    'LJ_TCDATA':   dump_lj_tv_cdata,  # noqa: F821 # Generated.
+    'LJ_TTAB':     dump_lj_tv_tab,  # noqa: F821 # Generated.
+    'LJ_TUDATA':   dump_lj_tv_udata,  # noqa: F821 # Generated.
+    'LJ_TNUMX':    dump_lj_tv_numx,
+}
+
+
+def dump_gcobj(gcobj):
+    return gco_dumpers.get(
+        typenames(i2notu32(gcobj['gch']['gct'])), dump_lj_gco_invalid
+    )(gcobj)
+
+
 def dump_tvalue(tvalue):
-    return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue)
+    return tv_dumpers.get(
+        typenames(itypemap(tvalue)),
+        dump_lj_tv_invalid  # noqa: F821 # Generated.
+    )(tvalue)
 
 
 def dump_framelink_slot_address(fr):
@@ -1011,7 +1049,7 @@ def dump_framelink(L, 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),
+        f=dump_lj_tv_func(fr - LJ_FR2),  # noqa: F821 # Generated.
     )
 
 
@@ -1141,6 +1179,35 @@ The command requires no args and dumps current GC stats:
         ))
 
 
+class LJDumpGCobj(dbg.LJBase):
+    '''
+lj-gco <GCobj *>
+
+The command receives a pointer to <GCobj> (GCobj address) and dumps
+the type and some info related to it.
+
+* 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>
+
+Whether the type of the given address differs from the listed above, then
+error message occurs.
+    '''
+
+    def execute(self, arg):
+        gcobj = dbg.cast('GCobj *', dbg.eval(arg))
+        dbg.write('{}\n'.format(dump_gcobj(gcobj)))
+
+
 class LJDumpStack(dbg.LJBase):
     '''
 lj-stack [<lua_State *>]
@@ -1302,6 +1369,7 @@ def load(event=None):
     dbg.initialize_extension({
         'lj-arch':  LJDumpArch,
         'lj-gc':    LJGC,
+        'lj-gco':   LJDumpGCobj,
         'lj-stack': LJDumpStack,
         'lj-state': LJState,
         'lj-str':   LJDumpString,
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
index 06a118ff..7e2b5ac4 100644
--- a/test/tarantool-debugger-tests/debug-extension-tests.py
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -138,6 +138,17 @@ class TestCaseBase(unittest.TestCase):
             self.assertRegex(self.output, self.pattern.strip())
 
 
+# LLDB + Clang on macOS can't produce debug info for the C-defined
+# macros. Thus, we hardcoded its value manually.
+def gcval(arg):
+    if sys.platform == 'darwin':
+        # Assume GC64 build only.
+        LJ_GCVMASK = '(((uint64_t)1 << 47) - 1)'
+        return '(((' + arg + ')->gcr).gcptr64 & ' + LJ_GCVMASK + ')'
+    else:
+        return 'gcval(' + arg + ')'
+
+
 class TestLoad(TestCaseBase):
     extension_cmds = ''
     location = 'lj_cf_print'
@@ -145,6 +156,7 @@ class TestLoad(TestCaseBase):
     pattern = (
         r'lj-arch command initialized\n'
         r'lj-gc command initialized\n'
+        r'lj-gco command initialized\n'
         r'lj-stack command initialized\n'
         r'lj-state command initialized\n'
         r'lj-str command initialized\n'
@@ -223,6 +235,31 @@ class TestLJStackFunc(TestCaseBase):
     pattern = STACK_RX
 
 
+# Sorted in LJT order.
+GCO_ARGS = (
+    '"hello",\n'
+    'coroutine.create(function() end),\n'
+    'function() end,\n'
+    'require,\n'
+    'print,\n'
+    'ffi.new("int*"),\n'
+    '{1},\n'
+    'newproxy(),\n'
+)
+
+
+GCO_RX = (
+    r'string \"hello\" @ ' + RX_ADDR + r'\n'
+    r'thread @ ' + RX_ADDR + r'\n'
+    r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[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'
+)
+
+
 class TestLJTV(TestCaseBase):
     location = 'lj_cf_print'
     extension_cmds = (
@@ -249,15 +286,8 @@ class TestLJTV(TestCaseBase):
         '  nil,\n'
         '  false,\n'
         '  true,\n'
-        '  debug.upvalueid(print, 1), \n'  # lightuserdata
-        '  "hello",\n'
-        '  coroutine.create(function() end),\n'
-        '  function() end,\n'
-        '  require,\n'
-        '  print,\n'
-        '  ffi.new("int*"),\n'
-        '  {1},\n'
-        '  newproxy(),\n'
+        '  debug.upvalueid(print, 1), \n' +  # lightuserdata
+        GCO_ARGS +
         '  1,\n'
         '  1.1\n'
         ')\n'
@@ -267,15 +297,8 @@ 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'thread @ ' + RX_ADDR + r'\n'
-        r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[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'light userdata @ ' + RX_ADDR + r'\n' +
+        GCO_RX +
         RX_INT + r' .*1.*\n'
         r'number 1.1\d+\n'
     )
@@ -312,6 +335,30 @@ class TestLJTab(TestCaseBase):
     )
 
 
+class TestLJGCo(TestCaseBase):
+    location = 'lj_cf_print'
+    extension_cmds = (
+        'lj-gco ' + gcval('L->base + 0') + '\n'
+        'lj-gco ' + gcval('L->base + 1') + '\n'
+        'lj-gco ' + gcval('L->base + 2') + '\n'
+        'lj-gco ' + gcval('L->base + 3') + '\n'
+        'lj-gco ' + gcval('L->base + 4') + '\n'
+        'lj-gco ' + gcval('L->base + 5') + '\n'
+        'lj-gco ' + gcval('L->base + 6') + '\n'
+        'lj-gco ' + gcval('L->base + 7') + '\n'
+    )
+
+    lua_script = (
+        'local ffi = require("ffi")\n'
+        'print(\n' +
+        GCO_ARGS +
+        '  1\n'  # Stub for the pattern.
+        ')\n'
+    )
+
+    pattern = GCO_RX
+
+
 for test_cls in TestCaseBase.__subclasses__():
     test_cls.test = lambda self: self.check()
 
--------------blm00fabqpTZ2VvfpRKorM0i--