mikage-dev/source/interpreter.h
2024-03-08 10:54:13 +01:00

442 lines
13 KiB
C++

#pragma once
#include "arm.h"
#include "memory.h"
#include <spdlog/sinks/sink.h>
#include <atomic>
#include <condition_variable>
#include <cstring>
#include <list>
#include <map>
#include <mutex>
#include <optional>
#include <fstream>
// TODO: Shouldn't need to include this header...
#include "loader/gamecard.hpp"
namespace HLE {
namespace OS {
class OS;
}
}
namespace Profiler {
class Profiler;
}
namespace Debugger {
class DebugServer;
}
struct KeyDatabase;
class LogManager;
namespace Interpreter {
struct Breakpoint {
uint32_t address;
bool operator==(const Breakpoint& bp) const {
return address == bp.address;
}
};
struct AttachInfo {
uint32_t pid;
};
struct WrappedAttachInfo {
// Waits until some other thread has successfully signalled request_continue via TryAccess
void AwaitProcessing() {
std::unique_lock<std::mutex> lock(condvar_mutex);
request_continue.wait(lock);
}
std::mutex condvar_mutex;
std::unique_ptr<AttachInfo> data;
private:
std::mutex access_mutex;
std::condition_variable request_continue;
friend bool TryAccess(std::weak_ptr<WrappedAttachInfo> attach_info_weak, std::function<bool(const AttachInfo&)> eval);
};
/**
* Tries to unwrap the AttachInfo data stored in the given weak_ptr and passes
* it to the given callback on success.
* @param eval Callback that should return true if the AttachInfo should be
* permanently consumed (allowing the main thread to continue)
* @return true if and only if the AttachInfo was consumed
* @todo Define this elsewhere
*/
inline bool TryAccess(std::weak_ptr<WrappedAttachInfo> attach_info_weak, std::function<bool(const AttachInfo&)> eval) {
auto attach_info_shared = attach_info_weak.lock();
if (!attach_info_shared)
return false;
std::lock_guard<std::mutex> guard(attach_info_shared->access_mutex);
auto* attach_info = attach_info_shared->data.get();
if (!attach_info)
return false;
if (!eval(*attach_info))
return false;
// Drop the original data and signal the main thread to continue
// Note that we lock the mutex here purely to make sure we don't signal the condition variable before it is being waited for
attach_info_shared->data = nullptr;
std::lock_guard<std::mutex>(attach_info_shared->condvar_mutex);
attach_info_shared->request_continue.notify_all();
return true;
}
// Controls thread-safe stepping/running/pausing/quitting of a Processor
class ProcessorController {
// Handlers called from ProcessEventQueue when a segfault has been reported
virtual void OnSegfault(uint32_t process_id, uint32_t thread_id) = 0;
virtual void OnBreakpoint(uint32_t process_id, uint32_t thread_id) = 0;
virtual void OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) = 0;
virtual void OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) = 0;
virtual void OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) = 0;
protected:
public: // TODO: Un-public-ize!
std::atomic<bool> request_pause{false};
// std::atomic<bool> paused{true};
std::atomic<bool> paused{false};
std::atomic<bool> request_continue{false};
protected:
/**
* If valid, this denotes the process-thread-id of the thread that should
* be paused. If boost::any is used for the thread-id (second element), any
* thread of the given process is paused.
*/
std::optional<std::pair<uint32_t, std::optional<uint32_t>>> thread_to_pause;
friend class Processor;
struct Event {
enum class Type {
Segfault,
Breakpoint,
ProcessLaunched,
ReadWatchpoint,
WriteWatchpoint,
} type;
uint32_t process_id;
uint32_t thread_id;
union {
uint32_t launched_process_id;
uint32_t watchpoint_data_address;
};
};
std::list<Event> events;
void ProcessEventQueue() {
while (!events.empty()) {
auto& event = events.front();
switch (event.type) {
case Event::Type::Segfault:
OnSegfault(event.process_id, event.thread_id);
break;
case Event::Type::Breakpoint:
OnBreakpoint(event.process_id, event.thread_id);
break;
case Event::Type::ProcessLaunched:
OnProcessLaunched(event.process_id, event.thread_id, event.launched_process_id);
break;
case Event::Type::ReadWatchpoint:
OnReadWatchpoint(event.process_id, event.thread_id, event.watchpoint_data_address);
break;
case Event::Type::WriteWatchpoint:
OnWriteWatchpoint(event.process_id, event.thread_id, event.watchpoint_data_address);
break;
default:
// Do nothing
;
}
events.pop_front();
}
}
public:
virtual ~ProcessorController() = default;
// TODO: This is probably a mess and full of race conditions... :(
// TODO: Need functions to signal quit requests
/**
* Request the emulation core to be paused and busy-waits until the request has been fulfilled.
* @note The amount of CPU instructions processed between the request of the pause and the actual pausing is undefined.
*/
void RequestPause() {
// TODO: Get rid of this! Currently, we need it in case automated stepping encounters a breakpoint..
if (paused)
return;
assert(!paused);
thread_to_pause = std::nullopt;
request_pause = true;
while (!paused) {
}
request_pause = false;
assert(paused);
}
// Request emulation to be continued without any planned pause in the future. Does not return before emulation has continued.
void RequestContinue() {
assert(paused);
request_continue = true;
while (paused) {};
request_continue = false;
assert(!paused);
}
// Request emulation to be continued and pause after the next instruction. Does not return before emulation has continued and stopped again.
void RequestStep(std::optional<std::pair<uint32_t, std::optional<uint32_t>>> process_thread_id = std::nullopt) {
assert(paused);
thread_to_pause = process_thread_id;
request_pause = true;
request_continue = true;
while (paused) {};
request_continue = false;
while (!paused) {};
request_pause = false;
assert(paused);
}
bool ShouldPause(uint32_t process_id, uint32_t thread_id) const {
if (!request_pause)
return false;
if (thread_to_pause == std::nullopt)
return true;
if (thread_to_pause->first != process_id)
return false;
return thread_to_pause->second.value_or(thread_id) == thread_id;
}
// Use this in the Processor when an internal (unrecoverable) error happens
void NotifySegfault(uint32_t process_id, uint32_t thread_id) {
events.push_back({Event::Type::Segfault, process_id, thread_id});
}
void NotifyBreakpoint(uint32_t process_id, uint32_t thread_id) {
events.push_back({Event::Type::Breakpoint, process_id, thread_id});
}
void NotifyReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) {
events.push_back({Event::Type::ReadWatchpoint, process_id, thread_id, address});
}
void NotifyWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) {
events.push_back({Event::Type::WriteWatchpoint, process_id, thread_id, address});
}
void NotifyProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) {
events.push_back({Event::Type::ProcessLaunched, process_id, thread_id, launched_process_id});
}
virtual void OfferAttach(std::weak_ptr<WrappedAttachInfo> attach_info) = 0;
};
/// Trivial ProcessorController with debugging features disabled
class DummyController final : public ProcessorController {
void OnSegfault(uint32_t process_id, uint32_t thread_id) override {
}
void OnBreakpoint(uint32_t process_id, uint32_t thread_id) override {
}
void OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override {
}
void OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override {
}
void OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) override {
}
void OfferAttach(std::weak_ptr<WrappedAttachInfo> attach_info) override {
}
};
} // namespace Interpreter
namespace Interpreter {
struct Callsite {
uint32_t source;
uint32_t target;
ARM::State state;
};
struct Setup;
struct CPUContext;
// #define CONTROL_FLOW_LOGGING
struct ControlFlowLogger {
uint32_t indent = 0;
void Branch(CPUContext& ctx, const char* kind, uint32_t addr);
void Return(CPUContext& ctx, const char* kind);
void SVC(CPUContext& ctx, uint32_t id);
void Log(CPUContext& ctx, const std::string& str);
private:
std::string GetIndent();
void MakeSureFileIsOpen(CPUContext& ctx);
#ifdef CONTROL_FLOW_LOGGING
std::ofstream os;
#endif
};
// TODO: Move to private implementation
struct CPUContext {
CPUContext(HLE::OS::OS* os = nullptr, Setup* setup = nullptr);
~CPUContext();
ARM::State cpu{};
std::list<Breakpoint> breakpoints;
std::list<Breakpoint> read_watchpoints;
std::list<Breakpoint> write_watchpoints;
std::vector<Callsite> backtrace;
// This must be true for any of the debugging functionality to be enabled
bool debugger_attached = false;
bool trap_on_resume = false;
ControlFlowLogger cfl{};
void RecordCall(uint32_t source, uint32_t target, ARM::State state);
HLE::OS::OS* os{};
Setup* setup{};
ProcessorController* controller{};
};
// TODO: This should be somewhere stored in its own header! (need to provide external default destructor in Processor to enable that
// TODO: Rename to Emulator (and strip the CPUContext; they belong in the Processors' ExecutionContext instead, and those are created by OS)
struct Setup {
Setup(LogManager&, const KeyDatabase&,
std::unique_ptr<Loader::GameCard>, Profiler::Profiler&,
Debugger::DebugServer&);
~Setup();
CPUContext cpus[2];
Memory::PhysicalMemory mem;
std::unique_ptr<HLE::OS::OS> os;
const KeyDatabase& keydb;
std::unique_ptr<Loader::GameCard> gamecard = nullptr;
Profiler::Profiler& profiler;
Debugger::DebugServer& debug_server;
};
// TODO: Don't leak this implementation detail
void Step(struct ExecutionContext&); // NOTE: This is only for the Interpreter
class Processor;
struct ExecutionContext {
Processor& parent;
ExecutionContext(Processor& parent);
ExecutionContext(const ExecutionContext&) = delete;
ExecutionContext& operator=(const ExecutionContext&) = delete;
ExecutionContext(const ExecutionContext&&) = delete;
ExecutionContext& operator=(ExecutionContext&&) = delete;
// Automatically unregisters this context from its parent Processor
virtual ~ExecutionContext();
// TODO: Provide generic accessors instead
virtual ARM::State ToGenericContext() = 0;
virtual void FromGenericContext(const ARM::State&) = 0;
/**
* Enables ProcessorController to take control over the thread corresponding to this context.
*/
virtual void SetDebuggingEnabled(bool enabled = true) { (void)enabled; }
virtual bool IsDebuggingEnabled() const { return false; }
};
class Processor {
friend struct ExecutionContext;
virtual ExecutionContext* CreateExecutionContextImpl() = 0;
void UnregisterContext(ExecutionContext&);
protected:
std::vector<ExecutionContext*> contexts;
public:
virtual ~Processor() = default;
virtual void WriteVirtualMemory8(uint32_t virt_address, const uint8_t value) = 0;
virtual void WriteVirtualMemory16(uint32_t virt_address, const uint16_t value) = 0;
virtual void WriteVirtualMemory32(uint32_t virt_address, const uint32_t value) = 0;
virtual uint8_t ReadVirtualMemory8(uint32_t virt_address) = 0;
virtual uint16_t ReadVirtualMemory16(uint32_t virt_address) = 0;
virtual uint32_t ReadVirtualMemory32(uint32_t virt_address) = 0;
// Runs until shut down by controller
virtual void Run(ExecutionContext&, ProcessorController& controller, uint32_t process_id, uint32_t thread_id) = 0;
virtual void OnVirtualMemoryMapped(uint32_t phys_addr, uint32_t size, uint32_t vaddr) = 0;
virtual void OnVirtualMemoryUnmapped(uint32_t vaddr, uint32_t size) = 0;
std::unique_ptr<ExecutionContext> CreateExecutionContext() {
return std::unique_ptr<ExecutionContext> { CreateExecutionContextImpl() };
}
};
std::unique_ptr<Processor> CreateInterpreter(Setup& setup);
} // namespace Interpreter