mirror of
https://sourceware.org/git/binutils-gdb.git
synced 2026-08-27 00:26:02 -04:00
Result of: ... $ pre-commit run isort --all-files isort...................................................................Failed - hook id: isort - files were modified by this hook Fixing gdb/testsuite/gdb.python/py-frame.py ...
189 lines
7 KiB
Python
189 lines
7 KiB
Python
# Copyright (C) 2026 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/>.
|
|
|
|
# A class which is a GDB frame unwinder. This frame unwinder can be
|
|
# used to corrupt the backtrace by tricking GDB into believing that
|
|
# there is a repeated frame-id in the backtrace.
|
|
#
|
|
# Sourcing this script creates a global called 'global_unwinder' and
|
|
# registers a global unwinder called 'stop-at-level'. The unwinder
|
|
# needs to be primed with the frame-ids copied from the current stack,
|
|
# this can be done by enabling the unwinder using the GDB command:
|
|
#
|
|
# (gdb) enable unwinder global stop-at-level
|
|
#
|
|
# You then need to set the level at which to corrupt the stack, e.g.:
|
|
#
|
|
# (gdb) python global_unwinder.stop_at_level = 3
|
|
#
|
|
# This will allow the frames #0, #1, #2, and #3 to print correctly,
|
|
# but frame #4 will appear to be a repeat of frame #3, so GDB will
|
|
# terminate the backtrace.
|
|
#
|
|
# As the unwinder relies on cached frame-ids, then resuming the
|
|
# inferior for which the frame-ids were cached will cause the unwinder
|
|
# to auto-disable itself, after which you'll need to re-enable the
|
|
# next time the inferior stops.
|
|
#
|
|
# Changing the 'global_unwinder.stop_at_level' will flush the frame
|
|
# cache, so you can test breaking the stack at different levels.
|
|
|
|
|
|
import re
|
|
|
|
import gdb
|
|
from gdb.unwinder import FrameId, Unwinder
|
|
|
|
|
|
class corrupt_stack_unwinder(Unwinder):
|
|
def __init__(self):
|
|
# Is this unwinder enabled or not? Accessed via the 'enabled'
|
|
# property.
|
|
self._enabled = True
|
|
|
|
# List of FrameId instances, one for each stack frame. This list is
|
|
# populated when this file is sourced into GDB.
|
|
self._frame_ids = []
|
|
|
|
# The inferior for which we cached self._frame_ids. The
|
|
# unwinder will only work within this inferior.
|
|
self._inferior = None
|
|
|
|
# At what stack level should we break the unwind?
|
|
self._stop_at_level = None
|
|
|
|
Unwinder.__init__(self, "stop-at-level")
|
|
|
|
if gdb.selected_inferior().pid > 0:
|
|
self._update_frame_id_cache()
|
|
else:
|
|
self._enabled = False
|
|
|
|
gdb.events.cont.connect(lambda ev: self._continued_event_handler(ev))
|
|
|
|
# Assume that the current inferior has a stack. Collect a FrameId
|
|
# object for each level of the current stack.
|
|
def _update_frame_id_cache(self):
|
|
self._frame_ids = []
|
|
self._inferior = gdb.selected_inferior()
|
|
|
|
frame = gdb.newest_frame()
|
|
while frame is not None:
|
|
# Get the frame-id in a verbose text form.
|
|
output = gdb.execute(
|
|
"maint print frame-id %d" % frame.level(), to_string=True
|
|
)
|
|
|
|
# Parse the frame-id in OUTPUT, find the stack and code addresses.
|
|
match = re.search(r"stack=(0x[0-9a-fA-F]+).*?code=(0x[0-9a-fA-F]+)", output)
|
|
if not match:
|
|
raise gdb.GdbError(
|
|
"Could not parse frame-id for frame #%d" % frame.level()
|
|
)
|
|
|
|
# Create the FrameId object.
|
|
sp_addr = int(match.group(1), 16)
|
|
pc_addr = int(match.group(2), 16)
|
|
self._frame_ids.append(FrameId(sp_addr, pc_addr))
|
|
|
|
frame = frame.older()
|
|
|
|
# Called when an inferior continues. If this is the inferior for
|
|
# which this unwinder collected the stack, then discard the
|
|
# collected FrameId objects and disable the unwinder.
|
|
def _continued_event_handler(self, event):
|
|
if gdb.selected_inferior() == self._inferior:
|
|
self._enabled = False
|
|
self._frame_ids = []
|
|
self._inferior = None
|
|
|
|
# To aid debugging, print the captured FrameId instances.
|
|
def print_frame_ids(self):
|
|
for level, fid in enumerate(self._frame_ids):
|
|
print(
|
|
"frame-id for frame #%s: {stack=0x%x,code=0x%x}"
|
|
% (level, fid.sp, fid.pc)
|
|
)
|
|
|
|
def __call__(self, pending_frame):
|
|
if self.stop_at_level is None or pending_frame.level() != self.stop_at_level:
|
|
return None
|
|
|
|
if gdb.selected_inferior() != self._inferior:
|
|
return None
|
|
|
|
if len(self._frame_ids) <= self.stop_at_level:
|
|
raise gdb.GdbError("not enough parsed frame-ids")
|
|
|
|
# Set the frame-id for this frame to its actual, expected
|
|
# frame-id, which we captured in the FRAME_IDS list.
|
|
unwinder = pending_frame.create_unwind_info(self._frame_ids[self.stop_at_level])
|
|
|
|
# Provide the register values for the caller frame, that is,
|
|
# the frame at 'STOP_AT_LEVEL + 1'.
|
|
#
|
|
# We forward all of the register values unchanged from this
|
|
# frame.
|
|
#
|
|
# What this means is that, as far as GDB is concerned, the
|
|
# caller frame will appear to be identical to this frame. Of
|
|
# particular importance, we send $pc and $sp unchanged to the
|
|
# caller frame.
|
|
#
|
|
# Because the caller frame has the same $pc and $sp as this
|
|
# frame, GDB will compute the same frame-id for the caller
|
|
# frame as we just supplied for this frame (above). This
|
|
# creates the artificial frame cycle which is the whole point
|
|
# of this test.
|
|
#
|
|
# NOTE: Forwarding all registers unchanged like this to the
|
|
# caller frame is not how you'd normally write a frame
|
|
# unwinder. Some registers might indeed be unmodified between
|
|
# frames, but we'd usually expect the $sp and/or the $pc to
|
|
# change. This test is deliberately doing something weird in
|
|
# order to force a cycle, and so test GDB.
|
|
for reg in pending_frame.architecture().registers("general"):
|
|
val = pending_frame.read_register(reg)
|
|
# Having unavailable registers leads to a fall back to the standard
|
|
# unwinders. Don't add unavailable registers to avoid this.
|
|
if str(val) == "<unavailable>":
|
|
continue
|
|
unwinder.add_saved_register(reg, val)
|
|
return unwinder
|
|
|
|
@property
|
|
def enabled(self):
|
|
return self._enabled
|
|
|
|
@enabled.setter
|
|
def enabled(self, val):
|
|
if val:
|
|
self._enabled = False
|
|
self._update_frame_id_cache()
|
|
|
|
self._enabled = val
|
|
|
|
@property
|
|
def stop_at_level(self):
|
|
return self._stop_at_level
|
|
|
|
@stop_at_level.setter
|
|
def stop_at_level(self, val):
|
|
self._stop_at_level = val
|
|
gdb.invalidate_cached_frames()
|
|
|
|
|
|
global_unwinder = corrupt_stack_unwinder()
|
|
gdb.unwinder.register_unwinder(None, global_unwinder, True)
|