/* Branch trace support for GDB, the GNU debugger.
- Copyright (C) 2013-2014 Free Software Foundation, Inc.
+ Copyright (C) 2013-2015 Free Software Foundation, Inc.
Contributed by Intel Corp. <markus.t.metzger@intel.com>
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
+#include "defs.h"
#include "btrace.h"
#include "gdbthread.h"
-#include "exceptions.h"
#include "inferior.h"
#include "target.h"
#include "record.h"
#include "source.h"
#include "filenames.h"
#include "xml-support.h"
+#include "regcache.h"
/* Print a record debug message. Use do ... while (0) to avoid ambiguities
when used in if statements. */
return SYMBOL_PRINT_NAME (sym);
if (msym != NULL)
- return SYMBOL_PRINT_NAME (msym);
+ return MSYMBOL_PRINT_NAME (msym);
return "<unknown>";
}
sym = bfun->sym;
if (sym != NULL)
- filename = symtab_to_filename_for_display (sym->symtab);
+ filename = symtab_to_filename_for_display (symbol_symtab (sym));
else
filename = "<unknown>";
/* If the minimal symbol changed, we certainly switched functions. */
if (mfun != NULL && msym != NULL
- && strcmp (SYMBOL_LINKAGE_NAME (mfun), SYMBOL_LINKAGE_NAME (msym)) != 0)
+ && strcmp (MSYMBOL_LINKAGE_NAME (mfun), MSYMBOL_LINKAGE_NAME (msym)) != 0)
return 1;
/* If the symbol changed, we certainly switched functions. */
return 1;
/* Check the location of those functions, as well. */
- bfname = symtab_to_fullname (sym->symtab);
- fname = symtab_to_fullname (fun->symtab);
+ bfname = symtab_to_fullname (symbol_symtab (sym));
+ fname = symtab_to_fullname (symbol_symtab (fun));
if (filename_cmp (fname, bfname) != 0)
return 1;
}
if (sym == NULL)
return 1;
- bfile = symtab_to_fullname (sym->symtab);
+ bfile = symtab_to_fullname (symbol_symtab (sym));
return (filename_cmp (bfile, fullname) != 0);
}
tail calls ending with a jump). */
static struct btrace_function *
-ftrace_find_call (struct gdbarch *gdbarch, struct btrace_function *bfun)
+ftrace_find_call (struct btrace_function *bfun)
{
for (; bfun != NULL; bfun = bfun->up)
{
struct btrace_insn *last;
- CORE_ADDR pc;
/* We do not allow empty function segments. */
gdb_assert (!VEC_empty (btrace_insn_s, bfun->insn));
last = VEC_last (btrace_insn_s, bfun->insn);
- pc = last->pc;
- if (gdbarch_insn_is_call (gdbarch, pc))
+ if (last->iclass == BTRACE_INSN_CALL)
break;
}
MFUN and FUN are the symbol information we have for this function. */
static struct btrace_function *
-ftrace_new_return (struct gdbarch *gdbarch,
- struct btrace_function *prev,
+ftrace_new_return (struct btrace_function *prev,
struct minimal_symbol *mfun,
struct symbol *fun)
{
wrong or that the call is simply not included in the trace. */
/* Let's search for some actual call. */
- caller = ftrace_find_call (gdbarch, prev->up);
+ caller = ftrace_find_call (prev->up);
if (caller == NULL)
{
/* There is no call in PREV's back trace. We assume that the
Return the chronologically latest function segment, never NULL. */
static struct btrace_function *
-ftrace_update_function (struct gdbarch *gdbarch,
- struct btrace_function *bfun, CORE_ADDR pc)
+ftrace_update_function (struct btrace_function *bfun, CORE_ADDR pc)
{
struct bound_minimal_symbol bmfun;
struct minimal_symbol *mfun;
if (last != NULL)
{
- CORE_ADDR lpc;
+ switch (last->iclass)
+ {
+ case BTRACE_INSN_RETURN:
+ return ftrace_new_return (bfun, mfun, fun);
- lpc = last->pc;
+ case BTRACE_INSN_CALL:
+ /* Ignore calls to the next instruction. They are used for PIC. */
+ if (last->pc + last->size == pc)
+ break;
- /* Check for returns. */
- if (gdbarch_insn_is_ret (gdbarch, lpc))
- return ftrace_new_return (gdbarch, bfun, mfun, fun);
+ return ftrace_new_call (bfun, mfun, fun);
- /* Check for calls. */
- if (gdbarch_insn_is_call (gdbarch, lpc))
- {
- int size;
+ case BTRACE_INSN_JUMP:
+ {
+ CORE_ADDR start;
- size = gdb_insn_length (gdbarch, lpc);
+ start = get_pc_function_start (pc);
- /* Ignore calls to the next instruction. They are used for PIC. */
- if (lpc + size != pc)
- return ftrace_new_call (bfun, mfun, fun);
+ /* If we can't determine the function for PC, we treat a jump at
+ the end of the block as tail call. */
+ if (start == 0 || start == pc)
+ return ftrace_new_tailcall (bfun, mfun, fun);
+ }
}
}
ftrace_print_function_name (bfun),
ftrace_print_filename (bfun));
- if (last != NULL)
- {
- CORE_ADDR start, lpc;
-
- start = get_pc_function_start (pc);
-
- /* If we can't determine the function for PC, we treat a jump at
- the end of the block as tail call. */
- if (start == 0)
- start = pc;
-
- lpc = last->pc;
-
- /* Jumps indicate optimized tail calls. */
- if (start == pc && gdbarch_insn_is_jump (gdbarch, lpc))
- return ftrace_new_tailcall (bfun, mfun, fun);
- }
-
return ftrace_new_switch (bfun, mfun, fun);
}
/* Add the instruction at PC to BFUN's instructions. */
static void
-ftrace_update_insns (struct btrace_function *bfun, CORE_ADDR pc)
+ftrace_update_insns (struct btrace_function *bfun,
+ const struct btrace_insn *insn)
{
- struct btrace_insn *insn;
-
- insn = VEC_safe_push (btrace_insn_s, bfun->insn, NULL);
- insn->pc = pc;
+ VEC_safe_push (btrace_insn_s, bfun->insn, insn);
if (record_debug > 1)
ftrace_debug (bfun, "update insn");
}
-/* Compute the function branch trace from a block branch trace BTRACE for
- a thread given by BTINFO. */
+/* Classify the instruction at PC. */
+
+static enum btrace_insn_class
+ftrace_classify_insn (struct gdbarch *gdbarch, CORE_ADDR pc)
+{
+ volatile struct gdb_exception error;
+ enum btrace_insn_class iclass;
+
+ iclass = BTRACE_INSN_OTHER;
+ TRY_CATCH (error, RETURN_MASK_ERROR)
+ {
+ if (gdbarch_insn_is_call (gdbarch, pc))
+ iclass = BTRACE_INSN_CALL;
+ else if (gdbarch_insn_is_ret (gdbarch, pc))
+ iclass = BTRACE_INSN_RETURN;
+ else if (gdbarch_insn_is_jump (gdbarch, pc))
+ iclass = BTRACE_INSN_JUMP;
+ }
+
+ return iclass;
+}
+
+/* Compute the function branch trace from BTS trace. */
static void
-btrace_compute_ftrace (struct btrace_thread_info *btinfo,
- VEC (btrace_block_s) *btrace)
+btrace_compute_ftrace_bts (struct thread_info *tp,
+ const struct btrace_data_bts *btrace)
{
+ struct btrace_thread_info *btinfo;
struct btrace_function *begin, *end;
struct gdbarch *gdbarch;
unsigned int blk;
int level;
- DEBUG ("compute ftrace");
-
gdbarch = target_gdbarch ();
- begin = NULL;
- end = NULL;
- level = INT_MAX;
- blk = VEC_length (btrace_block_s, btrace);
+ btinfo = &tp->btrace;
+ begin = btinfo->begin;
+ end = btinfo->end;
+ level = begin != NULL ? -btinfo->level : INT_MAX;
+ blk = VEC_length (btrace_block_s, btrace->blocks);
while (blk != 0)
{
blk -= 1;
- block = VEC_index (btrace_block_s, btrace, blk);
+ block = VEC_index (btrace_block_s, btrace->blocks, blk);
pc = block->begin;
for (;;)
{
+ volatile struct gdb_exception error;
+ struct btrace_insn insn;
int size;
/* We should hit the end of the block. Warn if we went too far. */
break;
}
- end = ftrace_update_function (gdbarch, end, pc);
+ end = ftrace_update_function (end, pc);
if (begin == NULL)
begin = end;
- /* Maintain the function level offset. */
- level = min (level, end->level);
+ /* Maintain the function level offset.
+ For all but the last block, we do it here. */
+ if (blk != 0)
+ level = min (level, end->level);
+
+ size = 0;
+ TRY_CATCH (error, RETURN_MASK_ERROR)
+ size = gdb_insn_length (gdbarch, pc);
- ftrace_update_insns (end, pc);
+ insn.pc = pc;
+ insn.size = size;
+ insn.iclass = ftrace_classify_insn (gdbarch, pc);
+
+ ftrace_update_insns (end, &insn);
ftrace_update_lines (end, pc);
/* We're done once we pushed the instruction at the end. */
if (block->end == pc)
break;
- size = gdb_insn_length (gdbarch, pc);
-
- /* Make sure we terminate if we fail to compute the size. */
+ /* We can't continue if we fail to compute the size. */
if (size <= 0)
{
warning (_("Recorded trace may be incomplete around %s."),
}
pc += size;
+
+ /* Maintain the function level offset.
+ For the last block, we do it here to not consider the last
+ instruction.
+ Since the last instruction corresponds to the current instruction
+ and is not really part of the execution history, it shouldn't
+ affect the level. */
+ if (blk == 0)
+ level = min (level, end->level);
}
}
btinfo->level = -level;
}
+/* Compute the function branch trace from a block branch trace BTRACE for
+ a thread given by BTINFO. */
+
+static void
+btrace_compute_ftrace (struct thread_info *tp, struct btrace_data *btrace)
+{
+ DEBUG ("compute ftrace");
+
+ switch (btrace->format)
+ {
+ case BTRACE_FORMAT_NONE:
+ return;
+
+ case BTRACE_FORMAT_BTS:
+ btrace_compute_ftrace_bts (tp, &btrace->variant.bts);
+ return;
+ }
+
+ internal_error (__FILE__, __LINE__, _("Unkown branch trace format."));
+}
+
+/* Add an entry for the current PC. */
+
+static void
+btrace_add_pc (struct thread_info *tp)
+{
+ struct btrace_data btrace;
+ struct btrace_block *block;
+ struct regcache *regcache;
+ struct cleanup *cleanup;
+ CORE_ADDR pc;
+
+ regcache = get_thread_regcache (tp->ptid);
+ pc = regcache_read_pc (regcache);
+
+ btrace_data_init (&btrace);
+ btrace.format = BTRACE_FORMAT_BTS;
+ btrace.variant.bts.blocks = NULL;
+
+ cleanup = make_cleanup_btrace_data (&btrace);
+
+ block = VEC_safe_push (btrace_block_s, btrace.variant.bts.blocks, NULL);
+ block->begin = pc;
+ block->end = pc;
+
+ btrace_compute_ftrace (tp, &btrace);
+
+ do_cleanups (cleanup);
+}
+
/* See btrace.h. */
void
-btrace_enable (struct thread_info *tp)
+btrace_enable (struct thread_info *tp, const struct btrace_config *conf)
{
if (tp->btrace.target != NULL)
return;
- if (!target_supports_btrace ())
+ if (!target_supports_btrace (conf->format))
error (_("Target does not support branch tracing."));
DEBUG ("enable thread %d (%s)", tp->num, target_pid_to_str (tp->ptid));
- tp->btrace.target = target_enable_btrace (tp->ptid);
+ tp->btrace.target = target_enable_btrace (tp->ptid, conf);
+
+ /* Add an entry for the current PC so we start tracing from where we
+ enabled it. */
+ if (tp->btrace.target != NULL)
+ btrace_add_pc (tp);
+}
+
+/* See btrace.h. */
+
+const struct btrace_config *
+btrace_conf (const struct btrace_thread_info *btinfo)
+{
+ if (btinfo->target == NULL)
+ return NULL;
+
+ return target_btrace_conf (btinfo->target);
}
/* See btrace.h. */
btrace_clear (tp);
}
+/* Stitch branch trace in BTS format. */
+
+static int
+btrace_stitch_bts (struct btrace_data_bts *btrace,
+ const struct btrace_thread_info *btinfo)
+{
+ struct btrace_function *last_bfun;
+ struct btrace_insn *last_insn;
+ btrace_block_s *first_new_block;
+
+ last_bfun = btinfo->end;
+ gdb_assert (last_bfun != NULL);
+
+ /* Beware that block trace starts with the most recent block, so the
+ chronologically first block in the new trace is the last block in
+ the new trace's block vector. */
+ gdb_assert (!VEC_empty (btrace_block_s, btrace->blocks));
+ first_new_block = VEC_last (btrace_block_s, btrace->blocks);
+ last_insn = VEC_last (btrace_insn_s, last_bfun->insn);
+
+ /* If the current PC at the end of the block is the same as in our current
+ trace, there are two explanations:
+ 1. we executed the instruction and some branch brought us back.
+ 2. we have not made any progress.
+ In the first case, the delta trace vector should contain at least two
+ entries.
+ In the second case, the delta trace vector should contain exactly one
+ entry for the partial block containing the current PC. Remove it. */
+ if (first_new_block->end == last_insn->pc
+ && VEC_length (btrace_block_s, btrace->blocks) == 1)
+ {
+ VEC_pop (btrace_block_s, btrace->blocks);
+ return 0;
+ }
+
+ DEBUG ("stitching %s to %s", ftrace_print_insn_addr (last_insn),
+ core_addr_to_string_nz (first_new_block->end));
+
+ /* Do a simple sanity check to make sure we don't accidentally end up
+ with a bad block. This should not occur in practice. */
+ if (first_new_block->end < last_insn->pc)
+ {
+ warning (_("Error while trying to read delta trace. Falling back to "
+ "a full read."));
+ return -1;
+ }
+
+ /* We adjust the last block to start at the end of our current trace. */
+ gdb_assert (first_new_block->begin == 0);
+ first_new_block->begin = last_insn->pc;
+
+ /* We simply pop the last insn so we can insert it again as part of
+ the normal branch trace computation.
+ Since instruction iterators are based on indices in the instructions
+ vector, we don't leave any pointers dangling. */
+ DEBUG ("pruning insn at %s for stitching",
+ ftrace_print_insn_addr (last_insn));
+
+ VEC_pop (btrace_insn_s, last_bfun->insn);
+
+ /* The instructions vector may become empty temporarily if this has
+ been the only instruction in this function segment.
+ This violates the invariant but will be remedied shortly by
+ btrace_compute_ftrace when we add the new trace. */
+ return 0;
+}
+
+/* Adjust the block trace in order to stitch old and new trace together.
+ BTRACE is the new delta trace between the last and the current stop.
+ BTINFO is the old branch trace until the last stop.
+ May modifx BTRACE as well as the existing trace in BTINFO.
+ Return 0 on success, -1 otherwise. */
+
+static int
+btrace_stitch_trace (struct btrace_data *btrace,
+ const struct btrace_thread_info *btinfo)
+{
+ /* If we don't have trace, there's nothing to do. */
+ if (btrace_data_empty (btrace))
+ return 0;
+
+ switch (btrace->format)
+ {
+ case BTRACE_FORMAT_NONE:
+ return 0;
+
+ case BTRACE_FORMAT_BTS:
+ return btrace_stitch_bts (&btrace->variant.bts, btinfo);
+ }
+
+ internal_error (__FILE__, __LINE__, _("Unkown branch trace format."));
+}
+
+/* Clear the branch trace histories in BTINFO. */
+
+static void
+btrace_clear_history (struct btrace_thread_info *btinfo)
+{
+ xfree (btinfo->insn_history);
+ xfree (btinfo->call_history);
+ xfree (btinfo->replay);
+
+ btinfo->insn_history = NULL;
+ btinfo->call_history = NULL;
+ btinfo->replay = NULL;
+}
+
/* See btrace.h. */
void
btrace_fetch (struct thread_info *tp)
{
struct btrace_thread_info *btinfo;
- VEC (btrace_block_s) *btrace;
+ struct btrace_target_info *tinfo;
+ struct btrace_data btrace;
struct cleanup *cleanup;
+ int errcode;
DEBUG ("fetch thread %d (%s)", tp->num, target_pid_to_str (tp->ptid));
btinfo = &tp->btrace;
- if (btinfo->target == NULL)
+ tinfo = btinfo->target;
+ if (tinfo == NULL)
return;
- btrace = target_read_btrace (btinfo->target, BTRACE_READ_NEW);
- cleanup = make_cleanup (VEC_cleanup (btrace_block_s), &btrace);
+ /* There's no way we could get new trace while replaying.
+ On the other hand, delta trace would return a partial record with the
+ current PC, which is the replay PC, not the last PC, as expected. */
+ if (btinfo->replay != NULL)
+ return;
+
+ btrace_data_init (&btrace);
+ cleanup = make_cleanup_btrace_data (&btrace);
- if (!VEC_empty (btrace_block_s, btrace))
+ /* Let's first try to extend the trace we already have. */
+ if (btinfo->end != NULL)
{
- btrace_clear (tp);
- btrace_compute_ftrace (btinfo, btrace);
+ errcode = target_read_btrace (&btrace, tinfo, BTRACE_READ_DELTA);
+ if (errcode == 0)
+ {
+ /* Success. Let's try to stitch the traces together. */
+ errcode = btrace_stitch_trace (&btrace, btinfo);
+ }
+ else
+ {
+ /* We failed to read delta trace. Let's try to read new trace. */
+ errcode = target_read_btrace (&btrace, tinfo, BTRACE_READ_NEW);
+
+ /* If we got any new trace, discard what we have. */
+ if (errcode == 0 && !btrace_data_empty (&btrace))
+ btrace_clear (tp);
+ }
+
+ /* If we were not able to read the trace, we start over. */
+ if (errcode != 0)
+ {
+ btrace_clear (tp);
+ errcode = target_read_btrace (&btrace, tinfo, BTRACE_READ_ALL);
+ }
+ }
+ else
+ errcode = target_read_btrace (&btrace, tinfo, BTRACE_READ_ALL);
+
+ /* If we were not able to read the branch trace, signal an error. */
+ if (errcode != 0)
+ error (_("Failed to read branch trace."));
+
+ /* Compute the trace, provided we have any. */
+ if (!btrace_data_empty (&btrace))
+ {
+ btrace_clear_history (btinfo);
+ btrace_compute_ftrace (tp, &btrace);
}
do_cleanups (cleanup);
DEBUG ("clear thread %d (%s)", tp->num, target_pid_to_str (tp->ptid));
+ /* Make sure btrace frames that may hold a pointer into the branch
+ trace data are destroyed. */
+ reinit_frame_cache ();
+
btinfo = &tp->btrace;
it = btinfo->begin;
btinfo->begin = NULL;
btinfo->end = NULL;
- xfree (btinfo->insn_history);
- xfree (btinfo->call_history);
-
- btinfo->insn_history = NULL;
- btinfo->call_history = NULL;
+ btrace_clear_history (btinfo);
}
/* See btrace.h. */
DEBUG ("free objfile");
- ALL_THREADS (tp)
+ ALL_NON_EXITED_THREADS (tp)
btrace_clear (tp);
}
const struct gdb_xml_element *element,
void *user_data, VEC (gdb_xml_value_s) *attributes)
{
- VEC (btrace_block_s) **btrace;
+ struct btrace_data *btrace;
struct btrace_block *block;
ULONGEST *begin, *end;
btrace = user_data;
- block = VEC_safe_push (btrace_block_s, *btrace, NULL);
+
+ switch (btrace->format)
+ {
+ case BTRACE_FORMAT_BTS:
+ break;
+
+ case BTRACE_FORMAT_NONE:
+ btrace->format = BTRACE_FORMAT_BTS;
+ btrace->variant.bts.blocks = NULL;
+ break;
+
+ default:
+ gdb_xml_error (parser, _("Btrace format error."));
+ }
begin = xml_find_attribute (attributes, "begin")->value;
end = xml_find_attribute (attributes, "end")->value;
+ block = VEC_safe_push (btrace_block_s, btrace->variant.bts.blocks, NULL);
block->begin = *begin;
block->end = *end;
}
/* See btrace.h. */
-VEC (btrace_block_s) *
-parse_xml_btrace (const char *buffer)
+void
+parse_xml_btrace (struct btrace_data *btrace, const char *buffer)
{
- VEC (btrace_block_s) *btrace = NULL;
struct cleanup *cleanup;
int errcode;
#if defined (HAVE_LIBEXPAT)
- cleanup = make_cleanup (VEC_cleanup (btrace_block_s), &btrace);
+ btrace->format = BTRACE_FORMAT_NONE;
+
+ cleanup = make_cleanup_btrace_data (btrace);
errcode = gdb_xml_parse_quick (_("btrace"), "btrace.dtd", btrace_elements,
- buffer, &btrace);
+ buffer, btrace);
if (errcode != 0)
- {
- do_cleanups (cleanup);
- return NULL;
- }
+ error (_("Error parsing branch trace."));
/* Keep parse results. */
discard_cleanups (cleanup);
error (_("Cannot process branch trace. XML parsing is not supported."));
#endif /* !defined (HAVE_LIBEXPAT) */
+}
+
+#if defined (HAVE_LIBEXPAT)
+
+/* Parse a btrace-conf "bts" xml record. */
+
+static void
+parse_xml_btrace_conf_bts (struct gdb_xml_parser *parser,
+ const struct gdb_xml_element *element,
+ void *user_data, VEC (gdb_xml_value_s) *attributes)
+{
+ struct btrace_config *conf;
+ struct gdb_xml_value *size;
+
+ conf = user_data;
+ conf->format = BTRACE_FORMAT_BTS;
+ conf->bts.size = 0;
+
+ size = xml_find_attribute (attributes, "size");
+ if (size != NULL)
+ conf->bts.size = (unsigned int) * (ULONGEST *) size->value;
+}
+
+static const struct gdb_xml_attribute btrace_conf_bts_attributes[] = {
+ { "size", GDB_XML_AF_OPTIONAL, gdb_xml_parse_attr_ulongest, NULL },
+ { NULL, GDB_XML_AF_NONE, NULL, NULL }
+};
+
+static const struct gdb_xml_element btrace_conf_children[] = {
+ { "bts", btrace_conf_bts_attributes, NULL, GDB_XML_EF_OPTIONAL,
+ parse_xml_btrace_conf_bts, NULL },
+ { NULL, NULL, NULL, GDB_XML_EF_NONE, NULL, NULL }
+};
+
+static const struct gdb_xml_attribute btrace_conf_attributes[] = {
+ { "version", GDB_XML_AF_NONE, NULL, NULL },
+ { NULL, GDB_XML_AF_NONE, NULL, NULL }
+};
+
+static const struct gdb_xml_element btrace_conf_elements[] = {
+ { "btrace-conf", btrace_conf_attributes, btrace_conf_children,
+ GDB_XML_EF_NONE, NULL, NULL },
+ { NULL, NULL, NULL, GDB_XML_EF_NONE, NULL, NULL }
+};
+
+#endif /* defined (HAVE_LIBEXPAT) */
- return btrace;
+/* See btrace.h. */
+
+void
+parse_xml_btrace_conf (struct btrace_config *conf, const char *xml)
+{
+ int errcode;
+
+#if defined (HAVE_LIBEXPAT)
+
+ errcode = gdb_xml_parse_quick (_("btrace-conf"), "btrace-conf.dtd",
+ btrace_conf_elements, xml, conf);
+ if (errcode != 0)
+ error (_("Error parsing branch trace configuration."));
+
+#else /* !defined (HAVE_LIBEXPAT) */
+
+ error (_("XML parsing is not supported."));
+
+#endif /* !defined (HAVE_LIBEXPAT) */
}
/* See btrace.h. */
btinfo->call_history->begin = *begin;
btinfo->call_history->end = *end;
}
+
+/* See btrace.h. */
+
+int
+btrace_is_replaying (struct thread_info *tp)
+{
+ return tp->btrace.replay != NULL;
+}
+
+/* See btrace.h. */
+
+int
+btrace_is_empty (struct thread_info *tp)
+{
+ struct btrace_insn_iterator begin, end;
+ struct btrace_thread_info *btinfo;
+
+ btinfo = &tp->btrace;
+
+ if (btinfo->begin == NULL)
+ return 1;
+
+ btrace_insn_begin (&begin, btinfo);
+ btrace_insn_end (&end, btinfo);
+
+ return btrace_insn_cmp (&begin, &end) == 0;
+}
+
+/* Forward the cleanup request. */
+
+static void
+do_btrace_data_cleanup (void *arg)
+{
+ btrace_data_fini (arg);
+}
+
+/* See btrace.h. */
+
+struct cleanup *
+make_cleanup_btrace_data (struct btrace_data *data)
+{
+ return make_cleanup (do_btrace_data_cleanup, data);
+}