btrace: Add support for interrupt events.

Newer Intel CPUs support recording asynchronous events in the PT trace.
Libipt also recently added support for decoding these.

This patch adds support for interrupt events, based on the existing aux
infrastructure.  GDB can now display such events during the record
instruction-history and function-call-history commands.

Subsequent patches will add the rest of the events currently supported.

Approved-By: Markus Metzger <markus.t.metzger@intel.com>
This commit is contained in:
Felix Willgerodt
2023-06-26 16:54:25 +02:00
parent 13b3a89bc2
commit cdd65168f3
8 changed files with 364 additions and 25 deletions

View File

@@ -547,31 +547,39 @@ ftrace_new_gap (struct btrace_thread_info *btinfo, int errcode,
Return the chronologically latest function segment, never NULL. */ Return the chronologically latest function segment, never NULL. */
static struct btrace_function * static struct btrace_function *
ftrace_update_function (struct btrace_thread_info *btinfo, CORE_ADDR pc) ftrace_update_function (struct btrace_thread_info *btinfo,
std::optional<CORE_ADDR> pc)
{ {
struct minimal_symbol *mfun; struct minimal_symbol *mfun = nullptr;
struct symbol *fun; struct symbol *fun = nullptr;
struct btrace_function *bfun;
/* Try to determine the function we're in. We use both types of symbols /* Try to determine the function we're in. We use both types of symbols
to avoid surprises when we sometimes get a full symbol and sometimes to avoid surprises when we sometimes get a full symbol and sometimes
only a minimal symbol. */ only a minimal symbol. */
fun = find_pc_function (pc); if (pc.has_value ())
bound_minimal_symbol bmfun = lookup_minimal_symbol_by_pc (pc); {
mfun = bmfun.minsym; fun = find_pc_function (*pc);
bound_minimal_symbol bmfun = lookup_minimal_symbol_by_pc (*pc);
mfun = bmfun.minsym;
if (fun == NULL && mfun == NULL) if (fun == nullptr && mfun == nullptr)
DEBUG_FTRACE ("no symbol at %s", core_addr_to_string_nz (pc)); DEBUG_FTRACE ("no symbol at %s", core_addr_to_string_nz (*pc));
}
/* If we didn't have a function, we create one. */ /* If we didn't have a function, we create one. */
if (btinfo->functions.empty ()) if (btinfo->functions.empty ())
return ftrace_new_function (btinfo, mfun, fun); return ftrace_new_function (btinfo, mfun, fun);
/* If we had a gap before, we create a function. */ /* If we had a gap before, we create a function. */
bfun = &btinfo->functions.back (); btrace_function *bfun = &btinfo->functions.back ();
if (bfun->errcode != 0) if (bfun->errcode != 0)
return ftrace_new_function (btinfo, mfun, fun); return ftrace_new_function (btinfo, mfun, fun);
/* If there is no valid PC, which can happen for events with a
suppressed IP, we can't do more than return the last bfun. */
if (!pc.has_value ())
return bfun;
/* Check the last instruction, if we have one. /* Check the last instruction, if we have one.
We do this check first, since it allows us to fill in the call stack We do this check first, since it allows us to fill in the call stack
links in addition to the normal flow links. */ links in addition to the normal flow links. */
@@ -605,7 +613,7 @@ ftrace_update_function (struct btrace_thread_info *btinfo, CORE_ADDR pc)
case BTRACE_INSN_CALL: case BTRACE_INSN_CALL:
/* Ignore calls to the next instruction. They are used for PIC. */ /* Ignore calls to the next instruction. They are used for PIC. */
if (last->pc + last->size == pc) if (last->pc + last->size == *pc)
break; break;
return ftrace_new_call (btinfo, mfun, fun); return ftrace_new_call (btinfo, mfun, fun);
@@ -614,10 +622,10 @@ ftrace_update_function (struct btrace_thread_info *btinfo, CORE_ADDR pc)
{ {
CORE_ADDR start; CORE_ADDR start;
start = get_pc_function_start (pc); start = get_pc_function_start (*pc);
/* A jump to the start of a function is (typically) a tail call. */ /* A jump to the start of a function is (typically) a tail call. */
if (start == pc) if (start == *pc)
return ftrace_new_tailcall (btinfo, mfun, fun); return ftrace_new_tailcall (btinfo, mfun, fun);
/* Some versions of _Unwind_RaiseException use an indirect /* Some versions of _Unwind_RaiseException use an indirect
@@ -642,6 +650,18 @@ ftrace_update_function (struct btrace_thread_info *btinfo, CORE_ADDR pc)
break; break;
} }
case BTRACE_INSN_AUX:
/* An aux insn couldn't have switched the function. But the
segment might not have had a symbol name resolved yet, as events
might not have an IP. Use the current IP in that case and update
the name. */
if (bfun->sym == nullptr && bfun->msym == nullptr)
{
bfun->sym = fun;
bfun->msym = mfun;
}
break;
} }
} }
@@ -668,6 +688,8 @@ ftrace_update_insns (struct btrace_function *bfun, const btrace_insn &insn)
if (insn.iclass == BTRACE_INSN_AUX) if (insn.iclass == BTRACE_INSN_AUX)
bfun->flags |= BFUN_CONTAINS_AUX; bfun->flags |= BFUN_CONTAINS_AUX;
else
bfun->flags |= BFUN_CONTAINS_NON_AUX;
if (record_debug > 1) if (record_debug > 1)
ftrace_debug (bfun, "update insn"); ftrace_debug (bfun, "update insn");
@@ -1101,7 +1123,8 @@ btrace_compute_ftrace_bts (struct thread_info *tp,
break; break;
} }
bfun = ftrace_update_function (btinfo, pc); bfun = ftrace_update_function (btinfo,
std::make_optional<CORE_ADDR> (pc));
/* Maintain the function level offset. /* Maintain the function level offset.
For all but the last block, we do it here. */ For all but the last block, we do it here. */
@@ -1211,10 +1234,10 @@ pt_btrace_insn (const struct pt_insn &insn)
static void static void
handle_pt_aux_insn (btrace_thread_info *btinfo, std::string &aux_str, handle_pt_aux_insn (btrace_thread_info *btinfo, std::string &aux_str,
CORE_ADDR ip) std::optional<CORE_ADDR> pc)
{ {
btinfo->aux_data.emplace_back (std::move (aux_str)); btinfo->aux_data.emplace_back (std::move (aux_str));
struct btrace_function *bfun = ftrace_update_function (btinfo, ip); struct btrace_function *bfun = ftrace_update_function (btinfo, pc);
btrace_insn insn {{btinfo->aux_data.size () - 1}, 0, btrace_insn insn {{btinfo->aux_data.size () - 1}, 0,
BTRACE_INSN_AUX, 0}; BTRACE_INSN_AUX, 0};
@@ -1222,8 +1245,47 @@ handle_pt_aux_insn (btrace_thread_info *btinfo, std::string &aux_str,
ftrace_update_insns (bfun, insn); ftrace_update_insns (bfun, insn);
} }
/* Check if the recording contains real instructions and not only auxiliary
instructions since the last gap (or since the beginning). */
static bool
ftrace_contains_non_aux_since_last_gap (const btrace_thread_info *btinfo)
{
const std::vector<btrace_function> &functions = btinfo->functions;
std::vector<btrace_function>::const_reverse_iterator rit;
for (rit = functions.crbegin (); rit != functions.crend (); ++rit)
{
if (rit->errcode != 0)
return false;
if ((rit->flags & BFUN_CONTAINS_NON_AUX) != 0)
return true;
}
return false;
}
#endif /* defined (HAVE_PT_INSN_EVENT) */ #endif /* defined (HAVE_PT_INSN_EVENT) */
#if (LIBIPT_VERSION >= 0x201)
/* Translate an interrupt vector to a mnemonic string as defined for x86.
Returns nullptr if there is none. */
static const char *
decode_interrupt_vector (const uint8_t vector)
{
static const char *mnemonic[]
= { "#de", "#db", nullptr, "#bp", "#of", "#br", "#ud", "#nm",
"#df", "#mf", "#ts", "#np", "#ss", "#gp", "#pf", nullptr,
"#mf", "#ac", "#mc", "#xm", "#ve", "#cp" };
if (vector < (sizeof (mnemonic) / sizeof (mnemonic[0])))
return mnemonic[vector];
return nullptr;
}
#endif /* defined (LIBIPT_VERSION >= 0x201) */
/* Handle instruction decode events (libipt-v2). */ /* Handle instruction decode events (libipt-v2). */
static int static int
@@ -1236,6 +1298,7 @@ handle_pt_insn_events (struct btrace_thread_info *btinfo,
{ {
struct pt_event event; struct pt_event event;
uint64_t offset; uint64_t offset;
std::optional<CORE_ADDR> pc;
status = pt_insn_event (decoder, &event, sizeof (event)); status = pt_insn_event (decoder, &event, sizeof (event));
if (status < 0) if (status < 0)
@@ -1251,8 +1314,13 @@ handle_pt_insn_events (struct btrace_thread_info *btinfo,
if (event.status_update != 0) if (event.status_update != 0)
break; break;
/* Only create a new gap if there are non-aux instructions in
the trace since the last gap. We could be at the beginning
of the recording and could already have handled one or more
events, like ptev_iret, that created aux insns. In that
case we don't want to create a gap or print a warning. */
if (event.variant.enabled.resumed == 0 if (event.variant.enabled.resumed == 0
&& !btinfo->functions.empty ()) && ftrace_contains_non_aux_since_last_gap (btinfo))
{ {
struct btrace_function *bfun struct btrace_function *bfun
= ftrace_new_gap (btinfo, BDE_PT_NON_CONTIGUOUS, gaps); = ftrace_new_gap (btinfo, BDE_PT_NON_CONTIGUOUS, gaps);
@@ -1282,7 +1350,6 @@ handle_pt_insn_events (struct btrace_thread_info *btinfo,
#if defined (HAVE_STRUCT_PT_EVENT_VARIANT_PTWRITE) #if defined (HAVE_STRUCT_PT_EVENT_VARIANT_PTWRITE)
case ptev_ptwrite: case ptev_ptwrite:
{ {
uint64_t pc = 0;
std::optional<std::string> ptw_string; std::optional<std::string> ptw_string;
/* Lookup the PC if available. The event often doesn't provide /* Lookup the PC if available. The event often doesn't provide
@@ -1314,9 +1381,10 @@ handle_pt_insn_events (struct btrace_thread_info *btinfo,
} }
} }
if (pc == 0) if (!pc.has_value ())
warning (_("Failed to determine the PC for ptwrite.")); warning (_("Failed to determine the PC for ptwrite."));
if (btinfo->ptw_callback_fun != nullptr) if (btinfo->ptw_callback_fun != nullptr)
ptw_string ptw_string
= btinfo->ptw_callback_fun (event.variant.ptwrite.payload, = btinfo->ptw_callback_fun (event.variant.ptwrite.payload,
@@ -1333,6 +1401,34 @@ handle_pt_insn_events (struct btrace_thread_info *btinfo,
break; break;
} }
#endif /* defined (HAVE_STRUCT_PT_EVENT_VARIANT_PTWRITE) */ #endif /* defined (HAVE_STRUCT_PT_EVENT_VARIANT_PTWRITE) */
#if (LIBIPT_VERSION >= 0x201)
case ptev_interrupt:
{
std::string aux_string = std::string (_("interrupt: vector = "))
+ hex_string (event.variant.interrupt.vector);
const char *decoded
= decode_interrupt_vector (event.variant.interrupt.vector);
if (decoded != nullptr)
aux_string += std::string (" (") + decoded + ")";
if (event.variant.interrupt.has_cr2 != 0)
{
aux_string += std::string (", cr2 = ")
+ hex_string (event.variant.interrupt.cr2);
}
if (event.ip_suppressed == 0)
{
pc = event.variant.interrupt.ip;
aux_string += std::string (", ip = ") + hex_string (*pc);
}
handle_pt_aux_insn (btinfo, aux_string, pc);
break;
}
#endif /* defined (LIBIPT_VERSION >= 0x201) */
} }
} }
#endif /* defined (HAVE_PT_INSN_EVENT) */ #endif /* defined (HAVE_PT_INSN_EVENT) */
@@ -1428,7 +1524,9 @@ ftrace_add_pt (struct btrace_thread_info *btinfo,
/* Handle events indicated by flags in INSN. */ /* Handle events indicated by flags in INSN. */
handle_pt_insn_event_flags (btinfo, decoder, insn, gaps); handle_pt_insn_event_flags (btinfo, decoder, insn, gaps);
bfun = ftrace_update_function (btinfo, insn.ip); bfun
= ftrace_update_function (btinfo,
std::make_optional<CORE_ADDR> (insn.ip));
/* Maintain the function level offset. */ /* Maintain the function level offset. */
*plevel = std::min (*plevel, bfun->level); *plevel = std::min (*plevel, bfun->level);

View File

@@ -110,7 +110,11 @@ enum btrace_function_flag
/* Indicates that at least one auxiliary instruction is in the current /* Indicates that at least one auxiliary instruction is in the current
function segment. */ function segment. */
BFUN_CONTAINS_AUX = (1 << 2) BFUN_CONTAINS_AUX = (1 << 2),
/* Indicates that at least one instruction not of type BTRACE_INSN_AUX
is in the current function segment. */
BFUN_CONTAINS_NON_AUX = (1 << 3)
}; };
DEF_ENUM_FLAGS_TYPE (enum btrace_function_flag, btrace_function_flags); DEF_ENUM_FLAGS_TYPE (enum btrace_function_flag, btrace_function_flags);
@@ -356,7 +360,7 @@ struct btrace_thread_info
/* Function pointer to the ptwrite callback. Returns the string returned /* Function pointer to the ptwrite callback. Returns the string returned
by the ptwrite filter function. */ by the ptwrite filter function. */
std::optional<std::string> (*ptw_callback_fun) (const uint64_t payload, std::optional<std::string> (*ptw_callback_fun) (const uint64_t payload,
const uint64_t ip, std::optional<uint64_t> ip,
const void *ptw_context) const void *ptw_context)
= nullptr; = nullptr;

View File

@@ -810,7 +810,7 @@ recpy_bt_function_call_history (PyObject *self, void *closure)
/* Helper function that calls PTW_FILTER with PAYLOAD and IP as arguments. /* Helper function that calls PTW_FILTER with PAYLOAD and IP as arguments.
Returns the string that will be printed, if there is a filter to call. */ Returns the string that will be printed, if there is a filter to call. */
static std::optional<std::string> static std::optional<std::string>
recpy_call_filter (const uint64_t payload, const uint64_t ip, recpy_call_filter (const uint64_t payload, std::optional<uint64_t> ip,
const void *ptw_filter) const void *ptw_filter)
{ {
std::optional<std::string> result; std::optional<std::string> result;
@@ -824,10 +824,10 @@ recpy_call_filter (const uint64_t payload, const uint64_t ip,
gdbpy_ref<> py_payload = gdb_py_object_from_ulongest (payload); gdbpy_ref<> py_payload = gdb_py_object_from_ulongest (payload);
gdbpy_ref<> py_ip; gdbpy_ref<> py_ip;
if (ip == 0) if (!ip.has_value ())
py_ip = gdbpy_ref<>::new_reference (Py_None); py_ip = gdbpy_ref<>::new_reference (Py_None);
else else
py_ip = gdb_py_object_from_ulongest (ip); py_ip = gdb_py_object_from_ulongest (*ip);
gdbpy_ref<> py_result (PyObject_CallFunctionObjArgs ((PyObject *) ptw_filter, gdbpy_ref<> py_result (PyObject_CallFunctionObjArgs ((PyObject *) ptw_filter,
py_payload.get (), py_payload.get (),

View File

@@ -0,0 +1,32 @@
/* This testcase is part of GDB, the GNU debugger.
Copyright 2024 Free Software Foundation, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
static int
square (int num)
{
return num * num; /* bp1. */
}
int
main (void)
{
int a = 2;
int ans = 0;
ans = square (a);
return 0; /* bp2. */
}

View File

@@ -0,0 +1,72 @@
# This testcase is part of GDB, the GNU debugger.
#
# Copyright 2024 Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Test event tracing with gaps.
require allow_btrace_pt_event_trace_tests
standard_testfile
if {[prepare_for_testing "failed to prepare" $testfile $srcfile]} {
return -1
}
if {![runto_main]} {
return -1
}
gdb_test_no_output "set record btrace pt event-tracing on"
gdb_test_no_output "record btrace pt"
set bp_1 [gdb_get_line_number "bp1"]
set bp_2 [gdb_get_line_number "bp2"]
gdb_breakpoint $bp_1
gdb_breakpoint $bp_2
gdb_test "next"
# Inferior calls and return commands will create gaps in the recording.
gdb_test "call square (3)" [multi_line \
"" \
"Breakpoint $decimal, square \\(num=3\\) at \[^\r\n\]+:$bp_1" \
"$decimal.*bp1.*" \
"The program being debugged stopped while in a function called from GDB\\." \
"Evaluation of the expression containing the function" \
"\\(square\\) will be abandoned\\." \
"When the function is done executing, GDB will silently stop\\."]
gdb_test "return 9" "0.*main.*" \
"return" \
"Make.*return now\\? \\(y or n\\) " "y"
gdb_continue_to_breakpoint "break at bp_1" ".*$srcfile:$bp_1.*"
gdb_continue_to_breakpoint "break at bp_2" ".*$srcfile:$bp_2.*"
# We should have 2 gaps and events for each breakpoint we hit.
# Note that due to the asynchronous nature of certain events, we use
# gdb_test_sequence and check only for events that we can control.
gdb_test_sequence "record function-call-history" "function-call-history" {
"\[0-9\]+\tmain"
"\[0-9\]+\t\\\[non-contiguous\\\]"
"\[0-9\]+\tsquare"
"\\\[interrupt: vector = 0x3 \\\(#bp\\\)(, ip = 0x\[0-9a-fA-F\]+)?\\\]"
"\[0-9\]+\t\\\[non-contiguous\\\]"
"\[0-9\]+\tmain"
"\[0-9\]+\tsquare"
"\\\[interrupt: vector = 0x3 \\\(#bp\\\)(, ip = 0x\[0-9a-fA-F\]+)?\\\]"
"\[0-9\]+\tmain"
}

View File

@@ -0,0 +1,51 @@
# This testcase is part of GDB, the GNU debugger.
#
# Copyright 2024 Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Test basic Intel PT event tracing
require allow_btrace_pt_event_trace_tests
standard_testfile null-deref.c
if {[prepare_for_testing "failed to prepare" $testfile $srcfile]} {
return -1
}
if {![runto_main]} {
return -1
}
gdb_test_no_output "set record btrace pt event-tracing on"
gdb_test_no_output "record btrace pt"
gdb_test "continue" "Program received signal SIGSEGV, Segmentation fault.*"
# Test printing of at least one INTERRUPT event.
# This uses test_sequence to avoid random events failing the tests.
gdb_test_sequence "record function-call-history" "function-call-history" {
"\[0-9\]+\tmain"
"\t \\\[interrupt: vector = 0xe \\\(#pf\\\)(, cr2 = 0x0)?(, ip = 0x\[0-9a-fA-F\]+)?\\\]"
}
# Test the instruction-history. Assembly can differ between compilers. To
# avoid creating a .S file for this test we just check the event at the end.
gdb_test "record instruction-history" \
"$decimal\t \\\[interrupt: vector = 0xe \\\(#pf\\\)(, cr2 = 0x0)?(, ip = $hex)?\\\]"
# Test reverse stepping and replay stepping
gdb_test "reverse-stepi" "\\\[interrupt: vector = 0xe \\\(#pf\\\)(, cr2 = 0x0)?(, ip = $hex)?\\\].*"
gdb_test "stepi" "\\\[interrupt: vector = 0xe \\\(#pf\\\)(, cr2 = 0x0)?(, ip = $hex)?\\\].*"

View File

@@ -0,0 +1,26 @@
/* Copyright 2024 Free Software Foundation, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
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 <stddef.h>
int
main ()
{
int a = 34;
int *b = NULL;
a = *b; /* BP1. */
return 0;
}

View File

@@ -4438,6 +4438,62 @@ gdb_caching_proc allow_btrace_ptw_tests {} {
} }
# Run a test on the target to see if GDB supports event tracing on it.
# Return 1 if so, 0 if it does not.
gdb_caching_proc allow_btrace_pt_event_trace_tests {} {
global srcdir subdir
set me "allow_btrace_pt_event_trace_tests"
require allow_btrace_pt_tests
set src {
int
main ()
{
return 0;
}
}
if {![gdb_simple_compile $me $src executable]} {
return 0
}
gdb_exit
gdb_start
gdb_reinitialize_dir $srcdir/$subdir
gdb_load "$obj"
if ![runto_main] {
return 0
}
set allow_event_trace_tests 0
gdb_test_multiple "set record btrace pt event-tracing on" "$me: first check" {
-re -wrap "Event-tracing is not supported by GDB." {
}
-re -wrap "" {
set allow_event_trace_tests 1
}
}
if { $allow_event_trace_tests == 1 } {
gdb_test_multiple "record btrace pt" "$me: check OS support" {
-re -wrap "^" {
}
-re -wrap "" {
verbose -log "$me: Target doesn't support event tracing."
set allow_event_trace_tests 0
}
}
}
gdb_exit
remote_file build delete $obj
verbose "$me: returning $allow_event_trace_tests" 2
return $allow_event_trace_tests
}
# Run a test on the target to see if it supports Aarch64 SVE hardware. # Run a test on the target to see if it supports Aarch64 SVE hardware.
# Return 1 if so, 0 if it does not. Note this causes a restart of GDB. # Return 1 if so, 0 if it does not. Note this causes a restart of GDB.