Skip to content
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ This creates a tmux session with editor, compiler, and miscellaneous panes for e

### Core Debugging Engine
- **Target**: Process control, breakpoint management, and binary inspection
- **Register Modification**: View and write register contents
- **Macho**: Mach-O parser supporting 64-bit architectures and byte swapping
- **Exception Ports**: Mach-based exception handling for traps and signals
- **ASLR**: Automatic slide detection for address resolution


## Project Status

This is an early-stage project under active development. The core expression evaluation system is functional, with binary analysis and other features in development.
Expand Down
28 changes: 28 additions & 0 deletions src/cmd/callable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
#include <format>
#include <vector>

#include "cmd/subcommand.hpp"
#include "core/context.hpp"
#include "core/target.hpp"
#include "object.hpp"
#include "typedefs.hpp"

class Interpreter;

Expand All @@ -32,4 +34,30 @@ struct std::formatter<T> { // NOLINT(cert-dcl58-cpp)
}
};

class SubcommandCallable : public Callable {
protected:
SubcommandHandler m_subcmds;

explicit SubcommandCallable(SubcommandHandler subcmds)
: m_subcmds(std::move(subcmds)) {}

public:
Object call(std::vector<Object> args) override {
if (args.empty()) return std::monostate{};
auto* subcmdStr = std::get_if<std::string>(&args.front());
if (subcmdStr == nullptr) return "Subcommand must be a string";
std::string subcmd = *subcmdStr;
args.erase(args.begin());
return m_subcmds.exec(subcmd, args);
}
};

inline FnPtr requiresRunningTarget(FnPtr fn) {
return [fn = std::move(fn)](const std::vector<Object>& args) -> Object {
auto& target = Context::getTarget();
if (!target || !target->m_started) return "Target is not running!";
return fn(args);
};
}

#endif
12 changes: 12 additions & 0 deletions src/cmd/scanner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ void Scanner::string() {
}

