summaryrefslogblamecommitdiffstats
path: root/src/core/debugger/gdbstub.cpp
blob: ee759816584342f488c4405b069a4a91059b683f (plain) (tree)







































































































































































































































































































































































                                                                                                    

                                                                                      



















                                                                                                  
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <atomic>
#include <numeric>
#include <optional>
#include <thread>

#include <boost/asio.hpp>
#include <boost/process/async_pipe.hpp>

#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "core/arm/arm_interface.h"
#include "core/core.h"
#include "core/debugger/gdbstub.h"
#include "core/debugger/gdbstub_arch.h"
#include "core/hle/kernel/k_page_table.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_thread.h"
#include "core/loader/loader.h"
#include "core/memory.h"

namespace Core {

constexpr char GDB_STUB_START = '$';
constexpr char GDB_STUB_END = '#';
constexpr char GDB_STUB_ACK = '+';
constexpr char GDB_STUB_NACK = '-';
constexpr char GDB_STUB_INT3 = 0x03;
constexpr int GDB_STUB_SIGTRAP = 5;

constexpr char GDB_STUB_REPLY_ERR[] = "E01";
constexpr char GDB_STUB_REPLY_OK[] = "OK";
constexpr char GDB_STUB_REPLY_EMPTY[] = "";

GDBStub::GDBStub(DebuggerBackend& backend_, Core::System& system_)
    : DebuggerFrontend(backend_), system{system_} {
    if (system.CurrentProcess()->Is64BitProcess()) {
        arch = std::make_unique<GDBStubA64>();
    } else {
        arch = std::make_unique<GDBStubA32>();
    }
}

GDBStub::~GDBStub() = default;

void GDBStub::Connected() {}

void GDBStub::Stopped(Kernel::KThread* thread) {
    SendReply(arch->ThreadStatus(thread, GDB_STUB_SIGTRAP));
}

std::vector<DebuggerAction> GDBStub::ClientData(std::span<const u8> data) {
    std::vector<DebuggerAction> actions;
    current_command.insert(current_command.end(), data.begin(), data.end());

    while (current_command.size() != 0) {
        ProcessData(actions);
    }

    return actions;
}

void GDBStub::ProcessData(std::vector<DebuggerAction>& actions) {
    const char c{current_command[0]};

    // Acknowledgement
    if (c == GDB_STUB_ACK || c == GDB_STUB_NACK) {
        current_command.erase(current_command.begin());
        return;
    }

    // Interrupt
    if (c == GDB_STUB_INT3) {
        LOG_INFO(Debug_GDBStub, "Received interrupt");
        current_command.erase(current_command.begin());
        actions.push_back(DebuggerAction::Interrupt);
        SendStatus(GDB_STUB_ACK);
        return;
    }

    // Otherwise, require the data to be the start of a command
    if (c != GDB_STUB_START) {
        LOG_ERROR(Debug_GDBStub, "Invalid command buffer contents: {}", current_command.data());
        current_command.clear();
        SendStatus(GDB_STUB_NACK);
        return;
    }

    // Continue reading until command is complete
    while (CommandEnd() == current_command.end()) {
        const auto new_data{backend.ReadFromClient()};
        current_command.insert(current_command.end(), new_data.begin(), new_data.end());
    }

    // Execute and respond to GDB
    const auto command{DetachCommand()};

    if (command) {
        SendStatus(GDB_STUB_ACK);
        ExecuteCommand(*command, actions);
    } else {
        SendStatus(GDB_STUB_NACK);
    }
}

void GDBStub::ExecuteCommand(std::string_view packet, std::vector<DebuggerAction>& actions) {
    LOG_TRACE(Debug_GDBStub, "Executing command: {}", packet);

    if (packet.length() == 0) {
        SendReply(GDB_STUB_REPLY_ERR);
        return;
    }

    std::string_view command{packet.substr(1, packet.size())};

    switch (packet[0]) {
    case 'H': {
        Kernel::KThread* thread{nullptr};
        s64 thread_id{strtoll(command.data() + 1, nullptr, 16)};
        if (thread_id >= 1) {
            thread = GetThreadByID(thread_id);
        }

        if (thread) {
            SendReply(GDB_STUB_REPLY_OK);
            backend.SetActiveThread(thread);
        } else {
            SendReply(GDB_STUB_REPLY_ERR);
        }
        break;
    }
    case 'T': {
        s64 thread_id{strtoll(command.data(), nullptr, 16)};
        if (GetThreadByID(thread_id)) {
            SendReply(GDB_STUB_REPLY_OK);
        } else {
            SendReply(GDB_STUB_REPLY_ERR);
        }
        break;
    }
    case 'q':
        HandleQuery(command);
        break;
    case '?':
        SendReply(arch->ThreadStatus(backend.GetActiveThread(), GDB_STUB_SIGTRAP));
        break;
    case 'k':
        LOG_INFO(Debug_GDBStub, "Shutting down emulation");
        actions.push_back(DebuggerAction::ShutdownEmulation);
        break;
    case 'g':
        SendReply(arch->ReadRegisters(backend.GetActiveThread()));
        break;
    case 'G':
        arch->WriteRegisters(backend.GetActiveThread(), command);
        SendReply(GDB_STUB_REPLY_OK);
        break;
    case 'p': {
        const size_t reg{static_cast<size_t>(strtoll(command.data(), nullptr, 16))};
        SendReply(arch->RegRead(backend.GetActiveThread(), reg));
        break;
    }
    case 'P': {
        const auto sep{std::find(command.begin(), command.end(), '=') - command.begin() + 1};
        const size_t reg{static_cast<size_t>(strtoll(command.data(), nullptr, 16))};
        arch->RegWrite(backend.GetActiveThread(), reg, std::string_view(command).substr(sep));
        break;
    }
    case 'm': {
        const auto sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1};
        const size_t addr{static_cast<size_t>(strtoll(command.data(), nullptr, 16))};
        const size_t size{static_cast<size_t>(strtoll(command.data() + sep, nullptr, 16))};

        if (system.Memory().IsValidVirtualAddressRange(addr, size)) {
            std::vector<u8> mem(size);
            system.Memory().ReadBlock(addr, mem.data(), size);

            SendReply(Common::HexToString(mem));
        } else {
            SendReply(GDB_STUB_REPLY_ERR);
        }
        break;
    }
    case 'M': {
        const auto size_sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1};
        const auto mem_sep{std::find(command.begin(), command.end(), ':') - command.begin() + 1};

        const size_t addr{static_cast<size_t>(strtoll(command.data(), nullptr, 16))};
        const size_t size{static_cast<size_t>(strtoll(command.data() + size_sep, nullptr, 16))};

        const auto mem_substr{std::string_view(command).substr(mem_sep)};
        const auto mem{Common::HexStringToVector(mem_substr, false)};

        if (system.Memory().IsValidVirtualAddressRange(addr, size)) {
            system.Memory().WriteBlock(addr, mem.data(), size);
            system.InvalidateCpuInstructionCacheRange(addr, size);
            SendReply(GDB_STUB_REPLY_OK);
        } else {
            SendReply(GDB_STUB_REPLY_ERR);
        }
        break;
    }
    case 's':
        actions.push_back(DebuggerAction::StepThread);
        break;
    case 'C':
    case 'c':
        actions.push_back(DebuggerAction::Continue);
        break;
    case 'Z': {
        const auto addr_sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1};
        const size_t addr{static_cast<size_t>(strtoll(command.data() + addr_sep, nullptr, 16))};

