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 GitHub committed 2026-06-21 07:18:08 -07:00
1 parent 58152f6a42
commit 4b03e97d7b
7 files changed
+348

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ DOCS += README.md
DOCS += ca-cli.md
DOCS += RELEASE_NOTES.md
DOCS += ReleaseChecklist.md
DOCS += gdb.md
OLD_NOTES = $(wildcard ../RELEASE-*.md)
DOCS += $(OLD_NOTES:../%=%)
+114
View File
@@ -0,0 +1,114 @@
# Debugging with GDB
Starts a process in [GDB](https://sourceware.org/gdb/current/onlinedocs/gdb.html/):
```sh
gdb --args ../../bin/linux-x86_64-debug/myioc ./st.cmd
```
Attach to a running process by numeric PID:
```sh
gdb --pid=##
```
Shorthand if only one instance is running:
```sh
gdb --pid=`pgrep myioc`
```
## GDB shell Cheat sheet
Print stack traces of all threads. Optionally with pagination disabled:
```gdb
(gdb) set pagination off
(gdb) thread apply all backtrace
(gdb) set pagination on
```
Note: consider sending or posting this very long output as an attachment file.
Break when a certain record begins processing:
```gdb
(gdb) break dbProcess if $_streq(precord.name, "my:record")
```
Break when a `PACT` field changes:
```gdb
(gdb) watch -l $record("my:record").pact
```
Note: Uses custom `$record()` helper. See below.
## Helpers
If the bundled `epics-gdb-helper.py` is [enabled](#helper-enable),
then a set of pretty printers are registered for some common database types.
eg. `dbCommon*` will show the record name.
`DBLINK*` will show the associated record name, and the link target.
Also, several convenience are available.
Use `$record()` to lookup a record by name:
```gdb
(gdb) print $record("my:record")
$1 = (calcRecord*)(0x55555562f6f8, "my:record")
```
Note: `$record()` may only be called after record allocation during `iocInit()`.
Use `$rcast()` to up-cast `dbCommon*` to the specific record type:
```gdb
(gdb) print precord
$1 = (dbCommon*)(0x55555562f6f8, "my:record")
(gdb) print $rcast(precord)
$2 = (calcRecord*)(0x55555562f6f8, "my:record")
```
Use `$elln()` to index into `ELLLIST` linked lists.
```gdb
(gdb) print $elln(pdbbase.recordTypeList, 2)
```
### Enable (helper-enable)
To automatically [enable](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Auto_002dloading-safe-path.html),
add a line to `~/.gdbinit`.
```
add-auto-load-safe-path /path/to/base/checkout/
```
Or each time run:
```
(gdb) source /path/to/base/checkout/modules/database/src/ioc/epics-gdb-helper.py
```
Note: Entries in the `safe-path` list may be files (`.so` or `.py`), or directories.
When a directory is included, sub-directories are also trusted.
eg. `add-auto-load-safe-path /opt/epics` would also trust a `/opt/epics/lib`.
Afterwards verify at GDB shell by running:
```gdb
(gdb) info auto-load python-scripts
Loaded Script
...
Yes epics-gdb-helper.py
(gdb) info pretty-printer
global pretty-printers:
EPICS
...
```
+1
View File
@@ -14,6 +14,7 @@ EPICS Base Documentation
README
RELEASE_NOTES
ACF-Language
gdb
.. toctree::
:maxdepth: 2
+7
View File
@@ -0,0 +1,7 @@
## Add GDB Helper Script
Add [Debugging with GDB](gdb.md) section in documentation.
Add GDB helper script to pretty print some common database types.
Tested with GDB 16.3.
Compatible with python 3.
+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()