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:
1 parent
58152f6a42
commit
4b03e97d7b
7 files changed
+348
No files matched your search
@@ -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:../%=%)
|
||||
|
||||
@@ -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
|
||||
...
|
||||
```
|
||||
@@ -14,6 +14,7 @@ EPICS Base Documentation
|
||||
README
|
||||
RELEASE_NOTES
|
||||
ACF-Language
|
||||
gdb
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user