Work at SourceForge, help us to make it a better place! We have an immediate need for a Support Technician in our San Francisco or Denver office.

Close

Diff of /doc/cmucl/internals/debugger.tex [000000] .. [a530bb] Maximize Restore

  Switch to side-by-side view

--- a
+++ b/doc/cmucl/internals/debugger.tex
@@ -0,0 +1,537 @@
+%  					-*- Dictionary: design; Package: C -*-
+
+\#|
+\chapter{Debugger Information}
+\index{debugger information}
+\label{debug-info}
+
+Although the compiler's great freedom in choice of function call conventions
+and variable representations has major efficiency advantages, it also has
+unfortunate consequences for the debugger.  The debug information that we need
+is even more elaborate than for conventional "compiled" languages, since we
+cannot even do a simple backtrace without some debug information.  However,
+once having gone this far, it is not that difficult to go the extra distance,
+and provide full source level debugging of compiled code.
+
+Full debug information has a substantial space penalty, so we allow different
+levels of debug information to be specified.  In the extreme case, we can
+totally omit debug information.
+
+
+\section{The Debug-Info Structure}
+\index{debug-info structure}
+
+The Debug-Info structure directly represents information about the
+source code, and points to other structures that describe the layout of
+run-time data structures.
+
+
+Make some sort of minimal debug-info format that would support at least the
+common cases of level 1 (since that is what we would release), and perhaps
+level 0.  Actually, it seems it wouldn't be hard to crunch nearly all of the
+debug-function structure and debug-info function map into a single byte-vector.
+We could have an uncrunch function that restored the current format.  This
+would be used by the debugger, and also could be used by purify to delete parts
+of the debug-info even when the compiler dumps it in crunched form.
+[Note that this isn't terribly important if purify is smart about
+debug-info...]
+|\#
+
+
+Compiled source map representation:
+
+[\#\#\# store in debug-function PC at which env is properly initialized, i.e.
+args (and return-pc, etc.) in internal locations.  This is where a
+:function-start breakpoint would break.]
+
+[\#\#\# Note that that we can easily cache the form-number => source-path or
+form-number => form translation using a vector indexed by form numbers that we
+build during a walk.]
+
+
+
+
+Instead of using source paths in the debug-info, use "form numbers".  The form
+number of a form is the number of forms that we walk to reach that form when
+doing a pre-order walk of the source form.  [Might want to use a post-order
+walk, as that would more closely approximate evaluation order.]
+
+
+We probably want to continue using source-paths in the compiler, since they are
+quick to compute and to get you to a particular form.  [\#\#\# But actually, I
+guess we don't have to precompute the source paths and annotate nodes with
+them: instead we could annotate the nodes with the actual original source form.
+Then if we wanted to find the location of that form, we could walk the root
+source form, looking that original form.  But we might still need to enter all
+the forms in a hashtable so that we can tell during IR1 conversion that a given
+form appeared in the original source.]
+
+
+Note that form numbers have an interesting property: it is quite efficient to
+determine whether an arbitrary form is a subform of some other form, since the
+form number of B will be > than A's number and < A's next sibling's number iff
+B is a subform of A.  
+
+This should be quite useful for doing the source=>pc mapping in the debugger,
+since that problem reduces to finding the subset of the known locations that
+are for subforms of the specified form.
+
+
+Assume a byte vector with a standard variable-length integer format, something
+like this:
+    0..253 => the integer
+    254 => read next two bytes for integer
+    255 => read next four bytes for integer
+
+Then a compiled debug block is just a sequence of variable-length integers in a
+particular order, something like this:
+    number of successors
+    ...offsets of each successor in the function's blocks vector...
+    first PC
+    [offset of first top-level form (in forms) (only if not component default)]
+    form number of first source form
+    first live mask (length in bytes determined by number of VARIABLES)
+    ...more <PC, top-level form offset, form-number, live-set> tuples...
+
+We determine the number of locations recorded in a block by the finding the
+start of the next compiled debug block in the blocks vector.
+
+[\#\#\# Actually, only need 2 bits for number of successors {0,1,2}.  We might
+want to use other bits in the first byte to indicate the kind of location.]
+[\#\#\# We could support local packing by having a general concept of "alternate
+locations" instead of just regular and save locations.  The location would have
+a bit indicating that there are alternate locations, in which case we read the
+number of alternate locations and then that many more SC-OFFSETs.  In the
+debug-block, we would have a second bit mask with bits set for TNs that are in
+an alternate location.  We then read a number for each such TN, with the value
+being interpreted as an index into the Location's alternate locations.]
+
+
+
+It looks like using structures for the compiled-location-info is too bulky.
+Instead we need some packed binary representation.
+
+First, let's represent a SC/offset pair with an "SC-Offset", which is an
+integer with the SC in the low 5 bits and the offset in the remaining bits:
+    ----------------------------------------------------
+    | Offset (as many bits as necessary) | SC (5 bits) |
+    ----------------------------------------------------
+Probably the result should be constrained to fit in a fixnum, since it will be
+more efficient and gives more than enough possible offsets.
+
+We can the represent a compiled location like this:
+    single byte of boolean flags:
+	uninterned name
+	packaged name
+	environment-live
+	has distinct save location
+        has ID (name not unique in this fun)
+    name length in bytes (as var-length integer)
+    ...name bytes...
+    [if packaged, var-length integer that is package name length]
+     ...package name bytes...]
+    [If has ID, ID as var-length integer]
+    SC-Offset of primary location (as var-length integer)
+    [If has save SC, SC-Offset of save location (as var-length integer)]
+
+
+
+
+But for a whizzy breakpoint facility, we would need a good source=>code map.
+Dumping a complete code=>source map might be as good a way as any to represent
+this, due to the one-to-many relationship between source and code locations.
+
+We might be able to get away with just storing the source locations for the
+beginnings of blocks and maintaining a mapping from code ranges to blocks.
+This would be fine both for the profiler and for the "where am I running now"
+indication.  Users might also be convinced that it was most interesting to
+break at block starts, but I don't really know how easily people could develop
+an understanding of basic blocks.
+
+It could also be a bit tricky to map an arbitrary user-designated source
+location to some "closest" source location actually in the debug info.
+This problem probably exists to some degree even with a full source map, since
+some forms will never appear as the source of any node.  It seems you might
+have to negotiate with the user.  He would mouse something, and then you would
+highlight some source form that has a common prefix (i.e. is a prefix of the
+user path, or vice-versa.)  If they aren't happy with the result, they could
+try something else.  In some cases, the designated path might be a prefix of
+several paths.  This ambiguity might be resolved by picking the shortest path
+or letting the user choose.
+
+At the primitive level, I guess what this means is that the structure of source
+locations (i.e. source paths) must be known, and the source=>code operation
+should return a list of <source,code> pairs, rather than just a list of code
+locations.  This allows the debugger to resolve the ambiguity however it wants.
+
+I guess the formal definition of which source paths we would return is:
+    All source paths in the debug info that have a maximal common prefix with
+    the specified path.  i.e. if several paths have the complete specified path
+    as a prefix, we return them all.  Otherwise, all paths with an equally
+    large common prefix are returned: if the path with the most in common
+    matches only the first three elements, then we return all paths that match
+    in the first three elements.  As a degenerate case (which probably
+    shouldn't happen), if there is no path with anything in common, then we
+    return *all* of the paths.
+
+
+
+In the DEBUG-SOURCE structure we may ultimately want a vector of the start
+positions of each source form, since that would make it easier for the debugger
+to locate the source.  It could just open the file, FILE-POSITION to the form,
+do a READ, then loop down the source path.  Of course, it could read each form
+starting from the beginning, but that might be too slow.
+
+
+Do XEPs really need Debug-Functions?  The only time that we will commonly end
+up in the debugger on an XEP is when an argument type check fails.  But I
+suppose it would be nice to be able to print the arguments passed...
+
+
+Note that assembler-level code motion such as pipeline reorganization can cause
+problems with our PC maps.  The assembler needs to know that debug info markers
+are different from real labels anyway, so I suppose it could inhibit motion
+across debug markers conditional on policy.  It seems unworthwhile to remember
+the node for each individual instruction.
+
+
+For tracing block-compiled calls:
+    Info about return value passing locations?
+    Info about where all the returns are?
+
+We definitely need the return-value passing locations for debug-return.  The
+question is what the interface should be.  We don't really want to have a
+visible debug-function-return-locations operation, since there are various
+value passing conventions, and we want to paper over the differences.
+
+
+Probably should be a compiler option to initialize stack frame to a special
+uninitialized object (some random immediate type).  This would aid debugging,
+and would also help GC problems.  For the latter reason especially, this should
+be locally-turn-onable (off of policy?  the new debug-info quality?).
+
+
+What about the interface between the evaluator and the debugger? (i.e. what
+happens on an error, etc.)  Compiler error handling should be integrated with
+run-time error handling.  Ideally the error messages should look the same.
+Practically, in some cases the run-time errors will have less information.  But
+the error should look the same to the debugger (or at least similar).
+
+
+
+;;;; Debugger interface:
+
+How does the debugger interface to the "evaluator" (where the evaluator means
+all of native code, byte-code and interpreted IR1)?  It seems that it would be
+much more straightforward to have a consistent user interface to debugging
+all code representations if there was a uniform debugger interface to the
+underlying stuff, and vice-versa.  
+
+Of course, some operations might not be supported by some representations, etc.
+For example, fine-control stepping might not be available in native code.
+In other cases, we might reduce an operation to the lowest common denominator,
+for example fetching lexical variables by string and admitting the possibility
+of ambiguous matches.  [Actually, it would probably be a good idea to store the
+package if we are going to allow variables to be closed over.]
+
+Some objects we would need:
+Location:
+	The constant information about the place where a value is stored,
+        everything but which particular frame it is in.  Operations:
+        location name, type, etc.
+        location-value frame location (setf'able)
+	monitor-location location function
+            Function is called whenever location is set with the location,
+            frame and old value.  If active values aren't supported, then we
+            dummy the effect using breakpoints, in which case the change won't
+            be noticed until the end of the block (and intermediate changes
+            will be lost.)
+debug info:
+        All the debug information for a component.
+Frame:
+	frame-changed-locations frame => location*
+            Return a list of the locations in frame that were changed since the
+            last time this function was called.  Or something.  This is for
+            displaying interesting state changes at breakpoints.
+	save-frame-state frame => frame-state
+	restore-frame-state frame frame-state
+	    These operations allow the debugger to back up evaluation, modulo
+	    side-effects and non-local control transfers.  This copies and
+	    restores all variables, temporaries, etc, local to the frame, and
+	    also the current PC and dynamic environment (current catch, etc.)
+
+	    At the time of the save, the frame must be for the running function
+	    (not waiting for a call to return.)  When we restore, the frame
+	    becomes current again, effectively exiting from any frames on top.
+	    (Of course, frame must not already be exited.)
+       
+Thread:
+        Representation of which stack to use, etc.
+Block:
+        What successors the block has, what calls there are in the block.
+        (Don't need to know where calls are as long as we know called function,
+        since can breakpoint at the function.)  Whether code in this block is
+        wildly out of order due to being the result of loop-invariant
+        optimization, etc.  Operations:
+        block-successors block => code-location*
+        block-forms block => (source-location code-location)*
+            Return the corresponding source locations and code locations for
+            all forms (and form fragments) in the block.
+
+
+Variable maps:
+
+There are about five things that the debugger might want to know about a
+variable:
+
+    Name
+	Although a lexical variable's name is "really" a symbol (package and
+	all), in practice it doesn't seem worthwhile to require all the symbols
+	for local variable names to be retained.  There is much less VM and GC
+	overhead for a constant string than for a symbol.  (Also it is useful
+	to be able to access gensyms in the debugger, even though they are
+	theoretically ineffable).
+
+    ID
+	Which variable with the specified name is this?  It is possible to have
+	multiple variables with the same name in a given function.  The ID is
+	something that makes Name unique, probably a small integer.  When
+	variables aren't unique, we could make this be part of the name, e.g.
+	"FOO\#1", "FOO\#2".  But there are advantages to keeping this separate,
+	since in many cases lifetime information can be used to disambiguate,
+	making qualification unnecessary.
+
+    SC
+	When unboxed representations are in use, we must have type information
+	to properly read and write a location.  We only need to know the
+	SC for this, which would be amenable to a space-saving
+	numeric encoding.
+
+    Location
+	Simple: the offset in SC.  [Actually, we need the save location too.]
+
+    Lifetime
+	In what parts of the program does this variable hold a meaningful
+	value?  It seems prohibitive to record precise lifetime information,
+	both in space and compiler effort, so we will have to settle for some
+	sort of approximation.
+
+	The finest granularity at which it is easy to determine liveness is the
+	the block: we can regard the variable lifetime as the set of blocks
+	that the variable is live in.  Of course, the variable may be dead (and
+	thus contain meaningless garbage) during arbitrarily large portions of
+	the block.
+
+	Note that this subsumes the notion of which function a variable belongs
+	to.  A given block is only in one function, so the function is
+	implicit.
+
+
+The variable map should represent this information space-efficiently and with
+adequate computational efficiency.
+
+The SC and ID can be represented as small integers.  Although the ID can in
+principle be arbitrarily large, it should be <100 in practice.  The location
+can be represented by just the offset (a moderately small integer), since the
+SB is implicit in the SC.
+
+The lifetime info can be represented either as a bit-vector indexed by block
+numbers, or by a list of block numbers.  Which is more compact depends both on
+the size of the component and on the number of blocks the variable is live in.
+In the limit of large component size, the sparse representation will be more
+compact, but it isn't clear where this crossover occurs.  Of course, it would
+be possible to use both representations, choosing the more compact one on a
+per-variable basis.  Another interesting special case is when the variable is
+live in only one block: this may be common enough to be worth picking off,
+although it is probably rarer for named variables than for TNs in general.
+
+If we dump the type, then a normal list-style type descriptor is fine: the
+space overhead is small, since the shareability is high.
+
+We could probably save some space by cleverly representing the var-info as
+parallel vectors of different types, but this would be more painful in use.
+It seems better to just use a structure, encoding the unboxed fields in a
+fixnum.  This way, we can pass around the structure in the debugger, perhaps
+even exporting it from the the low-level debugger interface.
+
+[\#\#\# We need the save location too.  This probably means that we need two slots
+of bits, since we need the save offset and save SC.  Actually, we could let the
+save SC be implied by the normal SC, since at least currently, we always choose
+the same save SC for a given SC.  But even so, we probably can't fit all that
+stuff in one fixnum without squeezing a lot, so we might as well split and
+record both SCs.
+
+In a localized packing scheme, we would have to dump a different var-info
+whenever either the main location or the save location changes.  As a practical
+matter, the save location is less likely to change than the main location, and
+should never change without the main location changing.
+
+One can conceive of localized packing schemes that do saving as a special case
+of localized packing.  If we did this, then the concept of a save location
+might be eliminated, but this would require major changes in the IR2
+representation for call and/or lifetime info.  Probably we will want saving to
+continue to be somewhat magical.]
+
+
+How about:
+
+(defstruct var-info
+  ;;
+  ;; This variable's name. (symbol-name of the symbol)
+  (name nil :type simple-string)
+  ;;
+  ;; The SC, ID and offset, encoded as bit-fields.
+  (bits nil :type fixnum)
+  ;;
+  ;; The set of blocks this variable is live in.  If a bit-vector, then it has
+  ;; a 1 when indexed by the number of a block that it is live in.  If an
+  ;; I-vector, then it lists the live block numbers.  If a fixnum, then that is
+  ;; the number of the sole live block.
+  (lifetime nil :type (or vector fixnum))
+  ;;
+  ;; The variable's type, represented as list-style type descriptor.
+  type)
+
+Then the debug-info holds a simple-vector of all the var-info structures for
+that component.  We might as well make it sorted alphabetically by name, so
+that we can binary-search to find the variable corresponding to a particular
+name.
+
+We need to be able to translate PCs to block numbers.  This can be done by an
+I-Vector in the component that contains the start location of each block.  The
+block number is the index at which we find the correct PC range.  This requires
+that we use an emit-order block numbering distinct from the IR2-Block-Number,
+but that isn't any big deal.  This seems space-expensive, but it isn't too bad,
+since it would only be a fraction of the code size if the average block length
+is a few words or more.
+
+An advantage of our per-block lifetime representation is that it directly
+supports keeping a variable in different locations when in different blocks,
+i.e. multi-location packing.  We use a different var-info for each different
+packing, since the SC and offset are potentially different.  The Name and ID
+are the same, representing the fact that it is the same variable.  It is here
+that the ID is most significant, since the debugger could otherwise make
+same-name variables unique all by itself.
+
+
+
+Stack parsing:
+
+[\#\#\# Probably not worth trying to make the stack parseable from the bottom up.
+There are too many complications when we start having variable sized stuff on
+the stack.  It seems more profitable to work on making top-down parsing robust.
+Since we are now planning to wire the bottom-up linkage info, scanning from the
+bottom to find the top frame shouldn't be too inefficient, even when there was
+a runaway recursion.  If we somehow jump into hyperspace, then the debugger may
+get confused, but we can debug this sort of low-level system lossage using
+ADB.]
+
+
+There are currently three relevant context pointers:
+  -- The PC.  The current PC is wired (implicit in the machine).  A saved
+     PC (RETURN-PC) may be anywhere in the current frame.
+  -- The current stack context (CONT).  The current CONT is wired.  A saved
+     CONT (OLD-CONT) may be anywhere in the current frame.
+  -- The current code object (ENV).  The current ENV is wired.  When saved,
+     this is extra-difficult to locate, since it is saved by the caller, and is
+     thus at an unknown offset in OLD-CONT, rather than anywhere in the current
+     frame.
+
+We must have all of these to parse the stack.
+
+With the proposed Debug-Function, we parse the stack (starting at the top) like
+this:
+ 1] Use ENV to locate the current Debug-Info
+ 2] Use the Debug-Info and PC to determine the current Debug-Function.
+ 3] Use the Debug-Function to find the OLD-CONT and RETURN-PC.
+ 4] Find the old ENV by searching up the stack for a saved code object
+    containing the RETURN-PC.
+ 5] Assign old ENV to ENV, OLD-CONT to CONT, RETURN-PC to PC and goto 1.
+
+If we changed the function representation so that the code and environment were
+a single object, then the location of the old ENV would be simplified.  But we
+still need to represent ENV as separate from PC, since interrupts and errors
+can happen when the current PC isn't positioned at a valid return PC.
+
+It seems like it might be a good idea to save OLD-CONT, RETURN-PC and ENV at
+the beginning of the frame (before any stack arguments).  Then we wouldn't have
+to search to locate ENV, and we also have a hope of parsing the stack even if
+it is damaged.  As long as we can locate the start of some frame, we can trace
+the stack above that frame.  We can recognize a probable frame start by
+scanning the stack for a code object (presumably a saved ENV).
+
+ Probably we want some fairly general
+mechanism for specifying that a TN should be considered to be live for the
+duration of a specified environment.  It would be somewhat easier to specify
+that the TN is live for all time, but this would become very space-inefficient
+in large block compilations.
+
+This mechanism could be quite useful for other debugger-related things.  For
+example, when debuggability is important, we could make the TNs holding
+arguments live for the entire environment.  This would guarantee that a
+backtrace would always get the right value (modulo setqs).  
+
+Note that in this context, "environment" means the Environment structure (one
+per non-let function).  At least according to current plans, even when we do
+inter-routine register allocation, the different functions will have different
+environments: we just "equate" the environments.  So the number of live
+per-environment TNs is bounded by the size of a "function", and doesn't blow up
+in block compilation.
+
+The implementation is simple: per-environment TNs are flagged by the
+:Environment kind.  :Environment TNs are treated the same as :Normal TNs by
+everyone except for lifetime/conflict analysis.  An environment's TNs are also
+stashed in a list in the IR2-Environment structure.  During during the conflict
+analysis post-pass, we look at each block's environment, and make all the
+environment's TNs always-live in that block.
+
+We can implement the "fixed save location" concept needed for lazy frame
+creation by allocating the save TNs as wired TNs at IR2 conversion time.  We
+would use the new "environment lifetime" concept to specify the lifetimes of
+the save locations.  There isn't any run-time overhead if we never get around
+to using the save TNs.  [Pack would also have to notice TNs with pre-allocated
+save TNs, packing the original TN in the stack location if its FSC is the
+stack.]
+
+
+We want a standard (recognizable) format for an "escape" frame.  We must make
+an escape frame whenever we start running another function without the current
+function getting a chance to save its registers.  This may be due either to a
+truly asynchronous event such as a software interrupt, or due to an "escape"
+from a miscop.  An escape frame marks a brief conversion to a callee-saves
+convention.
+
+Whenever a miscop saves registers, it should make an escape frame.  This
+ensures that the "current" register contents can always be located by the
+debugger.  In this case, it may be desirable to be able to indicate that only
+partial saving has been done.  For example, we don't want to have to save all
+the FP registers just so that we can use a couple extra general registers.
+
+When when the debugger see an escape frame, it knows that register values are
+located in the escape frame's "register save" area, rather than in the normal
+save locations.
+
+It would be nice if there was a better solution to this internal error concept.
+One problem is that it seems there is a substantial space penalty for emitting
+all that error code, especially now that we don't share error code between
+errors because we want to preserve the source context in the PC.  But this
+probably isn't really all that bad when considered as a fraction of the code.
+For example, the check part of a type check is 12 bytes, whereas the error part
+is usually only 6.  In this case, we could never reduce the space overhead for
+type checks by more than 1/3, thus the total code size reduction would be
+small.  This will be made even less important when we do type check
+optimizations to reduce the number of type checks.
+
+Probably we should stick to the same general internal error mechanism, but make
+it interact with the debugger better by allocating linkage registers and
+allowing proceedable errors.  We could support shared error calls and
+non-proceedable errors when space is more important than debuggability, but
+this is probably more complexity than is worthwhile.
+
+We jump or trap to a routine that saves the context (allocating at most the
+return PC register).  We then encode the error and context in the code
+immediately following the jump/trap.  (On the MIPS, the error code can be
+encoded in the trap itself.)  The error arguments would be encoded as
+SC-offsets relative to the saved context.  This could solve both the
+arg-trashing problem and save space, since we could encode the SC-offsets more
+tersely than the corresponding move instructions.