From: Andy Wingo <wingo@pobox.com>
To: guile-user@gnu.org
Subject: Potluck dish: GDB frame filter interface
Date: Tue, 17 Feb 2015 12:10:01 +0100 [thread overview]
Message-ID: <87y4nwx08m.fsf@pobox.com> (raw)
Hi!
A little potluck dish. I have some patches on the GDB mailing list to
expose the "frame filter" interface to Guile. This allows you to filter
the frames shown in a GDB backtrace using Guile. You can alter the
printed function address, name, arguments, file, line, and locals. You
can also elide, insert, and nest the printed frames. It's cosmetic but
can be useful. Documentation follows this mail.
To illustrate, on master I implemented a filter that zips a SRFI-41
stream of GDB frames with a SRFI-41 stream of Guile VM frames, nesting
the Guile frames in the right places on the GDB stack. For example,
here is the top of a stack trace after starting the REPL:
(gdb) bt
#0 0x00007ffff736aadd in ()
#1 0x00007ffff7b093ac in fport_fill_input (port=<optimized out>) at fports.c:756
#2 0x00007ffff7b36138 in scm_slow_get_byte_or_eof_unlocked (port=port@entry=#<port 0 6ee5e0>) at ports.c:2399
#3 0x00007ffff7b361c8 in scm_get_byte_or_eof (port=#<port 0 6ee5e0>) at ../libguile/ports.h:433
#4 0x00007ffff7b361c8 in scm_get_byte_or_eof (port=#<port 0 6ee5e0>) at ports.c:1417
#5 0x00007ffff1571447 in ()
#6 0x00007ffff155ac4c in ()
#7 0x00007ffff155b3c5 in ()
#8 0x00007ffff17be0e2 in %readline [scm_readline] (text="scheme@(guile-user)> ") at readline.c:249
#9 0x00007ffff17be0e2 in %readline [scm_readline] (text="scheme@(guile-user)> ", inp=#<port 0 6ee5e0>, outp=#<port 0 6ee540>, read_hook=<optimized out>) at readline.c:201
#10 0x00007ffff7b6ed91 in vm_debug_engine (thread=0x0, vp=0x68fd80, registers=0x1, resume=0) at vm-engine.c:815
#10 0x00007ffff7b8b608 in [subr call] ()
#10 0x00007ffff19ccc50 in 0x7ffff19ccc50 ()
#10 0x00007ffff19c8244 in 0x7ffff19c8244 ()
#10 0x00007ffff19c8458 in get-character ()
#11 0x00007ffff7b74380 in scm_call_n (proc=#<program 87d5d0>, argv=argv@entry=0x0, nargs=nargs@entry=0) at vm.c:1258
#12 0x00007ffff7afb9d9 in scm_call_0 (proc=<optimized out>) at eval.c:475
#13 0x00007ffff7b74a0e in sf_fill_input (port=#<port 3 6ce9a0>) at vports.c:94
#14 0x00007ffff7b36138 in scm_slow_get_byte_or_eof_unlocked (port=port@entry=#<port 3 6ce9a0>) at ports.c:2399
#15 0x00007ffff7b37ba6 in peek-char [scm_peek_char] (port=#<port 3 6ce9a0>) at ../libguile/ports.h:433
#16 0x00007ffff7b37ba6 in peek-char [scm_peek_char] (len=<synthetic pointer>, buf=0x7fffffffd940 "\210m\210", codepoint=<synthetic pointer>, port=#<port 3 6ce9a0>) at ports.c:1681
#17 0x00007ffff7b37ba6 in peek-char [scm_peek_char] (len=<synthetic pointer>, buf=0x7fffffffd940 "\210m\210", codepoint=<synthetic pointer>, port=#<port 3 6ce9a0>) at ports.c:1909
#18 0x00007ffff7b37ba6 in peek-char [scm_peek_char] (port=#<port 3 6ce9a0>) at ports.c:2252
#19 0x00007ffff7b6edd0 in vm_debug_engine (thread=0x0, vp=0x68fd80, registers=0x1, resume=0) at vm-engine.c:806
#19 0x00007ffff7b8b4e8 in [subr call] ()
#19 0x00007ffff7e144c0 in flush-leading-whitespace ()
#19 0x00007ffff7e1569c in 0x7ffff7e1569c ()
#19 0x00007ffff2710a70 in with-input-from-port ()
#19 0x00007ffff19cbe8c in 0x7ffff19cbe8c ()
#19 0x00007ffff26fdec4 in catch ()
#19 0x00007ffff7e14640 in run-repl* ()
#19 0x00007ffff7e1517c in start-repl* ()
#19 0x00007ffff7e1d428 in 0x7ffff7e1d428 ()
#19 0x00007ffff7e1d5d4 in 0x7ffff7e1d5d4 ()
#19 0x00007ffff2714154 in call-with-prompt ()
#20 0x00007ffff7b74380 in scm_call_n (proc=#<program 7ffff7fea008>, argv=argv@entry=0x7fffffffdbb8, nargs=nargs@entry=1) at vm.c:1258
#21 0x00007ffff7afcac7 in scm_primitive_eval (exp=exp@entry=((@ (ice-9 control) %) (begin (load-user-init) ((@ (ice-9 top-repl) top-repl))))) at eval.c:656
#22 0x00007ffff7afcb23 in eval [scm_eval] (exp=((@ (ice-9 control) %) (begin (load-user-init) ((@ (ice-9 top-repl) top-repl)))), module_or_state=module_or_state@entry="#<struct module>" = {...}) at eval.c:690
#23 0x00007ffff7b465dd in scm_shell (argc=1, argv=0x7fffffffe1f8) at script.c:454
[...]
You see the nested function frames corresponding to the Guile calls that
are active. It would be nice to get the arguments but that's a bit
tricky, for a few reasons, but you can do a "bt full" and print out all
the stack slots.
Anyway, just a wee potluck dish. Happy hacking!
Andy
@node Guile Frame Filter API
@subsubsection Filtering Frames in Guile
@cindex frame filters api, guile
Frame filters allow the user to programmatically alter the way a
backtrace (@pxref{Backtrace}) prints. Frame filters can reorganize,
annotate, insert, and remove frames in a backtrace.
Only commands that print a backtrace, or, in the case of @sc{gdb/mi}
commands (@pxref{GDB/MI}), those that return a collection of frames
are affected. The commands that work with frame filters are:
@table @code
@item backtrace
@xref{backtrace-command,, The backtrace command}.
@item -stack-list-frames
@xref{-stack-list-frames,, The -stack-list-frames command}.
@item -stack-list-variables
@xref{-stack-list-variables,, The -stack-list-variables command}.
@item -stack-list-arguments
@xref{-stack-list-arguments,, The -stack-list-arguments command}.
@item -stack-list-locals
@xref{-stack-list-locals,, The -stack-list-locals command}.
@end table
@cindex frame annotator
A frame filter is a function that takes a SRFI-41 stream of annotated
frame objects as an argument, and returns a potentially modified
stream of annotated frame objects. @xref{SRFI-41,,,guile,The Guile
Reference Manual}, for more on the SRFI-41 specification for lazy
streams. Operating over a stream allows frame filters to inspect,
reorganize, insert, and remove frames. @value{GDBN} also provides a
more simple @dfn{frame annotator} API that works on individual frames,
for the common case in which the user does not need to reorganize the
backtrace. Both APIs are described below.
There can be multiple frame filters registered with @value{GDBN}, and
each one may be individually enabled or disabled at will. Multiple
frame filters can be enabled at the same time. Frame filters have an
associated priority which determines the order in which they are
applied over the annotated frame stream. For example, if there are
two filters registered and enabled, @var{f1} and @var{f2}, and the
priority of @var{f2} is greater than that of @var{f1}, then the result
of frame filtering will be @code{(@var{f2} (@var{f1} @var{stream}))}.
In this way, higher-priority frame filters get the last word on the
backtrace that is ultimately printed.
An important consideration when designing frame filters, and well
worth reflecting upon, is that frame filters should avoid unwinding
the call stack if possible. Some stacks can run very deep, into the
tens of thousands in some cases. To search every frame when a frame
filter executes may be too expensive at that step. The frame filter
cannot know how many frames it has to iterate over, and it may have to
iterate through them all. This ends up duplicating effort as
@value{GDBN} performs this iteration when it prints the frames.
Therefore a frame filter should avoid peeking ahead in the frame
stream, if possible. @xref{Writing a Frame Filter}, for examples on
how to write a good frame filter.
To use frame filters, first load the @code{(gdb frames)} module to
have access to the procedures that manipulate frame filters:
@example
(use-modules (gdb frames))
@end example
@deffn {Scheme Procedure} add-frame-filter! name filter @
@r{[}#:priority priority@r{]} @r{[}#:enabled? boolean@r{]} @
@r{[}#:objfile objfile@r{]} @r{[}#:progspace progspace@r{]}
Register the frame filter procedure @var{filter} with @value{GDBN}.
@var{filter} should be a function of one argument, taking a SRFI-41
stream of annotated frames and returning a possibily modified stream
of annotated frames. The filter is identified by @var{name}, which
should be unique among all known filters.
The filter will be registered with the given @var{priority}, which
should be a number, and which defaults to 20 if not given. By
default, the filter is global, meaning that it is associated with all
objfiles and progspaces. Pass one of @code{#:objfile} or
@code{#:progspace} to instead associate the filter with a specific
objfile or progspace, respectively.
The filter will be initially enabled, unless the keyword argument
@code{#:enabled? #f} is given.
@end deffn
@deffn {Scheme Procedure} all-frame-filters
Return a list of the names of all frame filters.
@end deffn
@deffn {Scheme Procedure} remove-frame-filter! name
@deffnx {Scheme Procedure} enable-frame-filter! name
@deffnx {Scheme Procedure} disable-frame-filter! name
Remove, enable, or disable a frame filter, respectively. @var{name}
should correspond to the name of a filter previously added with
@code{add-frame-filter!}. If no such filter is found, an error is
signalled.
@end deffn
When a command is executed from @value{GDBN} that is compatible with
frame filters, @value{GDBN} selects all filters registered in the
current progspace, filters for all objfiles, and filters with no
associated objfile or progspace. That list is then sorted by
priority, as described above, and applied to the annotated frame
stream.
An annotated frame is a Guile record type that holds information about
a frame: its function name, its arguments, its locals, and so on. An
annotated frame is always associated with a @value{GDBN} frame object. To
add, remove, or otherwise alter information associated with an
annotated frame, use the @code{reannotate-frame} procedure.
@deffn {Scheme Procedure} reannotate-frame! ann @
@r{[}#:function-name function-name@r{]} @
@r{[}#:address address@r{]} @
@r{[}#:filename filename@r{]} @
@r{[}#:line line@r{]} @
@r{[}#:arguments arguments@r{]} @
@r{[}#:locals locals@r{]} @
@r{[}#:children children@r{]}
Take the annotated frame object @var{ann} and return a new annotated
frame object, replacing the fields specified by the keyword arguments
with their new values. For example, calling @code{(reannotate-frame
@var{x} #:function-name "foo")} will create a new annotated frame
object that inherits all fields from @var{x}, but whose function name
has been set to @samp{foo}.
@end deffn
The @code{(gdb frames)} module defines accessors for the various
fields of annotated frame objects.
@deffn {Scheme Procedure} annotated-frame-frame ann
Return the @value{GDBN} frame object associated with the annotated frame
@var{ann}. @xref{Frames In Guile}.
@end deffn
@deffn {Scheme Procedure} annotated-frame-function-name ann
Return the function name associated with the annotated frame
@var{ann}, as a string, or @code{#f} if not available.
@end deffn
@deffn {Scheme Procedure} annotated-frame-address ann
Return the address associated with the annotated frame @var{ann}, as
an integer.
@end deffn
@deffn {Scheme Procedure} annotated-frame-filename ann
Return the file name associated with the annotated frame @var{ann}, as
a string, or @code{#f} if not available.
@end deffn
@deffn {Scheme Procedure} annotated-frame-line ann
Return the line number associated with the annotated frame @var{ann},
as an integer, or @code{#f} if not available.
@end deffn
@deffn {Scheme Procedure} annotated-frame-arguments ann
Return a list of the function arguments associated with the annotated
frame @var{ann}. Each item of the list should either be a
@value{GDBN} symbol (@pxref{Symbols In Guile}), a pair of a
@value{GDBN} symbol and a @value{GDBN} value (@pxref{Values From
Inferior In Guile}, or a pair of a string and a @value{GDBN} value.
In the first case, the value will be loaded from the frame if needed.
@end deffn
@deffn {Scheme Procedure} annotated-frame-locals ann
Return a list of the function arguments associated with the annotated
frame @var{ann}, in the same format as for
@code{annotated-frame-arguments}.
@end deffn
Annotated frames may also have child frames. By default, no frame has
a child frame, but filters may reorganize the frame stream into a
stream of frame trees, by populating the child list. Of course, such
a reorganization is ultimately cosmetic, as it doesn't alter the stack
of frames seen by @value{GDBN} and navigable by the user, for example
by using the @code{frame} command. Still, nesting frames may lead to
a more understandable presentation of a backtrace.
@deffn {Scheme Procedure} annotated-frame-children ann
Return a list of the child frames associated with the annotated frame
@var{ann}. Each item of the list should be an annotated frame object.
@end deffn
While frame filters can both reorganize and reannotate the frame
stream, it is often the case that one only wants to reannotate the
frames in a stream, without reorganizing then. In that case there is
a simpler API for frame annotators that simply maps annotated frames
to annotated frames.
@deffn {Scheme Procedure} add-frame-annotator! name annotator @
@r{[}#:priority priority@r{]} @r{[}#:enabled? boolean@r{]} @
@r{[}#:objfile objfile@r{]} @r{[}#:progspace progspace@r{]}
Register the frame annotator procedure @var{annotator} with
@value{GDBN}. @var{annotator} should be a function of one argument,
taking annotated frame object and returning a possibily modified
annotated frame. The annotator is identified by @var{name}, which
should be unique among all known annotators.
The annotator has an associated priority, as with frame filters. See
the documentation on @code{add-frame-filter!}, for more.
The annotator will be initially enabled, unless the keyword argument
@code{#:enabled? #f} is given.
@end deffn
@deffn {Scheme Procedure} all-frame-annotators
Return a list of the names of all frame annotators.
@end deffn
@deffn {Scheme Procedure} remove-frame-annotator! name
@deffnx {Scheme Procedure} enable-frame-annotator! name
@deffnx {Scheme Procedure} disable-frame-annotator! name
Remove, enable, or disable a frame annotator, respectively.
@var{name} should correspond to the name of a annotator previously
added with @code{add-frame-annotator!}. If no such annotator is
found, an error is signalled.
@end deffn
@node Writing a Frame Filter in Guile
@subsubsection Writing a Frame Filter in Guile
@cindex writing a frame filter in guile
The simplest kind of frame filter just takes the incoming stream of
frames and produces an identical stream of values. For example:
@example
(use-modules (gdb frames))
(define (identity-frame-filter stream)
(cond
((stream-null? stream)
;; End of stream? Then return end-of-stream.
stream-null)
(else
;; Otherwise recurse on the tail of the stream.
(stream-cons (stream-car stream)
(identity-frame-filter (stream-cdr stream))))))
@end example
If you are not familiar with SRFI-41 streams, you might think that
this would eagerly traverse the whole stack of frames. This would be
bad because we don't want to produce an entire backtrace at once when
the user might cancel after only seeing one page. However this is not
the case, because unlike normal Scheme procedures, @code{stream-cons}
is @emph{lazy} in its arguments, which is to say that its arguments
are only evaluated when they are accessed via @code{stream-car} and
@code{stream-cdr}. In this way the stream looks infinite, but in
reality only produces values as they are requested by the caller.
To use this frame filter, we have to register it with @value{GDBN}.
@example
(add-frame-filter! "identity" identity-frame-filter)
@end example
Now our filter will run each time a backtrace is printed, or in
general for any @value{GDBN} command that uses the frame filter interface.
Filters are enabled when they are added. You can control the enabled
or disabled state of a filter using the appropriate procedures:
@example
(disable-frame-filter! "identity")
(enable-frame-filter! "identity")
@end example
Finally, we can remove all filters with a simple application of
@code{for-each}:
@example
(for-each remove-frame-filter! (all-frame-filters))
@end example
The same general mechanics apply to frame annotators as well.
Let us define a more interesting example. For example, in Guile there
is a function @code{scm_call_n}, which may be invoked directly but is
often invoked via well-known wrappers like @code{scm_call_0},
@code{scm_call_1}, and so on. For example here is part of a backtrace
of an optimized Guile build, when you first start a Guile REPL:
@smallexample
#10 0x00007ffff7b6ed91 in vm_debug_engine ([...]) at vm-engine.c:815
#11 0x00007ffff7b74380 in scm_call_n ([...]) at vm.c:1258
#12 0x00007ffff7afb9d9 in scm_call_0 ([...]) at eval.c:475
#13 0x00007ffff7b74a0e in sf_fill_input ([...]) at vports.c:94
@end smallexample
For the sake of the example, the arguments to each have been
abbreviated to @code{[...]}. Now, it might be nice if we could nest
@code{scm_call_n} inside @code{scm_call_0}, so let's do that:
@smallexample
(use-modules (gdb) (gdb frames) (srfi srfi-41))
(define (nest-scm-call-filter stream)
;; When we have the new head and tail, use this helper to make a
;; stream from them, lazily recursing on the tail.
(define (recur head tail)
(stream-cons head (nest-scm-call-filter tail)))
(cond
((stream-null? stream)
;; No more frames? Just return the stream as is.
stream)
(else
(let ((head (stream-car stream))
(tail (stream-cdr stream)))
(cond
;; Is this a call to scm_call_n and is there a next frame?
((and (equal? (annotated-frame-function-name head)
"scm_call_n")
(not (stream-null? tail)))
(let* ((next (stream-car tail))
(next-name (annotated-frame-function-name next)))
(cond
;; Does the next frame have a function name and
;; does it start with "scm_call_"?
((and next-name
(string-prefix? "scm_call_" next-name))
;; A match! Add `head' to the child list of `next'.
(let ((children (cons head
(annotated-frame-children next))))
(recur (reannotate-frame next #:children children)
(stream-cdr tail))))
(else (recur head tail)))))
(else (recur head tail)))))))
(add-frame-filter! "nest-scm-call" nest-scm-call-filter)
@end smallexample
With this filter in place, the resulting backtrace looks like:
@smallexample
#10 0x00007ffff7b6ed91 in vm_debug_engine ([...]) at vm-engine.c:815
#12 0x00007ffff7afb9d9 in scm_call_0 ([...]) at eval.c:475
#11 0x00007ffff7b74380 in scm_call_n ([...]) at vm.c:1258
#13 0x00007ffff7b74a0e in sf_fill_input ([...]) at vports.c:94
@end smallexample
As you can see, frame #11 has been nested below frame #12.
Sometimes, though, all this stream processing and stream recursion can
be too complicated if your desire is just to annotate individual
frames. In that situation, the frame annotator API can be more
appropriate. For example, if we know that there are some C procedures
that have ``aliases'' in some other language, like Scheme, then we can
annotate them in the backtrace with their Scheme names.
@smallexample
(use-modules (gdb frames))
(define *function-name-aliases*
'(("scm_primitive_eval" . "primitive-eval")))
(define (alias-annotator ann)
(let* ((name (annotated-frame-function-name ann))
(alias (assoc-ref *function-name-aliases* name)))
(if alias
(reannotate-frame ann #:function-name
(string-append "[" alias "] " name))
ann)))
(add-frame-annotator! "alias-annotator" alias-annotator)
@end smallexample
A backtrace with this annotator in place produces:
@smallexample
#19 [...] in vm_debug_engine ([...]) at vm-engine.c:806
#20 [...] in scm_call_n ([...]) at vm.c:1258
#21 [...] in [primitive-eval] scm_primitive_eval ([...]) at eval.c:656
#22 [...] in scm_eval ([...]) at eval.c:690
#23 [...] in scm_shell ([...]) at script.c:454
@end smallexample
Again, parts have been elided with @code{[...]}.
It is possible to do the job of an annotator with a filter, but if the
task is simple enough for an annotator, it's much less code, as the
above example shows.
--
http://wingolog.org/
reply other threads:[~2015-02-17 11:10 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.gnu.org/software/guile/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=87y4nwx08m.fsf@pobox.com \
--to=wingo@pobox.com \
--cc=guile-user@gnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).