| // Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "fake-step.h" | |
| #include <lib/zx/process.h> | |
| #include <lib/zx/result.h> | |
| #include <zircon/assert.h> | |
| #include <cinttypes> | |
| #include <utility> | |
| #include <hwreg/bitfields.h> | |
| // For RISC-V with no hardware single-step support, fake single-step by | |
| // decoding the instruction enough to set a breakpoint just after it. For | |
| // branches, set a breakpoint at its target. For conditional branches, set | |
| // breakpoints in both places. | |
| namespace captive_thread { | |
| namespace { | |
| // Replace the T at vaddr with new_value and return the old value. | |
| // This is used for the breakpoint insertion and removal. | |
| template <typename T> | |
| zx::result<T> ReplaceMemory(uintptr_t vaddr, const T& new_value) { | |
| T old_value; | |
| memcpy(&old_value, reinterpret_cast<const T*>(vaddr), sizeof(old_value)); | |
| size_t wrote = 0; | |
| zx_status_t status = | |
| zx::process::self()->write_memory(vaddr, &new_value, sizeof(new_value), &wrote); | |
| if (status != ZX_OK) { | |
| return zx::error{status}; | |
| } | |
| if (wrote != sizeof(new_value)) { | |
| return zx::error{ZX_ERR_IO}; | |
| } | |
| return zx::ok(old_value); | |
| } | |
| // Fetch the T at vaddr, for the instruction decoder. | |
| template <typename T> | |
| T Fetch(uintptr_t pc) { | |
| T insn; | |
| // The item is not necessarily naturally-aligned, so don't use T* in the | |
| // source type. In RVC, the 32-bit (non-C) instruction encoding can appear | |
| // aligned only to 16 bits. | |
| memcpy(&insn, reinterpret_cast<const std::byte*>(pc), sizeof(insn)); | |
| return insn; | |
| } | |
| // Helper function for the decoder. | |
| constexpr uint64_t Reg(const zx_riscv64_thread_state_general_regs_t& regs, uint32_t i) { | |
| std::span<const uint64_t, 32> x{®s.pc, 32}; | |
| return i == 0 ? 0 : x[i]; | |
| } | |
| template <uint8_t Bits> | |
| requires(Bits > 0 && Bits < 32) | |
| constexpr int32_t SignExtend(uint32_t x) { | |
| constexpr int kShift = 32 - Bits; | |
| return static_cast<int32_t>(x << kShift) >> kShift; | |
| } | |
| // This describes a 32-bit RISC-V instruction. It only has the formats that | |
| // are used for jump instructions. | |
| union RiscvInsn { | |
| uint32_t insn; | |
| DEF_SUBFIELD(insn, 6, 0, opcode); | |
| // jalr (jr) uses I format. | |
| struct { | |
| constexpr uintptr_t jump_target(const zx_riscv64_thread_state_general_regs_t& regs) const { | |
| return (Reg(regs, rs1()) + SignExtend<12>(imm())) & ~uintptr_t{1}; | |
| } | |
| uint32_t insn; | |
| DEF_SUBFIELD(insn, 31, 20, imm); | |
| DEF_SUBFIELD(insn, 19, 15, rs1); | |
| DEF_SUBFIELD(insn, 14, 12, funct3); | |
| DEF_SUBFIELD(insn, 11, 7, rd); | |
| DEF_SUBFIELD(insn, 6, 0, opcode); | |
| } i; | |
| // The conditional branches use B format. | |
| struct { | |
| constexpr uintptr_t jump_target(uintptr_t pc) const { return pc + SignExtend<13>(imm()); } | |
| constexpr uint32_t imm() const { | |
| return (imm12() << 12) | (imm11() << 11) | (imm10_5() << 5) | (imm4_1() << 1); | |
| } | |
| uint32_t insn; | |
| DEF_SUBFIELD(insn, 31, 31, imm12); | |
| DEF_SUBFIELD(insn, 30, 25, imm10_5); | |
| DEF_SUBFIELD(insn, 24, 20, rs2); | |
| DEF_SUBFIELD(insn, 19, 15, rs1); | |
| DEF_SUBFIELD(insn, 14, 12, funct3); | |
| DEF_SUBFIELD(insn, 11, 8, imm4_1); | |
| DEF_SUBFIELD(insn, 7, 7, imm11); | |
| DEF_SUBFIELD(insn, 6, 0, opcode); | |
| } b; | |
| // jal (j) uses J format. | |
| struct { | |
| constexpr uintptr_t jump_target(uintptr_t pc) const { return pc + SignExtend<21>(imm()); } | |
| constexpr uint32_t imm() const { | |
| return (imm20() << 20) | (imm19_12() << 12) | (imm11() << 11) | (imm10_1() << 1); | |
| } | |
| uint32_t insn; | |
| DEF_SUBFIELD(insn, 31, 31, imm20); | |
| DEF_SUBFIELD(insn, 30, 21, imm10_1); | |
| DEF_SUBFIELD(insn, 20, 20, imm11); | |
| DEF_SUBFIELD(insn, 19, 12, imm19_12); | |
| DEF_SUBFIELD(insn, 11, 7, rd); | |
| DEF_SUBFIELD(insn, 6, 0, opcode); | |
| } j; | |
| }; | |
| // This describes a 16-bit RISC-V extension C instruction. It only has the | |
| // formats that are used for jump instructions. | |
| union RiscvCInsn { | |
| uint16_t insn; | |
| DEF_SUBFIELD(insn, 1, 0, op); | |
| // c.jalr (c.jr) uses CR format. | |
| struct { | |
| constexpr uintptr_t jump_target(const zx_riscv64_thread_state_general_regs_t& regs) const { | |
| return Reg(regs, rs1()); | |
| } | |
| uint16_t insn; | |
| DEF_SUBFIELD(insn, 15, 12, funct4); | |
| DEF_SUBFIELD(insn, 11, 7, rs1); | |
| DEF_SUBFIELD(insn, 6, 2, rs2); | |
| DEF_SUBFIELD(insn, 1, 0, op); | |
| } cr; | |
| // c.beqz / c.bnez use CB format. | |
| struct { | |
| constexpr uintptr_t jump_target(uintptr_t pc) const { return pc + SignExtend<9>(imm()); } | |
| constexpr uint32_t imm() const { | |
| return (imm8() << 8) | (imm7_6() << 6) | (imm5() << 5) | (imm4_3() << 3) | (imm2_1() << 1); | |
| } | |
| uint16_t insn; | |
| DEF_SUBFIELD(insn, 15, 13, funct3); | |
| DEF_SUBFIELD(insn, 12, 12, imm8); | |
| DEF_SUBFIELD(insn, 11, 10, imm4_3); | |
| DEF_SUBFIELD(insn, 9, 7, rd); | |
| DEF_SUBFIELD(insn, 6, 5, imm7_6); | |
| DEF_SUBFIELD(insn, 4, 3, imm2_1); | |
| DEF_SUBFIELD(insn, 2, 2, imm5); | |
| DEF_SUBFIELD(insn, 1, 0, op); | |
| } cb; | |
| // c.j uses CJ format. | |
| struct { | |
| constexpr uintptr_t jump_target(uintptr_t pc) const { return pc + SignExtend<12>(imm()); } | |
| constexpr uint32_t imm() const { | |
| return (imm11() << 11) | (imm10() << 10) | (imm9_8() << 8) | (imm7() << 7) | (imm6() << 6) | | |
| (imm5() << 5) | (imm4() << 4) | (imm3_1() << 1); | |
| } | |
| uint16_t insn; | |
| DEF_SUBFIELD(insn, 15, 13, funct3); | |
| DEF_SUBFIELD(insn, 12, 12, imm11); | |
| DEF_SUBFIELD(insn, 11, 11, imm4); | |
| DEF_SUBFIELD(insn, 10, 9, imm9_8); | |
| DEF_SUBFIELD(insn, 8, 8, imm10); | |
| DEF_SUBFIELD(insn, 7, 7, imm6); | |
| DEF_SUBFIELD(insn, 6, 6, imm7); | |
| DEF_SUBFIELD(insn, 5, 3, imm3_1); | |
| DEF_SUBFIELD(insn, 2, 2, imm5); | |
| DEF_SUBFIELD(insn, 1, 0, op); | |
| } cj; | |
| }; | |
| // Any one instruction has at most two possible successor PCs. | |
| using Locations = cpp26::inplace_vector<uintptr_t, 2>; | |
| // A conditional branch has two successors unless they're identical. | |
| constexpr Locations Conditional(uintptr_t first, uintptr_t second) { | |
| return first == second ? Locations{first} : Locations{first, second}; | |
| } | |
| // Return one or both successor PCs for regs.pc by decoding the instruction. | |
| Locations Successors(const zx_riscv64_thread_state_general_regs_t& regs) { | |
| ZX_DEBUG_ASSERT_MSG(regs.pc % 2 == 0, ": PC=%#" PRIx64, regs.pc); | |
| const auto c_insn = Fetch<RiscvCInsn>(regs.pc); | |
| switch (c_insn.op()) { | |
| case 0b11: { // 32-bit-wide instruction | |
| const uintptr_t next = regs.pc + 4; | |
| const auto insn = Fetch<RiscvInsn>(regs.pc); | |
| switch (insn.opcode() >> 2) { | |
| case 0b11000: // B | |
| return Conditional(next, insn.b.jump_target(regs.pc)); | |
| case 0b11011: // jal (J) | |
| return {insn.j.jump_target(regs.pc)}; | |
| case 0b11001: // jalr (I) | |
| return {insn.i.jump_target(regs)}; | |
| default: | |
| return {next}; | |
| } | |
| } | |
| case 0b00: // C instructions, no branches here. | |
| break; | |
| case 0b01: // CJ / CB | |
| switch (c_insn.cj.funct3()) { | |
| case 0b101: // c.j | |
| return {c_insn.cj.jump_target(regs.pc)}; | |
| case 0b110: // c.beqz | |
| case 0b111: // c.bnez | |
| return Conditional(regs.pc + 2, c_insn.cb.jump_target(regs.pc)); | |
| default: | |
| break; | |
| } | |
| break; | |
| case 0b10: // CR | |
| if (c_insn.cr.funct4() == 0b1000 && c_insn.cr.rs1() != 0 && c_insn.cr.rs2() == 0) { | |
| // c.jr / c.jalr | |
| return {c_insn.cr.jump_target(regs)}; | |
| } | |
| break; | |
| default: | |
| std::unreachable(); | |
| } | |
| return {regs.pc + 2}; | |
| } | |
| } // namespace | |
| zx::result<> CaptiveThread::FakeStep::SetBreakpoints( | |
| const zx_riscv64_thread_state_general_regs_t& regs) { | |
| ZX_DEBUG_ASSERT(breakpoints_.empty()); | |
| for (auto location : Successors(regs)) { | |
| zx::result insn = ReplaceMemory(location, kBreakpointInsn); | |
| if (insn.is_error()) { | |
| return insn.take_error(); | |
| } | |
| breakpoints_.push_back({.location = location, .saved = *insn}); | |
| } | |
| return zx::ok(); | |
| } | |
| zx::result<> CaptiveThread::FakeStep::ClearBreakpoints() { | |
| zx::result<> result = zx::ok(); | |
| for (auto [location, saved] : std::exchange(breakpoints_, {})) { | |
| zx::result clear = ReplaceMemory(location, saved); | |
| if (clear.is_ok()) { | |
| ZX_DEBUG_ASSERT(*clear == kBreakpointInsn); | |
| } else if (result.is_ok()) { | |
| result = clear.take_error(); | |
| } | |
| } | |
| return result; | |
| } | |
| void CaptiveThread::FakeStep::FixupException( // | |
| CaptiveThread& thread, zx_exception_report_t& report) const { | |
| if (report.header.type != ZX_EXCP_SW_BREAKPOINT) { | |
| return; | |
| } | |
| zx::result regs = thread.Registers(); | |
| if (regs.is_error()) { | |
| return; | |
| } | |
| for (const auto& bkpt : breakpoints_) { | |
| if (regs->pc == bkpt.location) { | |
| report.header.type = kSingleStepException; | |
| return; | |
| } | |
| } | |
| } | |
| } // namespace captive_thread |