# HG changeset patch # User Steve Losh # Date 1483369870 0 # Node ID 668bb0e9c534a838014c33cbf9d861beb1e2adf1 # Parent 963a991b4e631def9991d55d7072a04817a180c3 Finish debugger first draft diff -r 963a991b4e63 -r 668bb0e9c534 content/blog/2017/01/chip8-debugging-infrastructure.markdown --- a/content/blog/2017/01/chip8-debugging-infrastructure.markdown Mon Jan 02 13:12:41 2017 +0000 +++ b/content/blog/2017/01/chip8-debugging-infrastructure.markdown Mon Jan 02 15:11:10 2017 +0000 @@ -31,9 +31,9 @@ ## Architecture The overall goal will be to keep the debugging infrastructure as separate from -possible the rest of the emulation code. Unfortunately the nature of debugging -will require us to weave it into some of the normal emulator code, but we'll try -to keep the pollution to a minimum. +the rest of the emulation code as possible. Unfortunately the nature of +debugging will require us to weave it into *some* of the normal emulator code, +but we'll try to keep the pollution to a minimum. All information about the state of the debugger (e.g. breakpoints, pause status, etc) will be stored in a separate debugger data structure. We'll define a small @@ -43,8 +43,8 @@ ## The Debugger Data Structure -We'll start by creating a `debugger` struct and a `with-debugger` macro for it. -We'll be adding fields to this struct as we build the debugging infrastructure. +We'll start by creating a `debugger` struct and a `with-debugger` macro for it +and will add fields to this struct as we build the debugging infrastructure: ```lisp (defstruct debugger @@ -64,6 +64,9 @@ (debugger (make-debugger) :type debugger :read-only t)) ``` +It's read-only because we should never be swapping out a `chip`'s debugger as it +runs. + ## Pausing The first thing we'll add is support for pausing and unpausing execution. We'll @@ -71,12 +74,10 @@ ```lisp (defstruct debugger - (paused nil :type boolean) - ; ... - ) + (paused nil :type boolean)) ``` -Then we'll add some simple API functions so the emulator won't have to directly +Then we'll make some simple API functions so the emulator won't have to directly work with the debugger's slots: ```lisp @@ -97,8 +98,12 @@ (debugger-paused debugger)) ``` -Now we'll modify `emulate-cycle` to check if the debugger is paused before -actually running an instruction: +Now we need to start modifying the emulator itself to pause execution when the +debugger is paused. Unfortunately this is going to be pretty invasive, but +I don't think there's much of a way around that. + +We'll change `emulate-cycle` to check if the debugger is paused before actually +running an instruction: ```lisp (defun emulate-cycle (chip) @@ -153,20 +158,21 @@ ``` We'll also need a way to actually pause and unpause the debugger. We could do -it through NREPL or SLIME, but it'll be much easier if we just add a key for it -over on the Qt side of things: +it through NREPL or SLIME, but it'll be easier if we just add a key for it over +on the Qt side of things: ```lisp (define-override (screen key-release-event) (ev) (let* ((key (q+:key ev)) - (pad-key (pad-key-for key))) + (pad-key (pad-key-for key)) + (debugger (chip8::chip-debugger chip))) (if pad-key (chip8::keyup chip pad-key) (qtenumcase key ((q+:qt.key_escape) (die screen)) ((q+:qt.key_f1) (chip8::reset chip)) - ((q+:qt.key_space) ; NEW - (chip8::debugger-toggle-pause (chip8::debugger chip)) ; NEW + ; NEW + ((q+:qt.key_space) (chip8::debugger-toggle-pause debugger) (t (pr :unknown-key (format nil "~X" key)))))) (stop-overriding)) ``` @@ -176,31 +182,267 @@ ## Stepping +The next step is adding support for single-stepping through the code. We'll +start by adding a slot to the `debugger`: + +```lisp +(defstruct debugger + ; ... + (take-step nil :type boolean)) +``` + +Then we'll add an API function to set it: + +```lisp +(defun debugger-step (debugger) + (setf (debugger-take-step debugger) t)) +``` + +Now we need to wire this into the emulator, which is a bit tricky. We need to +handle the following cases: + +* The debugger isn't paused at all, just run normally. +* The debugger is paused, but `take-step` is false, so just wait. +* The debugger is paused and `take-step` is true, so take a single step and then + pause after that. + +We'll encapsulate this in a separate API function with a decidedly mediocre +name. `debugger-check-wait` will be run before each cycle, and will return `t` +whenever execution should wait. It will handle checking and updating +`take-step` as needed: + +```lisp +(defun debugger-check-wait (debugger) + "Return `t` if the debugger wants execution to wait, `nil` otherwise." + (with-debugger (debugger) + (cond + ;; If we're not paused, just run normally. + ((not paused) nil) + + ;; If we're paused, but are ready to step, run just this once. + (take-step (setf take-step nil) + nil) + + ;; Otherwise we're fully paused, so wait. + (t t)))) +``` + +We can wire this into `emulate-cycle`, replacing the vanilla +`debugger-paused-p`: + +```lisp +(defun emulate-cycle (chip) + (with-chip (chip) + (if (debugger-check-wait debugger) ; NEW + (sleep 10/1000) + (let ((instruction (cat-bytes + (aref memory program-counter) + (aref memory (1+ program-counter))))) + (zapf program-counter (chop 12 (+ % 2))) + (dispatch-instruction chip instruction))) + nil)) +``` + +Now pausing will work as before, but whenever we set `take-step` to `t` the CPU +will emulate one more cycle before going back to being paused. We can add a key +to the Qt screen to request a step: + +```lisp +(define-override (screen key-release-event) (ev) + (let* ((key (q+:key ev)) + (pad-key (pad-key-for key)) + (debugger (chip8::chip-debugger chip))) + (if pad-key + (chip8::keyup chip pad-key) + (qtenumcase key + ((q+:qt.key_escape) (die screen)) + ((q+:qt.key_f1) (chip8::reset chip)) + ; NEW + ((q+:qt.key_f7) (chip8::debugger-step debugger) + ((q+:qt.key_space) (chip8::debugger-toggle-pause debugger) + (t (pr :unknown-key (format nil "~X" key)))))) + (stop-overriding)) +``` + +Now we can walk through the code one instruction at a time by pausing the +emulator and hitting `F7` to take a step. This is great, but is pretty useless +unless we can also see what instruction is about to run. + ## Printing +Handling the printing is going to be ugly. We want to print the disassembly of +the current instruction whenever we "arrive" at a new instruction. This will +happen: + +* When we first pause the debugger. +* After a single step has been taken. + +We'll try to keep things as clean as possible on the emulator side of things by +containing all the ugliness in a `debugger-arrive` function, which we'll call at +the start of every possible cycle: + +```lisp +(defun emulate-cycle (chip) + (with-chip (chip) + (debugger-arrive debugger chip) ; NEW + (if (debugger-check-wait debugger program-counter) + (sleep 10/1000) + (let ((instruction (cat-bytes + (aref memory program-counter) + (aref memory (1+ program-counter))))) + (zapf program-counter (chop 12 (+ % 2))) + (dispatch-instruction chip instruction))) + nil)) +``` + +Now we can handle the ugliness on the debugger side. We'll add a slot for +tracking when we're waiting to arrive: + +```lisp +(defstruct debugger + ; ... + (awaiting-arrival nil :type boolean)) +``` + +Then we'll update our API to set this flag properly: + +```lisp +(defun debugger-pause (debugger) + (with-debugger (debugger) + (setf paused t + awaiting-arrival t))) ; NEW + +(defun debugger-unpause (debugger) + (with-debugger (debugger) + (setf paused nil + awaiting-arrival nil))) ; NEW + +(defun debugger-check-wait (debugger) + "Return `t` if the debugger wants execution to wait, `nil` otherwise." + (with-debugger (debugger) + (cond + ;; If we're not paused, just run normally. + ((not paused) nil) + + ;; If we're paused, but are ready to step, run just this once. + (take-step (setf take-step nil + awaiting-arrival t) ; NEW + nil) + + ;; Otherwise we're fully paused, so wait. + (t t)))) +``` + +Now we can implement `debugger-arrive`: + +```lisp +(defun debugger-arrive (debugger chip) + (with-debugger (debugger) + (when awaiting-arrival + (setf awaiting-arrival nil) + (debugger-print debugger chip)))) +``` + +Finally we can implement the actual instruction-printing function, which is +trivial thanks to the work we did in the previous post: + +```lisp +(defun debugger-print (debugger chip) + (declare (ignore debugger)) + (print-disassembled-instruction (chip-memory chip) + (chip-program-counter chip))) +``` + +Now we can run the emulator, press `space` to pause and we'll see the current +instruction (the one about to be executed) dumped to the console. We can press +`F7` to step one instruction at a time and they'll each be dumped in turn: + +[![Screenshot of CHIP-8 stepping](/media/images/blog/2017/01/chip8-step.png)](/media/images/blog/2017/01/chip8-step.png) + ## Breakpoints -## UI +Now that we can pause and step we can start tracking down bugs in the emulator. +We'll want to add breakpoints so that we don't need to manually step until we +get to a problematic instruction. + +We'll start by adding a slot to store the breakpoint addresses, as well as API +functions for adding and removing them: + +```lisp +(defstruct debugger + ; ... + (breakpoints nil :type list)) + +(defun debugger-add-breakpoint (debugger address) + (pushnew address (debugger-breakpoints debugger))) + +(defun debugger-remove-breakpoint (debugger address) + (removef (debugger-breakpoints debugger) address)) +``` + +`removef` is from Alexandria and is just a `modify-macro` for `remove`. + +Next we'll create a `debugger-check-breakpoints` function that will check +whether we're at a breakpoint, and pause the debugger if so. It will return `t` +if we're at a breakpoint: + +```lisp +(defun debugger-check-breakpoints (debugger address) + "Return `t` if the debugger is at a breakpoint, `nil` otherwise." + (if (member address (debugger-breakpoints debugger)) + (progn (debugger-pause debugger) + t) + nil)) +``` + +Note that we use our own `debugger-pause` API function here to make sure we +handle setting the `paused` and `awaiting-arrival` slots properly. Now we can +modify `debugger-check-wait` to use this function. We'll also need to update it +to take the current instruction's address: + +```lisp + ; NEW +(defun debugger-check-wait (debugger address) + "Return `t` if the debugger wants execution to wait, `nil` otherwise." + (with-debugger (debugger) + (cond + ;; If we're not paused, we might be at a breakpoint. ; NEW + ((not paused) (debugger-check-breakpoints debugger ; NEW + address)) ; NEW + + ;; If we're paused, but are ready to step, run just this once. + (take-step (setf take-step nil + awaiting-arrival t) + nil) + + ;; Otherwise we're fully paused, so wait. + (t t)))) +``` + +And that's it, nothing else needs to change. We can add breakpoints to the +currently running `chip` in the REPL with something like +`(debugger-add-breakpoint (chip-debugger *c*) #x200)`. This isn't the nicest +interface, but it'll do the job for now. ## Result +We've implemented basic pausing, single-stepping, and breakpoints. There are +a lot more features we could add to the debugger, and we'll cover some in later +posts. + +Creating a debugging interface isn't the most glamorous work, but it pays for +itself the first time you run into a bug in the emulator. + +I'm not entirely happy with all the coupling between the emulator and the +debugger, but I'm not sure it's possible to completely untangle the two. If +you've got suggestions for how to design the interface more cleanly please let +me know. + ## Future - -```lisp -``` - -```lisp -``` +We're nearing the end of this series, but there are a couple more things I'll +cover before it's over: -```lisp -``` - -```lisp -``` - -```lisp -``` - -```lisp -``` +* A graphical interface to the debugger and disassembler. +* Polishing up the interface a bit with menus for loading ROMs, configuring + options, etc. diff -r 963a991b4e63 -r 668bb0e9c534 static/media/css/pygments-clean.css --- a/static/media/css/pygments-clean.css Mon Jan 02 13:12:41 2017 +0000 +++ b/static/media/css/pygments-clean.css Mon Jan 02 15:11:10 2017 +0000 @@ -1,6 +1,8 @@ /* @override http://localhost:8080/media/css/pygments-monokai-light.css */ -div.highlight .hll { background-color: #49483e } +div.highlight .hll { background-color: #FFD7EF; display: block; } div.highlight .err { color: #fff; background-color: #f00 } /* Error */ +div.highlight .gi { font-weight: bold } /* Diff Insert */ +div.highlight .gd { font-weight: bold } /* Diff Delete */ div.highlight .k { color: #111} /* Keyword */ div.highlight .l { color: #111 } /* Literal */ div.highlight .n { color: #111 } /* Name */ diff -r 963a991b4e63 -r 668bb0e9c534 static/media/images/blog/2017/01/chip8-step.png Binary file static/media/images/blog/2017/01/chip8-step.png has changed