        if (system.Memory().IsValidVirtualAddress(addr)) {
            replaced_instructions[addr] = system.Memory().Read32(addr);
            system.Memory().Write32(addr, arch->BreakpointInstruction());
            system.InvalidateCpuInstructionCacheRange(addr, sizeof(u32));

            SendReply(GDB_STUB_REPLY_OK);
        } else {
            SendReply(GDB_STUB_REPLY_ERR);
        }
        break;
    }
    case 'z': {
        const auto addr_sep{std::find(command.begin(), command.end(), ',') - command.begin() + 1};
        const size_t addr{static_cast<size_t>(strtoll(command.data() + addr_sep, nullptr, 16))};

        const auto orig_insn{replaced_instructions.find(addr)};
        if (system.Memory().IsValidVirtualAddress(addr) &&
            orig_insn != replaced_instructions.end()) {
            system.Memory().Write32(addr, orig_insn->second);
            system.InvalidateCpuInstructionCacheRange(addr, sizeof(u32));
            replaced_instructions.erase(addr);

            SendReply(GDB_STUB_REPLY_OK);
        } else {
            SendReply(GDB_STUB_REPLY_ERR);
        }
        break;
    }
    default:
        SendReply(GDB_STUB_REPLY_EMPTY);
        break;
    }
}

