668bb0e9c534

Finish debugger first draft
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Mon, 02 Jan 2017 15:11:10 +0000
parents 963a991b4e63
children d5779be0dd59
branches/tags (none)
files content/blog/2017/01/chip8-debugging-infrastructure.markdown static/media/css/pygments-clean.css static/media/images/blog/2017/01/chip8-step.png

Changes

--- 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.
--- 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 */
Binary file static/media/images/blog/2017/01/chip8-step.png has changed