[a530bb]: doc / cmucl / internals / debugger.tex Maximize Restore History

Download this file

debugger.tex    538 lines (429 with data), 26.8 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
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.