void GDBStub::HandleQuery(std::string_view command) {
    if (command.starts_with("TStatus")) {
        // no tracepoint support
        SendReply("T0");
    } else if (command.starts_with("Supported")) {
        SendReply("PacketSize=4000;qXfer:features:read+;qXfer:threads:read+;qXfer:libraries:read+");
    } else if (command.starts_with("Xfer:features:read:target.xml:")) {
        const auto offset{command.substr(30)};
        const auto amount{command.substr(command.find(',') + 1)};

        const auto offset_val{static_cast<u64>(strtoll(offset.data(), nullptr, 16))};
        const auto amount_val{static_cast<u64>(strtoll(amount.data(), nullptr, 16))};
        const auto target_xml{arch->GetTargetXML()};

        if (offset_val + amount_val > target_xml.size()) {
            SendReply("l" + target_xml.substr(offset_val));
        } else {
            SendReply("m" + target_xml.substr(offset_val, amount_val));
        }
    } else if (command.starts_with("Offsets")) {
        Loader::AppLoader::Modules modules;
        system.GetAppLoader().ReadNSOModules(modules);

        const auto main = std::find_if(modules.begin(), modules.end(),
                                       [](const auto& key) { return key.second == "main"; });
        if (main != modules.end()) {
            SendReply(fmt::format("TextSeg={:x}", main->first));
        } else {
            SendReply(fmt::format("TextSeg={:x}",
                                  system.CurrentProcess()->PageTable().GetCodeRegionStart()));
        }
    } else if (command.starts_with("fThreadInfo")) {
        // beginning of list
        const auto& threads = system.GlobalSchedulerContext().GetThreadList();
        std::vector<std::string> thread_ids;
        for (const auto& thread : threads) {
            thread_ids.push_back(fmt::format("{:x}", thread->GetThreadID()));
        }
        SendReply(fmt::format("m{}", fmt::join(thread_ids, ",")));
    } else if (command.starts_with("sThreadInfo")) {
        // end of list
        SendReply("l");
    } else if (command.starts_with("Xfer:threads:read")) {
        std::string buffer;
        buffer += R"(l<?xml version="1.0"?>)";
        buffer += "<threads>";

        const auto& threads = system.GlobalSchedulerContext().GetThreadList();
        for (const auto& thread : threads) {
            buffer +=
                fmt::format(R"(<thread id="{:x}" core="{:d}" name="Thread {:d}"/>)",
                            thread->GetThreadID(), thread->GetActiveCore(), thread->GetThreadID());
        }

        buffer += "</threads>";
        SendReply(buffer);
    } else {
        SendReply(GDB_STUB_REPLY_EMPTY);
    }
}

Kernel::KThread* GDBStub::GetThreadByID(u64 thread_id) {
    const auto& threads{system.GlobalSchedulerContext().GetThreadList()};
    for (auto* thread : threads) {
        if (thread->GetThreadID() == thread_id) {
            return thread;
        }
    }

    return nullptr;
}

std::vector<char>::const_iterator GDBStub::CommandEnd() const {
    // Find the end marker
    const auto end{std::find(current_command.begin(), current_command.end(), GDB_STUB_END)};

    // Require the checksum to be present
    return std::min(end + 2, current_command.end());
}

std::optional<std::string> GDBStub::DetachCommand() {
    // Slice the string part from the beginning to the end marker
    const auto end{CommandEnd()};

    // Extract possible command data
    std::string data(current_command.data(), end - current_command.begin() + 1);

    // Shift over the remaining contents
    current_command.erase(current_command.begin(), end + 1);

    // Validate received command
    if (data[0] != GDB_STUB_START) {
        LOG_ERROR(Debug_GDBStub, "Invalid start data: {}", data[0]);
        return std::nullopt;
    }

    u8 calculated = CalculateChecksum(std::string_view(data).substr(1, data.size() - 4));
    u8 received = static_cast<u8>(strtoll(data.data() + data.size() - 2, nullptr, 16));

    // Verify checksum
    if (calculated != received) {
        LOG_ERROR(Debug_GDBStub, "Checksum mismatch: calculated {:02x}, received {:02x}",
                  calculated, received);
        return std::nullopt;
    }

    return data.substr(1, data.size() - 4);
}

u8 GDBStub::CalculateChecksum(std::string_view data) {
    return std::accumulate(data.begin(), data.end(), u8{0},
                           [](u8 lhs, u8 rhs) { return static_cast<u8>(lhs + rhs); });
}

void GDBStub::SendReply(std::string_view data) {
    const auto output{
        fmt::format("{}{}{}{:02x}", GDB_STUB_START, data, GDB_STUB_END, CalculateChecksum(data))};
    LOG_TRACE(Debug_GDBStub, "Writing reply: {}", output);

    // C++ string support is complete rubbish
    const u8* output_begin = reinterpret_cast<const u8*>(output.data());
    const u8* output_end = output_begin + output.size();
    backend.WriteToClient(std::span<const u8>(output_begin, output_end));
}

void GDBStub::SendStatus(char status) {
    std::array<u8, 1> buf = {static_cast<u8>(status)};
    LOG_TRACE(Debug_GDBStub, "Writing status: {}", status);
    backend.WriteToClient(buf);
}

} // namespace Core