Embed GDB helper

* Add GDB helper script

Pretty printing of some common argument types

Need to add a line to ~/.gdbinit

> add-auto-load-safe-path /path/to/base/checkout/

afterwards verify at GDB shell with

> info auto-load python-scripts
> info pretty-printer

Tested with GDB 16.3 circa Debian 13

Add $rcast(dbCommon) to up-cast
Add $record("name") to lookup record
Add $elln(ELLLIST,#) to index into list
This commit is contained in:
mdavidsaver authored and Dirk Zimoch committed 2026-10-14 11:14:32 +02:00
1 parent 354fc5b757
commit a6c4933742
7 files changed
+348

No files matched your search

+3
View File
@@ -11,3 +11,6 @@
# dbLexRoutines.c is included in dbYacc.c
dbYacc.c: dbLex.c $(IOCDIR)/dbStatic/dbLexRoutines.c
dbStaticLib$(DEP): ../epics-gdb-helper.py
dbStaticLib$(OBJ): ../epics-gdb-helper.py
@@ -3664,3 +3664,24 @@ void dbReportDeviceConfig(dbBase *pdbbase, FILE *report)
finishOutstream(stream);
return;
}
#if defined(__GNUC__) && defined(__ELF__) && defined(__linux__)
/* Loading of GDB python helper script.
*
* Need to include the following line in ~/.gdbinit
*
* add-auto-load-safe-path /path/to/base
*
* after libCom.so is loaded (eg. break on main() ) run in the GDB shell:
*
* info auto-load python-scripts
*/
__asm__(
".pushsection \".debug_gdb_scripts\", \"MS\",@progbits,1\n"
".byte 4 /* Python text */\n"
".ascii \"epics-gdb-helper.py\\n\"\n"
".incbin \"epics-gdb-helper.py\"\n"
".byte 0\n"
".popsection \n"
);
#endif
@@ -0,0 +1,201 @@
#*************************************************************************
# SPDX-License-Identifier: EPICS
# EPICS BASE is distributed subject to a Software License Agreement found
# in file LICENSE that is included with this distribution.
#*************************************************************************
# see https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python.html
# also documentation/gdb.md in epics-base
#
# Troubleshooting:
# (gdb) set python print-stack full
import time
import gdb
import gdb.printing
voidp = gdb.lookup_type('void').pointer()
def ellIter(plist : gdb.Value, cast :gdb.Type =None): # -> ELLNODE*
# Iterate an ELLLIST*
if plist.type.code == gdb.TYPE_CODE_PTR:
plist = plist.dereference()
pnode = plist['node']['next']
while pnode:
yield pnode if cast is None else pnode.cast(cast)
pnode = pnode.dereference()['next']
class Wrapper(object):
def __init__(self, val: gdb.Value):
self._v = val
def to_string(self) -> str:
raise NotImplementedError()
class epicsTimeStamp(Wrapper):
def to_string(self):
sec = int(self._v['secPastEpoch'])
ns = int(self._v['nsec'])
T = sec + 631152000 + 1e-9*ns
return f'epicsTimeStamp(secPastEpoch = {sec}, ns = {ns}, {time.ctime(T)!r})'
class dbCommon(Wrapper):
def __init__(self, val: gdb.Value, name='dbCommon'):
self._v, self._name = val, name
def to_string(self):
try:
return '(%s*)(%s, "%s")'%(self._name, self._v.address.cast(voidp), self._v['name'].string())
except Exception:
raise ValueError(self._v.type)
class dbAddr(Wrapper):
def to_string(self):
v = self._v # Value
rec = v['precord'].dereference()['name'].string()
fld = v['pfldDes'].dereference()['name'].string()
return '(dbAddr*)(%s, "%s.%s")'%(self._v.address.cast(voidp), rec, fld)
class dbChannel(Wrapper):
def to_string(self):
v = self._v # Value
rec = v['addr']['precord'].dereference()['name'].string()
fld = v['addr']['pfldDes'].dereference()['name'].string()
return '(dbChannel*)(%s, "%s.%s")'%(self._v.address.cast(voidp), rec, fld)
class dbLink(Wrapper):
def to_string(self):
v = self._v # Value
# size_t offset = (char*)&v - (char*)v.precord
offset = int(v.address) - int(v['precord'])
# dbRecordType& rdes = *v.precord->rdes
rdes = v['precord'].dereference()['rdes'].dereference()
no_links = int(rdes['no_links'])
link_ind = rdes['link_ind'] # array of indicies of link fields
papFldDes = rdes['papFldDes'] # main array of field descriptions
for idx in range(no_links):
idx = link_ind[idx]
fdes = papFldDes[idx].dereference() # dbFldDes
if int(fdes['offset']) == offset:
fld = fdes['name'].string()
break
else:
fld = '???'
rec = v['precord'].dereference()['name'].string()
ltype = int(v['type'])
if ltype in (10, 11):
target = ', target="%s"'%v['value']['pv_link']['pvname'].string()
elif ltype==12:
target = ', target="@%s"'%v['value']['instio']['string'].string()
else:
ltype = {
0: 'CONSTANT',
1: 'PV_LINK',
10: 'DB_LINK',
11: 'CA_LINK',
12: 'INST_IO',
}.get(ltype, 'LINK(%s)'%ltype)
target = ', type=%s'%ltype
return '(link*)(%s, src="%s.%s"%s)'%(self._v.cast(voidp), rec, fld, target)
class EPICSPrinter(gdb.printing.PrettyPrinter):
_pointers = {
'epicsTimeStamp': epicsTimeStamp,
'dbCommon': dbCommon,
'dbChannel': dbChannel,
'dbAddr': dbAddr,
'DBADDR': dbAddr,
'link': dbLink,
}
_values = {
'epicsTimeStamp': epicsTimeStamp,
'link': dbLink,
}
def __init__(self):
gdb.printing.PrettyPrinter.__init__(self, 'EPICS')
@classmethod
def __call__(klass, val: gdb.Value) -> Wrapper:
isptr = val.type.code == gdb.TYPE_CODE_PTR
if isptr:
if val.type==voidp or not val: # NULL
return
val = val.dereference()
tbl = klass._pointers
else:
tbl = klass._values
tname = val.type.unqualified().strip_typedefs().name # str
printer = tbl.get(tname)
if isptr and printer is None and tname is not None and tname.endswith('Record'):
# exclude eg. lockRecord and processNotifyRecord which are not record types
# which are all too small to be records
if val.type.sizeof >= gdb.lookup_type('dbCommon').sizeof:
return dbCommon(val, str(tname))
if printer is not None:
return printer(val)
gdb.printing.register_pretty_printer(None, EPICSPrinter(), replace=True)
class RCast(gdb.Function):
#Cast dbCommon* to specific record type
def __init__(self):
super(RCast, self).__init__("rcast")
@staticmethod
def invoke(prec: gdb.Value) -> gdb.Value:
if prec.type.code!=gdb.TYPE_CODE_PTR:
return prec
ptype = prec.type.target().unqualified().strip_typedefs()
if ptype.name=='dbCommon':
rname = prec.dereference()['rdes'].dereference()['name'].string() # eg. 'calc'
rtype = gdb.lookup_type(f'{rname}Record')
return prec.cast(rtype.pointer())
return prec
RCast()
class Rec(gdb.Function):
#Lookup record by name
def __init__(self):
super(Rec, self).__init__("record")
@staticmethod
def invoke(prec: gdb.Value) -> gdb.Value:
target = prec.string()
pdbbase = gdb.lookup_global_symbol('pdbbase').value() # dbBase*
if not pdbbase:
raise RuntimeError('Process database not yet initialized. Load some records first.')
pdbbase = pdbbase.dereference()
dbRecordType = gdb.lookup_type('dbRecordType').pointer()
dbRecordNode = gdb.lookup_type('dbRecordNode').pointer()
dbCommon = gdb.lookup_type('dbCommon').pointer()
for rtype in ellIter(pdbbase['recordTypeList'].address, dbRecordType):
for rnode in ellIter(rtype.dereference()['recList'].address, dbRecordNode):
precname = rnode.dereference()['recordname'].string()
if precname==target:
return RCast.invoke(rnode.dereference()['precord'].cast(dbCommon))
raise RuntimeError(f'No record {target!r}')
Rec()
class EllNth(gdb.Function):
# Fetch entry in list
def __init__(self):
super(EllNth, self).__init__("elln")
@staticmethod
def invoke(plist: gdb.Value, idx: gdb.Value) -> gdb.Value:
idx = int(idx)
if idx < 0:
raise ValueError('Negative index')
it = ellIter(plist)
for _n in range(idx):
next(it) # discard
return next(it)
EllNth()