From 4b03e97d7b24cbbb9825ea09dc3e8046f73b24d0 Mon Sep 17 00:00:00 2001 From: mdavidsaver Date: Sun, 21 Jun 2026 07:18:08 -0700 Subject: [PATCH] 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 --- documentation/Makefile | 1 + documentation/gdb.md | 114 ++++++++++ documentation/index.rst | 1 + documentation/new-notes/PR-110.md | 7 + modules/database/src/ioc/dbStatic/RULES | 3 + .../database/src/ioc/dbStatic/dbStaticLib.c | 21 ++ modules/database/src/ioc/epics-gdb-helper.py | 201 ++++++++++++++++++ 7 files changed, 348 insertions(+) create mode 100644 documentation/gdb.md create mode 100644 documentation/new-notes/PR-110.md create mode 100644 modules/database/src/ioc/epics-gdb-helper.py diff --git a/documentation/Makefile b/documentation/Makefile index 6555ce278..517c294e6 100644 --- a/documentation/Makefile +++ b/documentation/Makefile @@ -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:../%=%) diff --git a/documentation/gdb.md b/documentation/gdb.md new file mode 100644 index 000000000..9922bd23e --- /dev/null +++ b/documentation/gdb.md @@ -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 +... +``` diff --git a/documentation/index.rst b/documentation/index.rst index a84446ba7..d5d894730 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -14,6 +14,7 @@ EPICS Base Documentation README RELEASE_NOTES ACF-Language + gdb .. toctree:: :maxdepth: 2 diff --git a/documentation/new-notes/PR-110.md b/documentation/new-notes/PR-110.md new file mode 100644 index 000000000..2c2ecc1e7 --- /dev/null +++ b/documentation/new-notes/PR-110.md @@ -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. diff --git a/modules/database/src/ioc/dbStatic/RULES b/modules/database/src/ioc/dbStatic/RULES index 70e2cdaee..0e5525a0b 100644 --- a/modules/database/src/ioc/dbStatic/RULES +++ b/modules/database/src/ioc/dbStatic/RULES @@ -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 diff --git a/modules/database/src/ioc/dbStatic/dbStaticLib.c b/modules/database/src/ioc/dbStatic/dbStaticLib.c index cbdcea5fa..c6bf1c1ff 100644 --- a/modules/database/src/ioc/dbStatic/dbStaticLib.c +++ b/modules/database/src/ioc/dbStatic/dbStaticLib.c @@ -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 diff --git a/modules/database/src/ioc/epics-gdb-helper.py b/modules/database/src/ioc/epics-gdb-helper.py new file mode 100644 index 000000000..a7f55de68 --- /dev/null +++ b/modules/database/src/ioc/epics-gdb-helper.py @@ -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()