microfmt
Zero-allocation C++ formatting for constrained environments
Loading...
Searching...
No Matches
fp_unwinder.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Jarosław Pelczar <jarek@jpelczar.com>
2//
3// SPDX-License-Identifier: BSD-2-Clause
4
5#pragma once
6
11#include "address_space.hpp"
12#include "dwarf_abi.hpp"
13#include "frame_pointer.hpp"
14#include "register_context.hpp"
15#include <cstdint>
16#include <tuple>
17
18namespace microfmt {
19
25template <typename AbiTraits> struct fp_unwinder_context {
28};
29
34template <typename AbiTraits> struct fp_unwinder_tag {};
35
36} // namespace microfmt
37
38template <typename AbiTraits> struct microfmt::frame_unwinder_traits<microfmt::fp_unwinder_tag<AbiTraits>> {
43
60 static bool step(value_ref<const context_type> context, register_context_ref reg_ctx, uintptr_t /*current_pc*/,
61 uintptr_t &next_fp, uintptr_t &next_pc) noexcept {
62 if (!reg_ctx)
63 return false;
64
65 // Read current frame pointer dynamically from register context using
66 // AbiTraits. On architectures whose frame-pointer register is fixed
67 // (all but ARM), this is just AbiTraits::fp_reg; on ARM it depends on
68 // the live ARM/Thumb instruction-set state (see
69 // arm_abi_traits::resolve_fp_reg).
70 const uint32_t fp_reg = resolve_fp_register<AbiTraits>(reg_ctx);
71 typename AbiTraits::register_type raw_fp = 0;
72 if (!reg_ctx.read_raw(fp_reg, &raw_fp, AbiTraits::pointer_size)) {
73 return false;
74 }
75
76 uintptr_t current_fp = static_cast<uintptr_t>(raw_fp);
77 if (current_fp == 0)
78 return false;
79
80 constexpr size_t ptr_size = AbiTraits::pointer_size;
81
82 if ((current_fp % ptr_size) != 0)
83 return false;
84
85 // Resolve the FP-relative saved-FP/saved-RA slot offsets dynamically.
86 // On architectures whose FP-chain layout is fixed (all but ARM), this
87 // is just AbiTraits::fp_slot_offset/ra_slot_offset; on ARM it is only
88 // reliable in A32 state (see arm_abi_traits::resolve_frame_slot_offsets)
89 // -- fail cleanly in Thumb state instead of applying a plausible-looking
90 // but wrong offset.
91 ptrdiff_t fp_slot_offset = 0;
92 ptrdiff_t ra_slot_offset = 0;
93 if (!resolve_frame_slot_offsets<AbiTraits>(reg_ctx, fp_slot_offset, ra_slot_offset))
94 return false;
95
96 typename AbiTraits::register_type saved_fp = 0;
97 typename AbiTraits::register_type saved_ra = 0;
98
99 uintptr_t fp_addr = static_cast<uintptr_t>(static_cast<ptrdiff_t>(current_fp) + fp_slot_offset);
100 uintptr_t ra_addr = static_cast<uintptr_t>(static_cast<ptrdiff_t>(current_fp) + ra_slot_offset);
101
102 if (!context->space.read_bytes(fp_addr, &saved_fp, ptr_size))
103 return false;
104 if (!context->space.read_bytes(ra_addr, &saved_ra, ptr_size))
105 return false;
106
107 if (saved_fp <= current_fp || saved_ra == 0) {
108 return false;
109 }
110
111 // reg_ctx is always a mutable scratch snapshot (a dead-process register
112 // view or a local ucontext_t/GPR-array copy) when it supports writes at
113 // all, never a live process to resume -- so writing the recovered
114 // registers back into it is both safe and desirable: it is what lets a
115 // *subsequent* step() (whether another fp_unwinder step, or a different
116 // tier's step reading the same fp/ra registers) see the caller's frame
117 // instead of silently re-reading this frame's now-stale values (which
118 // would otherwise loop forever). This is best-effort: read-only register
119 // contexts (e.g. an immutable core-dump view) still let this single step
120 // succeed via the next_fp/next_pc outputs, they just can't chain further
121 // through this same tier.
122 std::ignore = reg_ctx.write_raw(fp_reg, &saved_fp, ptr_size);
123 std::ignore = reg_ctx.write_raw(AbiTraits::ra_reg, &saved_ra, ptr_size);
124
125 next_fp = static_cast<uintptr_t>(saved_fp);
126 next_pc = AbiTraits::normalize_pc(static_cast<uintptr_t>(saved_ra));
127 return true;
128 }
129};
Type-erased remote address-space access and remote string/object views.
Type-erased, two-word handle to a remote address space.
Definition address_space.hpp:204
Zero-allocation, type-erased handle for inspecting and modifying CPU registers from a frame context.
Definition register_context.hpp:118
Per-architecture ABI traits describing registers used during DWARF unwinding.
Type-erased frame unwinder handles, frame cursor iteration, and backtrace views.
Definition assert.hpp:19
Context used by a frame-pointer unwinder.
Definition fp_unwinder.hpp:25
address_space_ref space
Address space containing the stack to unwind.
Definition fp_unwinder.hpp:27
Tag selecting the frame-pointer unwinder for an ABI.
Definition fp_unwinder.hpp:34
static bool step(value_ref< const context_type > context, register_context_ref reg_ctx, uintptr_t, uintptr_t &next_fp, uintptr_t &next_pc) noexcept
Reads the caller frame record from the current frame pointer register.
Definition fp_unwinder.hpp:60
Static customization point describing a frame unwinder backend.
Definition frame_pointer.hpp:56