void Scanner::number() {
// Check for hex prefix
if (peek() == 'x' || peek() == 'X') {
advance();
while (isxdigit(peek()) != 0) {
advance();
}
const std::string text = m_source.substr(m_start, m_current - m_start);
auto value = static_cast<double>(std::stoull(text, nullptr, 16));
addToken(TokenType::NUMBER, value);
return;
}

while (isdigit(peek()) != 0) {
advance();
}
Expand Down
206 changes: 105 additions & 101 deletions src/cmd/stdlib.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
#include "core/target.hpp"
#include "error.hpp"
#include "expected.hpp"
#include "subcommand.hpp"
#include "token_type.hpp"
#include "typedefs.hpp"

Expand Down Expand Up @@ -49,14 +48,9 @@ class PrintFn : public Callable {
Object call(std::vector<Object> args) override { return args[0]; }
};

class BreakpointFn : public Callable {
class BreakpointFn : public SubcommandCallable {
private:
static Object rmOrToggleBreakpoint(const std::vector<std::string>& args,
bool toggle = false) {
Expected<u64, std::string> addrRes = detail::strToAddr(args[0]);
if (!addrRes) return addrRes.error();
u64 addr = *addrRes;

static Object rmOrToggleBreakpoint(u64 addr, bool toggle = false) {
auto& target = Context::getTarget();
auto& bps = target->getRegisteredBreakpoints();

Expand All @@ -74,65 +68,61 @@ class BreakpointFn : public Callable {
return std::format("Removed breakpoint at {}", addr);
}

FnPtr list = [](const std::vector<std::string>& args) -> Object {
static inline FnPtr list =
requiresRunningTarget([](const std::vector<Object>& args) -> Object {
#pragma unused(args)
auto& breakpoints = Context::getTarget()->getRegisteredBreakpoints();
std::string retStr{};

if (breakpoints.empty()) return "No breakpoints set!";

for (const auto& [k, v] : breakpoints) {
retStr += std::format("Breakpoint @ {} ({})\n", detail::toHex(k),
breakpoints[k].enabled);
}

retStr.pop_back();
return retStr;
};

FnPtr set = [](const std::vector<std::string>& args) -> Object {
Expected<u64, std::string> addrRes = detail::strToAddr(args[0]);
if (!addrRes) return addrRes.error();
const u64 addr = *addrRes;

const i32 bpRes = Context::getTarget()->setBreakpoint(addr);
if (bpRes != 0)
return "Error setting breakpoint!";
else
return std::format("Breakpoint set at: {}", detail::toHex(addr));
};

FnPtr remove = [](const std::vector<std::string>& args) -> Object {
return BreakpointFn::rmOrToggleBreakpoint(args, false);
};

FnPtr toggle = [](const std::vector<std::string>& args) -> Object {
return BreakpointFn::rmOrToggleBreakpoint(args, true);
};

SubcommandHandler m_subcmds{{{sv("list"), list},
{sv("set"), set},
{sv("remove"), remove},
{sv("toggle"), toggle}},
"breakpoint"};
auto& breakpoints = Context::getTarget()->getRegisteredBreakpoints();
std::string retStr{};

if (breakpoints.empty()) return "No breakpoints set!";

for (const auto& [k, v] : breakpoints) {
retStr += std::format("Breakpoint @ {} ({})\n", detail::toHex(k),
breakpoints[k].enabled);
}

retStr.pop_back();
return retStr;
});

static inline FnPtr set =
requiresRunningTarget([](const std::vector<Object>& args) -> Object {
auto addr = detail::asU64(args.front());
if (!addr) return addr.error();

const i32 bpRes = Context::getTarget()->setBreakpoint(*addr);
if (bpRes != 0)
return "Error setting breakpoint!";
else
return std::format("Breakpoint set at: {}", detail::toHex(*addr));
});

static inline FnPtr remove =
requiresRunningTarget([](const std::vector<Object>& args) -> Object {
auto addr = detail::asU64(args.front());
if (!addr) return addr.error();
return BreakpointFn::rmOrToggleBreakpoint(*addr, false);
});

static inline FnPtr toggle =
requiresRunningTarget([](const std::vector<Object>& args) -> Object {
auto addr = detail::asU64(args.front());
if (!addr) return addr.error();
return BreakpointFn::rmOrToggleBreakpoint(*addr, true);
});

public:
[[nodiscard]] int arity() const override { return 1; }
[[nodiscard]] std::string str() const override {
return "<native fn: breakpoint>";
}

Object call(std::vector<Object> args) override {
if (m_target == nullptr) {
CoreError::error("Target is not running!");
return std::monostate{};
}
std::vector<std::string> convertedArgs = detail::convertToStr(args);
if (convertedArgs.empty()) return std::monostate{};
const std::string subcmd = convertedArgs.front();
convertedArgs.erase(convertedArgs.begin());
return m_subcmds.exec(subcmd, convertedArgs);
}
BreakpointFn()
: SubcommandCallable({{{sv("list"), list},
{sv("set"), set},
{sv("remove"), remove},
{sv("toggle"), toggle}},
"breakpoint"}) {}
};

class RunFn : public Callable {
Expand Down Expand Up @@ -221,75 +211,89 @@ class ContinueFn : public Callable {
}
};

class TargetFn : public Callable {
class TargetFn : public SubcommandCallable {
private:
FnPtr info = [](const std::vector<std::string>& args) -> Object {
static inline FnPtr info = [](const std::vector<Object>& args) -> Object {
#pragma unused(args)
auto& target = Context::getTarget();
if (!target) return "Target not set!";
return target->getInfo();
};

FnPtr set = [](const std::vector<std::string>& args) -> Object {
auto& target = Context::getTarget();
if (target) {
if (target->getTargetState() == TargetState::RUNNING ||
target->getTargetState() == TargetState::STOPPED)
return "Cannot set new target whilst one is running!";
}
static inline FnPtr set =
requiresRunningTarget([](const std::vector<Object>& args) -> Object {
auto& target = Context::getTarget();

if (Target::isFileValid(args[0])) {
Context::setTarget(Target::create(args[0]));
return std::format("Target: {}", args[0]);
}
auto newTarget = detail::asString(args.front());
if (newTarget) return newTarget.error();

return std::format("{} is not a valid target path!", args[0]);
};
if (Target::isFileValid(*newTarget)) {
Context::setTarget(Target::create(*newTarget));
return std::format("Target: {}", *newTarget);
}

SubcommandHandler m_subcmds{{{sv("info"), info}, {sv("set"), set}}, "target"};
return std::format("{} is not a valid target path!", *newTarget);
});

public:
[[nodiscard]] int arity() const override { return 1; }
[[nodiscard]] std::string str() const override {
return "<native fn: target>";
}

Object call(std::vector<Object> args) override {
std::vector<std::string> convertedArgs = detail::convertToStr(args);
if (convertedArgs.empty()) return std::monostate{};
const std::string subcmd = convertedArgs.front();
convertedArgs.erase(convertedArgs.begin());
return m_subcmds.exec(subcmd, convertedArgs);
TargetFn()
: SubcommandCallable({{{sv("info"), info}, {sv("set"), set}}, "target"}) {
}
};

class RegisterFn : public Callable {
class RegisterFn : public SubcommandCallable {
private:
FnPtr view = [](const std::vector<std::string>& args) -> Object {
auto& target = Context::getTarget();
if (!target && !target->m_started) return "Target is not running!";
if (args.front() == "all")
return target->formatRegisterOutput(&target->getLastKnownThreadState());
auto res = findRegEntry(args.front());
if (!res.hasValue()) return res.error();
return std::format("{}: {}", args.front(),
detail::toHex(readRegValue(
target->getLastKnownThreadState(), *res.value())));
};
SubcommandHandler m_subcmds{{{sv("view"), view}}, "register"};
static inline FnPtr view =
requiresRunningTarget([](const std::vector<Object>& args) -> Object {
auto& target = Context::getTarget();

auto reg = detail::asString(args.front());
if (*reg == "all")
return target->formatRegisterOutput(
&target->getLastKnownThreadState());

auto entry = findRegEntry(*reg);
if (!entry) return entry.error();

return std::format(
"{}: {}", *reg,
detail::toHex(readRegValue(target->getLastKnownThreadState(),
*entry.value())));
});

static inline FnPtr write =
requiresRunningTarget([](const std::vector<Object>& args) -> Object {
auto& target = Context::getTarget();

auto regName = detail::asString(args.front());
if (!regName) return regName.error();

auto entry = findRegEntry(*regName);
if (!entry) return entry.error();

auto val = detail::asU64(args[1]);
if (!val) return val.error();

if (target->writeRegValue(*entry.value(), *val) != 0)
return "Error writing to register!";

return std::format("{}: {}", *regName, *val);
});

public:
[[nodiscard]] int arity() const override { return 2; }
[[nodiscard]] std::string str() const override {
return "<native fn: register>";
}
Object call(std::vector<Object> args) override {
std::vector<std::string> convertedArgs = detail::convertToStr(args);
if (convertedArgs.empty()) return std::monostate{};
const std::string subcmd = convertedArgs.front();
convertedArgs.erase(convertedArgs.begin());
return m_subcmds.exec(subcmd, convertedArgs);
}

RegisterFn()
: SubcommandCallable(
{{{sv("view"), view}, {sv("write"), write}}, "register"}) {}
};

#endif
2 changes: 1 addition & 1 deletion src/cmd/subcommand.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class SubcommandHandler {
const std::string_view callee)
: m_subcommands(fns), m_callee(callee) {};

Object exec(const std::string& subcmd, const std::vector<std::string>& args) {
Object exec(const std::string& subcmd, const std::vector<Object>& args) {
if (m_subcommands.contains(subcmd))
return std::invoke(m_subcommands[subcmd], args);
CmdError::error(TokenType::IDENTIFIER,
Expand Down
29 changes: 29 additions & 0 deletions src/cmd/util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,42 @@
#include <vector>

#include "cmd/object.hpp"
#include "expected.hpp"
#include "token_type.hpp"
#include "typedefs.hpp"

namespace detail {
bool isop(char c);
TokenType ctoop(char c);
double parseNumber(const std::string& str);
std::vector<std::string> convertToStr(const std::vector<Object>& vec);

// Object extraction helpers
inline Expected<u64, std::string> asU64(const Object& obj) {
if (const auto* d = std::get_if<double>(&obj)) {
return static_cast<u64>(*d);
}
if (const auto* s = std::get_if<std::string>(&obj)) {
// Try parsing as hex/decimal
try {
if (s->starts_with("0x") || s->starts_with("0X")) {
return static_cast<u64>(std::stoull(*s, nullptr, 16));
}
return static_cast<u64>(std::stoull(*s, nullptr, 10));
} catch (...) {
return Unexpected{std::format("Cannot parse '{}' as number", *s)};
}
}
return Unexpected<std::string>{"Expected numeric value"};
}

inline Expected<std::string, std::string> asString(const Object& obj) {
if (const auto* s = std::get_if<std::string>(&obj)) {
return *s;
}
return Unexpected{"Expected string"};
}

} // namespace detail

#endif
Loading
Loading