feat(core): add interactive console debugger
This commit is contained in:
1 parent
2484235f80
commit
096c95e2c9
1 file changed
+288
@@ -0,0 +1,288 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Chip8.Core;
|
||||
|
||||
public sealed class DebuggerConsole
|
||||
{
|
||||
const int defaultRunSteps = 1_000_000;
|
||||
|
||||
public DebuggerConsole(Debugger debugger)
|
||||
{
|
||||
this.debugger = debugger;
|
||||
}
|
||||
|
||||
public Debugger debugger { get; }
|
||||
|
||||
public bool quitRequested { get; private set; }
|
||||
|
||||
public string execute(string line)
|
||||
{
|
||||
string[] parts = line.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string command = parts[0].ToLowerInvariant();
|
||||
return command switch
|
||||
{
|
||||
"help" or "h" or "?" => help(),
|
||||
"s" or "step" => step(parts),
|
||||
"so" => stepOver(),
|
||||
"c" or "cont" or "continue" => run(parts),
|
||||
"u" or "until" => runUntil(parts),
|
||||
"b" or "break" => addBreakpoint(parts),
|
||||
"rb" or "delete" => removeBreakpoint(parts),
|
||||
"bl" => listBreakpoints(),
|
||||
"wm" => watchMemory(parts),
|
||||
"wr" => watchRegister(parts),
|
||||
"wl" => listWatches(),
|
||||
"wc" => clearWatches(),
|
||||
"r" or "regs" => registers(),
|
||||
"mem" or "x" => memory(parts),
|
||||
"dis" or "d" => disassemble(parts),
|
||||
"reset" => reset(),
|
||||
"q" or "quit" or "exit" => quit(),
|
||||
_ => "unknown command '" + command + "' (try 'help')",
|
||||
};
|
||||
}
|
||||
|
||||
static string help() => string.Join('\n', new[]
|
||||
{
|
||||
"commands:",
|
||||
" s [n] step n instructions (default 1)",
|
||||
" so step over a CALL",
|
||||
" c [n] continue up to n steps (default until stop)",
|
||||
" u <addr> run until pc reaches addr",
|
||||
" b <addr> add breakpoint",
|
||||
" rb <addr> remove breakpoint",
|
||||
" bl list breakpoints",
|
||||
" wm <addr> watch a memory byte",
|
||||
" wr <Vx> watch a register",
|
||||
" wl list watchpoints",
|
||||
" wc clear watchpoints",
|
||||
" r show registers",
|
||||
" mem <a> [n] hex dump n bytes from a (default 16)",
|
||||
" dis [n] disassemble n instructions from pc (default 8)",
|
||||
" reset reset the machine",
|
||||
" q quit",
|
||||
});
|
||||
|
||||
string step(string[] parts)
|
||||
{
|
||||
int count = optionalInt(parts, 1, 1);
|
||||
for (int taken = 0; taken < count && !debugger.machine.exited; taken++)
|
||||
{
|
||||
debugger.step();
|
||||
}
|
||||
return registers();
|
||||
}
|
||||
|
||||
string stepOver()
|
||||
{
|
||||
RunResult outcome = debugger.stepOver(defaultRunSteps);
|
||||
return describe(outcome) + "\n" + registers();
|
||||
}
|
||||
|
||||
string run(string[] parts)
|
||||
{
|
||||
int limit = optionalInt(parts, 1, defaultRunSteps);
|
||||
RunResult outcome = debugger.run(limit);
|
||||
return describe(outcome) + "\n" + registers();
|
||||
}
|
||||
|
||||
string runUntil(string[] parts)
|
||||
{
|
||||
if (!tryAddress(parts, out ushort address))
|
||||
{
|
||||
return "usage: u <addr>";
|
||||
}
|
||||
RunResult outcome = debugger.runUntil(address, defaultRunSteps);
|
||||
return describe(outcome) + "\n" + registers();
|
||||
}
|
||||
|
||||
string addBreakpoint(string[] parts)
|
||||
{
|
||||
if (!tryAddress(parts, out ushort address))
|
||||
{
|
||||
return "usage: b <addr>";
|
||||
}
|
||||
debugger.addBreakpoint(address);
|
||||
return "breakpoint set at 0x" + address.ToString("X4", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
string removeBreakpoint(string[] parts)
|
||||
{
|
||||
if (!tryAddress(parts, out ushort address))
|
||||
{
|
||||
return "usage: rb <addr>";
|
||||
}
|
||||
debugger.removeBreakpoint(address);
|
||||
return "breakpoint cleared at 0x" + address.ToString("X4", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
string listBreakpoints()
|
||||
{
|
||||
var addresses = new List<ushort>(debugger.breakpointAddresses);
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
return "no breakpoints";
|
||||
}
|
||||
addresses.Sort();
|
||||
var builder = new StringBuilder("breakpoints:");
|
||||
foreach (ushort address in addresses)
|
||||
{
|
||||
builder.Append(" 0x").Append(address.ToString("X4", CultureInfo.InvariantCulture));
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
string watchMemory(string[] parts)
|
||||
{
|
||||
if (!tryAddress(parts, out ushort address))
|
||||
{
|
||||
return "usage: wm <addr>";
|
||||
}
|
||||
debugger.watchMemory(address);
|
||||
return "watching memory 0x" + address.ToString("X4", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
string watchRegister(string[] parts)
|
||||
{
|
||||
if (parts.Length < 2 || !tryRegister(parts[1], out int register))
|
||||
{
|
||||
return "usage: wr <Vx>";
|
||||
}
|
||||
debugger.watchRegister(register);
|
||||
return "watching register V" + register.ToString("X1", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
string listWatches()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (ushort address in debugger.watchedMemory)
|
||||
{
|
||||
builder.Append("mem 0x").Append(address.ToString("X4", CultureInfo.InvariantCulture)).Append('\n');
|
||||
}
|
||||
foreach (int register in debugger.watchedRegisters)
|
||||
{
|
||||
builder.Append("reg V").Append(register.ToString("X1", CultureInfo.InvariantCulture)).Append('\n');
|
||||
}
|
||||
return builder.Length == 0 ? "no watchpoints" : builder.ToString().TrimEnd('\n');
|
||||
}
|
||||
|
||||
string clearWatches()
|
||||
{
|
||||
debugger.clearWatches();
|
||||
return "watchpoints cleared";
|
||||
}
|
||||
|
||||
string registers() => debugger.snapshot().format().TrimEnd('\n');
|
||||
|
||||
string memory(string[] parts)
|
||||
{
|
||||
if (!tryAddress(parts, out ushort address))
|
||||
{
|
||||
return "usage: mem <addr> [len]";
|
||||
}
|
||||
int length = optionalInt(parts, 2, 16);
|
||||
byte[] window = debugger.readMemory(address, length);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
for (int offset = 0; offset < length; offset += 16)
|
||||
{
|
||||
builder.Append("0x").Append(((address + offset) & 0xFFFF).ToString("X4", CultureInfo.InvariantCulture)).Append(':');
|
||||
for (int column = 0; column < 16 && offset + column < length; column++)
|
||||
{
|
||||
builder.Append(' ').Append(window[offset + column].ToString("X2", CultureInfo.InvariantCulture));
|
||||
}
|
||||
builder.Append('\n');
|
||||
}
|
||||
return builder.ToString().TrimEnd('\n');
|
||||
}
|
||||
|
||||
string disassemble(string[] parts)
|
||||
{
|
||||
int count = optionalInt(parts, 1, 8);
|
||||
var builder = new StringBuilder();
|
||||
foreach (DecodedInstruction line in debugger.disassembleWindow(count))
|
||||
{
|
||||
builder.Append(line.address == debugger.machine.pc ? "-> " : " ");
|
||||
builder.Append(line).Append('\n');
|
||||
}
|
||||
return builder.ToString().TrimEnd('\n');
|
||||
}
|
||||
|
||||
string reset()
|
||||
{
|
||||
debugger.machine.reset();
|
||||
return "machine reset";
|
||||
}
|
||||
|
||||
string quit()
|
||||
{
|
||||
quitRequested = true;
|
||||
return "bye";
|
||||
}
|
||||
|
||||
static string describe(RunResult outcome) => outcome.reason switch
|
||||
{
|
||||
StopReason.Breakpoint => "stopped at breakpoint 0x" + outcome.triggeredAddress.ToString("X4", CultureInfo.InvariantCulture) + " after " + outcome.steps + " steps",
|
||||
StopReason.Watchpoint => "stopped on watchpoint after " + outcome.steps + " steps",
|
||||
StopReason.Exited => "interpreter exited after " + outcome.steps + " steps",
|
||||
_ => "ran " + outcome.steps + " steps",
|
||||
};
|
||||
|
||||
static int optionalInt(string[] parts, int index, int fallback)
|
||||
{
|
||||
if (parts.Length <= index)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
return int.TryParse(parts[index], NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) ? value : fallback;
|
||||
}
|
||||
|
||||
static bool tryAddress(string[] parts, out ushort address)
|
||||
{
|
||||
address = 0;
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!tryParseNumber(parts[1], out int value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
address = (ushort)(value & 0xFFFF);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool tryRegister(string token, out int register)
|
||||
{
|
||||
register = 0;
|
||||
string trimmed = token.StartsWith("V", StringComparison.OrdinalIgnoreCase) ? token.Substring(1) : token;
|
||||
if (!int.TryParse(trimmed, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int value) || value is < 0 or > 0xF)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
register = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool tryParseNumber(string token, out int value)
|
||||
{
|
||||
string digits = token;
|
||||
if (digits.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
digits = digits.Substring(2);
|
||||
}
|
||||
else if (digits.StartsWith("$", StringComparison.Ordinal))
|
||||
{
|
||||
digits = digits.Substring(1);
|
||||
}
|
||||
return int.TryParse(digits, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user