|
|
1.1 root 1: .so ../ADM/mac
2: .XX 24 397 "Pi: A Case Study in Object-Oriented Programming"
3: ...\" Wed May 21 16:37:07 EDT 1986
4: .de CW
5: \%\&\\$3\\f(CW\\$1\\fP\&\\$2
6: ..
7: ..
8: .TL
9: Pi: A Case Study in Object-Oriented Programming
10: .AU
11: T. A. Cargill
12: .MH
13: .AB
14: Pi is a debugger written in C++.
15: This paper explains how object-oriented programming in C++ has influenced Pi's
16: evolution.
17: The motivation for object-oriented programming was to experiment with a
18: browser-like graphical user interface.
19: The first unforeseen benefit was in the symbol table: lazy construction of an
20: abstract syntax-based tree gave a clean interface to the remainder of Pi,
21: with an efficient and robust implementation.
22: Next, though not in the original design, Pi was easily modified to control
23: multiple processes simultaneously.
24: Finally, Pi was extended to control processes
25: executing across multiple heterogeneous target processors.
26: .AE
27: .NH
28: Introduction
29: .PP
30: The subject of this paper is the impact of object-oriented programming on the
31: structure and capabilities of a complicated piece of software \- a debugger
32: called Pi.
33: The paper begins with observations about debugging in general and a description
34: of Pi, to motivate the introduction of object-oriented programming.
35: When the design of Pi began, object-oriented programming was chosen as
36: a means of achieving a style of user interface.
37: But the application of object-oriented techniques throughout had unforeseen
38: benefits with respect to symbol table structure, multi-process debugging and
39: target environment independence.
40: .NH
41: User Interfaces for Debugging
42: .PP
43: A debugger must provide many views of its subject.
44: The primary view of the subject program is static: the source text.
45: The primary view of the subject process is dynamic: the callstack, a sequence of
46: activation records.
47: The programmer's insight into the program's behavior comes from merging these
48: views: examining data within the process while controlling its progress through
49: the source text.
50: Debugging purely at the source level may be sufficient for a safe language,
51: correctly implemented, but in practice we also need views of the implementation.
52: Even if none of the subject is written in assembler, it is vital from time
53: to time to consider the process in terms of the instruction stream, registers
54: and uninterpreted memory.
55: .PP
56: A particular view is not used in isolation; it is related to other views.
57: The programmer moves rapidly among a set of related views.
58: This diversity complicates the user interface.
59: Any attempt to base an interface on a coherent model is confounded by the user's
60: need to switch among so many projections of the underlying subject.
61: For example, the keyboard input
62: .P1 0
63: 100
64: .P2
65: could mean ``display the context of line 100'' of the current source file.
66: It could also mean ``evaluate the expression 100'' or ``display the value of
67: the memory cell at location 100'' and so on.
68: There is no natural interpretation; it depends on the programmer's current focus.
69: If there is some simple model on which a user interface for such a
70: (necessarily) powerful tool could be based, no one has yet found it.
71: .PP
72: Switching among a set of input modes might help \- as long as there is no
73: confusion over what mode is current and how one changes mode.
74: However, experience with just two modes in text editors
75: suggests that a larger number of modes would not work.
76: On the other hand, modeless keyboard languages for debuggers tend to need many
77: qualifiers and options for each command, varying from verbose to cryptic and
78: from pedantic to treacherous.
79: But if they are useful, they are large and complicated.
80: Two recent debuggers use programmability of the user interface to permit the
81: custom definition of keyboard languages for different classes of users.
82: Kraut|reference(bruegge) (with ``path expressions'')
83: and Dbx|reference(bsdmanual) (with macros) let
84: new commands be defined in terms of a base language.
85: In contrast, Pi's user interface uses multi-window bitmap graphics, and cannot
86: be extended by the user.
87: .NH
88: Pi's User Interface
89: .PP
90: Pi is primarily an attempt to combine expressive power and ease of use in a
91: graphics interface.
92: The user browses through a network of views, each in its
93: own window with a specialized pop-up command menu and keyboard language.
94: The user selects a window by pointing at it with a mouse or by following a
95: menu or keyboard connection from a related window.
96: All the information within a window is textual, i.e., symbolic and numeric rather
97: than analog or geometric.
98: Moreover, each line of text is also part of the browsing network;
99: it defines its own menu and keyboard interface.
100: .PP
101: The details of the graphics are not essential, but will help make the discussion
102: more concrete.
103: The display has 100 monochrome pixels per inch.
104: Within Pi, windows may overlap and are positioned explicitly by the user.
105: A proportional scroll bar on the left shows how much of a window's text is
106: visible.
107: The current window has a thick border; the current line is video-inverted.
108: A three-button mouse is used.
109: The left button makes selections: to change
110: the current window, to scroll within it or to select a line.
111: The middle and right buttons raise the pop-up menus associated with the current
112: line and window, respectively.
113: A line of accumulated keyboard characters is displayed in a common space
114: below the other windows.
115: If the current line or window accepts keyboard input, its border
116: flashes at each keystroke.
117: At carriage return a complete line of input is sent to the current recipient.
118: .PP
119: The following example shows much of the user interface mechanics, but
120: only a few of Pi's features.
121: A more complete example appears in |reference(pi feel).
122: This trivial C program is taken from |reference(cbook):
123: .P1 0
124: #include <stdio.h>
125:
126: main() /* count lines in input */
127: {
128: int c, nl;
129:
130: nl = 0;
131: while ((c = getchar()) != EOF)
132: if (c == '\en')
133: ++nl;
134: printf("%d\en", nl);
135: }
136: .P2
137: Assume this program has been compiled and invoked by a command interpreter
138: running elsewhere on the screen.
139: The process reads from the keyboard of its virtual terminal.
140: To bind Pi and the process, the user selects the process from a
141: list of accessible processes in Pi's master window.
142: To control this subject process Pi creates a Process window, for which the
143: user must sweep a rectangle with the mouse.
144: The Process window identifies the process and shows its state, as in Figure 1.
145: Process 27775 is in a
146: .CW read
147: system call, waiting for keyboard input for the call to
148: .CW getchar() .
149: .KF
150: .so fig1.so
151: .ce
152: Figure 1. A Process window and its menu.
153: .SP
154: .KE
155: .PP
156: The Process window is the hub of a network of views of
157: the process.
158: Choosing
159: .CW src\ text ' `
160: from the Process window's pop-up menu creates a Source Text window for
161: the source file,
162: .CW count.c ,
163: shown in Figure 2.
164: .LP
165: A breakpoint is set by pointing at a source line
166: and choosing
167: .CW set\ bpt ' `
168: from the line's menu.
169: The presence of the breakpoint is recorded by
170: .CW >>>
171: at the left of the line (Figure 3).
172: .PP
173: The subject process reaches the breakpoint when the user supplies some keyboard
174: input and
175: .CW getchar()
176: returns a character.
177: The Process window announces the change of state and displays a callstack
178: traceback, in this case a degenerate stack of depth 1.
179: Pi shows the source text context by making the Source Text window current
180: and selecting the source line at which the program stopped.
181: Choosing
182: .CW open\ frame ' `
183: from the source line's menu creates a Frame window for evaluating expressions
184: with respect to the activation record associated with that source line.
185: .KF bottom
186: .so fig2.so
187: .ce
188: Figure 2. A Source Text window and a source line's menu.
189: .SP
190: .KE
191: .KF
192: .so fig3.so
193: .ce
194: Figure 3. A Frame window and an expression's menu at a breakpoint.
195: .SP
196: .KE
197: .PP
198: Expressions in a Frame window are built from menus or the keyboard.
199: Choosing an identifier, say
200: .CW c ', `
201: from the Frame's menu of local variables creates and evaluates a simple expression,
202: shown in Figure 3.
203: The variable's type determines the default format in which the value is
204: displayed: an integer in decimal.
205: It makes more sense to display
206: .CW c 's
207: value as an ASCII character.
208: Changing the format is one of the operations in the expression's menu.
209: Choosing
210: .CW format ' `
211: from the expression's menu and
212: .CW ascii\ on ' `
213: from a sub-menu of formats re-evaluates the expression and displays it as:
214: .P1 0
215: c='a'=97
216: .P2
217: .PP
218: In debugging a real program there are likely to be many more windows.
219: (Pi is used daily by dozens of programmers, often on programs of 10,000 lines
220: or more.)
221: The windows shown above may be instantiated arbitrarily often: a Source Text window
222: for each of the program's source files and a Frame window for each of the
223: callstack's activation records.
224: There may be only one instance of each of the other window types:
225: a Frame window bound to global scope rather than an activation record,
226: an Assembler window for controlling the process at the instruction level,
227: a Raw Memory window for manipulating memory as an uninterpreted array of cells,
228: and a Signals window for controlling process exceptions.
229: .PP
230: The resemblance between Pi and Dbxtool|reference(dbxtool) is superficial,
231: even though both debuggers use windows and menus on a bitmap display.
232: Dbxtool is a front-end that translates graphics commands
233: into the command language of a conventional debugger, Dbx.
234: Dbxtool provides a fixed set of windows, one of which, the ``Command Window,''
235: shows the Dbx commands and responses.
236: To see how this affects the user, consider trying to evaluate
237: expressions with respect to two different activations records at once.
238: In Pi, a window is opened for each activation record and the user
239: moves back and forth with the mouse, evaluating expressions in either window;
240: each window then contains a set of related expressions.
241: In Dbxtool, the user enters a mixture of commands that evaluate expressions and
242: move up or down the callstack; the Command Window then contains a transcript of
243: interleaved context changes and evaluated expressions.
244: .PP
245: Pi is also unlike the debugger in Smalltalk's ``integrated environment,''
246: where there is little distinction between the compiler, interpreter, browser and
247: debugger|reference(smalltalk80).
248: Smalltalk's tools cooperate through shared data structures and the
249: computer's uniformly defined graphics.
250: On the other hand, Pi is an isolated tool in a ``toolkit environment.''
251: Pi interacts with graphics, external data and other processes
252: through explicit interfaces.
253: Pi adapts the graphics techniques exemplified by Smalltalk to the toolkit
254: setting.
255: .NH
256: Object-Oriented Programming
257: .PP
258: The remainder of the paper concerns the implementation of Pi.
259: The object-oriented programming model is better suited to the implementation of
260: Pi than the sequential programming model or the multiple sequential processes
261: model.
262: .PP
263: Under the sequential model, a process executes a program.
264: At any time the process is in some state, with control at some location in the
265: program.
266: When the process blocks for input from the user it is in the state from which
267: it resumes when the user responds.
268: This makes it difficult to drive the process with the flexibility described
269: above, where the user is allowed to refuse a given menu and leap to some
270: unrelated context.
271: The process must accept either the menu selection or the context switch.
272: The menu selection is a local operation in the local context;
273: the context switch is a global operation involving unrelated parts of the
274: program.
275: This might be achieved by letting the user traverse a network of contexts.
276: But there is then a tradeoff between ease of use and ease of programming:
277: a tree is easy to program, but tedious for the user to traverse;
278: a fully connected network might be easy to use, but calls for every
279: part of the program to be intimately coupled to every other part.
280: .PP
281: With multiple sequential processes a separate process could be associated with
282: each context.
283: As the user moves around the graphics screen, input is directed to the
284: appropriate process by some agent that maps screen locations to processes.
285: Processes are suspended until the user needs them;
286: they need know only about semantically related processes.
287: As a model this yields a natural architecture,
288: but its implementation is usually very expensive.
289: The overhead for creation and interaction of separate processes
290: might be acceptable for a small number of processes, say one per window.
291: But the desired user interface has each line within each window interacting
292: independently with the user.
293: Many hundreds, even thousands, of processes would be needed.
294: With conventional multi-process techniques the cost is prohibitive.
295: .PP
296: The object-oriented model lies between these two models.
297: Instead of a collection of processes there is a single process containing a
298: collection of
299: .I objects ,
300: each an instance of a type called a
301: .I class .
302: Each object has a copy of the
303: .I data\ members
304: defined in its class and can execute the
305: .I function\ members
306: of the class.
307: Unlike a process, an object has no permanent state other than its data.
308: Objects share a universal address space and communicate with one another
309: by invoking function members as procedures.
310: It is feasible to have many thousands of objects.
311: That objects cannot execute concurrently, as processes do by timeslicing a
312: physical processor, is not significant; such concurrency is not required.
313: .PP
314: The C++|reference(cplusplus) programming language supports this model.
315: C++ is a superset of C with Simula-like classes|reference(simula begin).
316: The members of a class are data and functions, in private and public sections:
317: .P1 0
318: class \fIidentifier\fP {
319: \fIprivate_data_declarations\fP
320: \fIprivate_function_declarations\fP
321: public:
322: \fIpublic_data_declarations\fP
323: \fIpublic_function_declarations\fP
324: };
325: .P2
326: (This grammar generates only a subset of C++ class declarations.)
327: The example of C++ code below defines a class
328: .CW Const
329: that privately represents an integer value; the value is rendered as
330: a hexadecimal, decimal or octal character string
331: by three member functions.
332: Similar code is found in Pi.
333: .P1 0
334: class Const {
335: int val; // private data member
336: public:
337: Const(int); // constructor
338: char *hex(); // public function members
339: char *dec();
340: char *oct();
341: };
342:
343: void Const::Const(int v) // constructor body
344: {
345: val = v;
346: }
347:
348: char *Const::hex() // function member body
349: {
350: \fIbuild character string\fP
351: return \fIpointer to character string\fP;
352: }
353:
354: ...
355:
356: .P2
357: A member function whose name is the same as that of its class is a
358: .I constructor.
359: If defined, the constructor is invoked to initialize a new object.
360: .CW Const 's
361: constructor assigns its argument to the private representation of the value.
362: .CW Const 's
363: member functions
364: .CW hex() ,
365: .CW dec()
366: and
367: .CW oct()
368: return pointers to ASCII representations of the value.
369: The following client code prints 123 as
370: .CW 0x7B=123=0173 :
371: .P1 0
372: {
373: Const c(123); // c.Const(123) called implicitly
374: printf("%s=%s=%s", c.hex(), c.dec(), c.oct());
375: }
376: .P2
377: Here
378: .CW Const\ c(123)
379: declares an object instantiated on the stack for the lifetime of the block;
380: the argument
381: .CW 123
382: is passed to the constructor.
383: The object could also be allocated from the free store by means of the operator
384: .CW new ,
385: in which case the variable's type is pointer-to-\f(CWConst\fP:
386: .P1 0
387: {
388: Const *cp;
389: cp = new Const(123); // allocate from free store
390: printf("%s=%s=%s", cp->hex(), cp->dec(), cp->oct());
391: delete cp; // return to storage pool
392: }
393: .P2
394: .PP
395: In general, an executing C++ program consists of a network of objects,
396: referencing one another with pointers.
397: Subject to the encapsulation rules of C++,
398: objects may access one another's member data and invoke member functions.
399: Consider a Frame window that evaluates expressions with respect to an activation
400: record.
401: Figure 4 shows two objects:
402: .CW F ,
403: an instance of class
404: .CW Frame ,
405: and
406: .CW E ,
407: an instance of class
408: .CW Expr .
409: An arrow represents a pointer; an arrow labeled by a function call indicates
410: that the holder of the pointer may call the member function of the object to
411: which it points.
412: .KF
413: .PS
414: .ft CW
415: F: circle "F"
416: E: circle "E" at F.c+(0,-1)
417: arrow " create_expr(symbol)" "" from F.e+(1,0) to F.e
418: arrow " evaluate()" from F.s to E.n
419: .ft P
420: .PE
421: .ce
422: Figure 4. A sub-network of objects.
423: .KE
424: .PP
425: Here,
426: .CW create_expr()
427: is a member of
428: .CW Frame
429: and
430: .CW evaluate()
431: is a member of
432: .CW Expr :
433: .P1 0
434: class Frame{
435: ...
436: void create_expr(Symbol*);
437: ...
438: };
439:
440: class Expr{
441: ...
442: ErrMsg evaluate();
443: ...
444: };
445: .P2
446: The
447: .CW Symbol*
448: argument to
449: .CW create_expr
450: is a pointer to a symbol table object describing a variable in
451: .CW F 's
452: activation record's scope, from which the
453: .CW Frame
454: creates an
455: .CW Expr
456: object
457: .CW E .
458: .CW F
459: might cause the expression to evaluate itself by calling
460: .CW E.evaluate() ,
461: and so forth.
462: .PP
463: The notation in Figure 4 suggests a message-passing model.
464: Indeed, the terminology of object-oriented programming in
465: Smalltalk|reference(smalltalk80) refers to ``messages'' between objects.
466: However, the communication is by procedure call.
467: .NH
468: Object-Oriented User Interface
469: .PP
470: A user interface to such a network can be created by letting the user communicate
471: directly through the interfaces that objects present to one another|reference(smalltalk80).
472: The user must be able to select an object,
473: see a set of member function calls and select one to be invoked.
474: To receive a member function call from the user an object must describe
475: itself and a set of function calls to the software managing the display.
476: .PP
477: Figure 5 shows the object network of Figure 4 and a screen image.
478: .CW F
479: has associated itself with a window on the screen, and
480: .CW E
481: has associated itself with a line in that window.
482: The new ``pointers'' from images on the screen to objects in the program are
483: shown by dashed arrows.
484: If the user selects
485: .CW E 's
486: line, raises the line's menu and selects
487: .CW ascii\ on ', `
488: then the result is a call of
489: .CW E 's
490: member function:
491: .P1 0
492: E.reformat(ASCII_ON)
493: .P2
494: .KF
495: .PS
496: .ft CW
497: F: circle "F"
498: E: circle "E" at F.c+(0,-1)
499: arrow from F.s to E.n
500: SCREEN: spline from E.s+(2,-.8) \
501: then up 1.2 \
502: then up 0.1 right 0.1 then up 0.1 right 0.1 \
503: then right 1 up 0.1 right 1 down 0.1 \
504: then down 0.1 right 0.1 then down 0.1 right 0.1 \
505: then down 2.4 \
506: then left 0.1 down 0.1 then left 0.1 down 0.1 \
507: then left 1 down 0.1 left 1 up 0.1 \
508: then up 0.1 left 0.1 then up 0.1 left 0.1 \
509: then up 1.2
510: WINDOW: box at SCREEN+(1,0.5) height 1 width 1.5
511: "count.c:9 main()" at WINDOW.nw+(.7,-0.1)
512: "c=97" at WINDOW.nw+(.2,-0.25)
513: arrow dashed from WINDOW.n to F chop 0 chop circlerad
514: arrow "reformat(ASCII_ON)" "" dashed from WINDOW.nw+(.05,-0.3) to E chop 0 chop circlerad
515: MENU: box "." "." "ascii on" "." "." at WINDOW+(.5,-1.2) height 1.2 width .7
516: line dashed from MENU+(-.1,.1) to WINDOW.nw+(.05,-0.3)
517: .ft P
518: .PE
519: .ce
520: Figure 5. Operations invoked by the user.
521: .KE
522: .PP
523: Three points of detail should be mentioned.
524: First, the user does not really see the program's internal interfaces,
525: i.e., the identifiers and types of member functions.
526: The text of a menu entry is chosen to support the user's view of the
527: function and need not reflect program's source literally.
528: For example, the user sees
529: .CW ascii\ on ' `
530: instead of
531: .CW reformat(ASCII_ON) '. `
532: Second, when invoked, a member function cannot distinguish a call
533: originated directly by the user from a call by another object.
534: There is only one procedure call mechanism.
535: Third, once an object has exposed its image and a set of member function calls
536: to the user, it must be prepared to receive any sequence of calls the user
537: chooses to make.
538: To inhibit the user, the object must remove itself or change the set of calls
539: in the menu.
540: Changes in the menu reassure the user about the state of the object:
541: the menu of a line of source text on which the user has already set a breakpoint
542: shows
543: .CW clear\ bpt ' `
544: instead of
545: .CW set\ bpt '. `
546: .PP
547: The interaction between the user and program is now conducted without a
548: ``user interface'' in the conventional sense.
549: There is no part of the program responsible for reading, parsing and interpreting a
550: sequence of commands from the user.
551: The user sees a graphical image of the program and the program sees the user as an
552: active participant in its object-oriented world,
553: with implicit software mapping between the two.
554: .NH
555: Implementation
556: .PP
557: The interface between the graphics and the network of objects is a pair of C++
558: classes:
559: .CW Window
560: and
561: .CW Menu .
562: These classes in turn interact with a graphics package.
563: To create its window, the
564: .CW Frame
565: object executes:
566: .P1 0
567: w = new Window; // Save the pointer to a new
568: // Window in Frame's data member w.
569: w->bind(this); // Pass a pointer to this (a C++ keyword)
570: // instance of Frame to the Window.
571: w->title(description()); // Frame's member function description()
572: // yields a string like "count:9 main()".
573: w->makecurrent(); // To make a newly created window current
574: // the user must sweep a rectangle for it.
575: .P2
576: To display its value in the window an
577: .CW Expr
578: object executes:
579: .P1 0
580: Menu m; // Instantiate a Menu on the local stack.
581: .
582: .
583: m.append("octal on", &reformat, OCTAL_ON);
584: m.append("ascii on", &reformat, ASCII_ON); // Build the menu.
585: m.append("float on", &reformat, FLOAT_ON);
586: .
587: .
588: w->insert(line, this, m, evaltext()); // Insert line in window.
589: .P2
590: Each call to
591: .CW m.append()
592: adds an entry to the menu.
593: Each entry consists of the text string the user sees when the menu is raised,
594: the member function to be called and the argument to be passed.
595: The call to
596: .CW insert() ,
597: using a copy of
598: .CW w
599: passed to the
600: .CW Expr
601: by the
602: .CW Frame ,
603: creates a new line of text in the window.
604: The text string, like
605: \f(CW"c=97"\fP,
606: is obtained from
607: .CW Expr 's
608: member function
609: .CW evaltext() .
610: The
611: .CW line
612: argument is a numeric key that determines where this line will appear
613: relative to other lines in the window.
614: Not shown here is how
615: .CW ascii\ on ' `
616: would be made to appear in a sub-menu, as in Figure 3.
617: Instead of being bound to a window or line of text, one
618: .CW Menu
619: may be embedded in another.
620: The embedded sub-menu pops up when the cursor is positioned over a sub-menu
621: indicator in the super-menu.
622: .PP
623: Note that the code from
624: .CW Frame
625: and
626: .CW Expr
627: does not reflect the terminal's physical attributes.
628: The abstractions are windows and menus, not coordinates and mouse buttons.
629: Pi has no knowledge of, or control over, details such as the placement of
630: windows, the scrolling of text or the assignment of mouse buttons.
631: Though not used here, the most concrete graphics request is that a line of text
632: be made visible to the user.
633: This means that the graphics and mouse protocol described above is just one of
634: many possible choices; the user's view might be quite different in an
635: implementation for a one-button mouse.
636: .PP
637: Operations on
638: .CW Window
639: objects are translated into a stream of messages passed to a separate
640: process responsible for the real-time graphics.
641: Operations from the user are transmitted back to the main process, received by
642: the package and invoked on the Pi's objects.
643: The two processes execute asynchronously on separate processors: the main
644: process on a
645: .UX
646: system, ``the host,'' and the real-time graphics process on
647: a Teletype DMD 5620 bitmap terminal, ``the terminal'' (Figure 6).
648: .KF
649: .PS
650: HOST: circle "Pi and" "window" "package" rad .5
651: TERM: circle at HOST+(2,0) "real-time" "graphics" rad .5
652: TOTERM: arc -> cw from HOST.ne to TERM.nw
653: TOHOST: arc -> cw from TERM.sw to HOST.se
654: "definitions" at TOTERM.n+(0,.1)
655: "operations" at TOHOST.s-(0,.1)
656: LINE: line dashed from TOTERM.n-(0,.1) to TOHOST.s+(0,.1)
657: ABOVE: line dashed from TOTERM.n+(0,.2) to TOTERM.n+(0,0.4)
658: BELOW: line dashed from TOHOST.s-(0,.2) to TOHOST.s-(0,1.5)
659: SUBJ: circle at HOST-(0,1.5) "subject" "process" rad .5
660: arrow " OS support" <-> from HOST.s to SUBJ.n
661: "host" at SUBJ.s-(0,0.2)
662: "terminal" at TERM.s-(0,1.7)
663: .PE
664: .ce
665: Figure 6. Inter-process communication.
666: .KE
667: .PP
668: Global control in the graphics program lies in a loop that polls the
669: host and user for commands.
670: The following pseudo-code approximates the loop:
671: .P1 0
672: for(;;){ // forever
673: if( mouse activity ){
674: update screen
675: if( remote operation selected )
676: send message to host
677: }
678: if( message from host ){
679: receive from host
680: update screen
681: }
682: if( real-time clock event ){ // see below
683: send message to host
684: }
685: }
686: .P2
687: Global control in the host process lies in a loop in
688: the window package that blocks waiting for messages from the terminal:
689: .P1 0
690: for(;;){
691: read <object, operation, operand> from terminal
692: invoke object->operation(operand)
693: }
694: .P2
695: Usually, an invoked operation in turn causes definitions or redefinitions
696: of objects to be sent to the terminal, to show the user some kind of result.
697: The messages from the host arrive asynchronously with respect to the user's
698: interaction with the graphics.
699: An experienced user need not wait for changes from each operation to be reflected
700: on the screen before invoking another operation.
701: For example, having requested a particular context within a source file to be
702: displayed, the user can select a source line and set a breakpoint on it
703: as soon as that line is visible, even if the host has not finished sending
704: all of the lines needed to fill the window.
705: Some users take advantage of the asynchrony as the natural way to function;
706: others operate as though the communications were half-duplex.
707: .NH
708: Pi's Architecture
709: .PP
710: The debugger is a network of objects as described above.
711: An object of class
712: .CW Process
713: is created to take overall control of the subject process.
714: The
715: .CW Process
716: object creates two major objects to serve it: a symbol table object of class
717: .CW SymTab
718: and a core image access object of class
719: .CW Core .
720: The
721: .CW SymTab
722: is responsible for reading the symbol table as left by the compiler,
723: assembler and loader and providing that information to the rest of the
724: debugger, as discussed below.
725: The
726: .CW Core
727: is responsible for all access to the address space of the subject process and
728: the operating system's control information.
729: Details of the physical processor, operating system and compiler generated code
730: are encapsulated in
731: .CW Core .
732: .KF
733: .PS 5
734: .ft CW
735: .ps -2
736: PROC: circle "Process" rad 0.4
737: CIRC: circle at PROC rad 1.5 invis
738: CORE: circle "Core" at CIRC.w rad 0.4
739: SYMTAB: circle "SymTab" at CIRC.nw rad 0.4
740: BPTS: circle "BreakPts" at CIRC.n rad 0.4
741: ASM: circle "Assembler" at CIRC.ne rad 0.4
742: MEM: circle "Memory" at CIRC.e rad 0.4
743: STACK: circle "CallStack" at CIRC.sw rad 0.4
744: FRAME: circle "Frame" at CIRC.s rad 0.4
745: SIGNALS: circle "Signals" at CIRC.se rad 0.4
746: line from CORE to PROC chop 0.4
747: line from SYMTAB to PROC chop 0.4
748: line from BPTS to PROC chop 0.4
749: line from ASM to PROC chop 0.4
750: line from MEM to PROC chop 0.4
751: line from STACK to CORE chop 0.4
752: line from FRAME to STACK chop 0.4
753: line from SIGNALS to PROC chop 0.4
754: FRAMEN: circle "Frame" at FRAME-(0,1.5) rad 0.4
755: "." "." "." at 1/2 <FRAME.s,FRAMEN.n>
756: line from STACK to FRAMEN chop 0.4
757: EXPR: circle "Expr" at FRAMEN+(1.5,0) rad 0.4
758: EXPRN: circle "Expr" at EXPR+(1.5,0) rad 0.4
759: line from FRAMEN to EXPR chop 0.4
760: ". . ." at 1/2 <EXPR.e,EXPRN.w>
761: SRC: circle "SrcText" at SYMTAB+(0,1.5) rad 0.4
762: SRCN: circle "SrcText" at SRC+(-1.5,0) rad 0.4
763: line from SYMTAB to SRC chop 0.4
764: line from SYMTAB to SRCN chop 0.4
765: ". . ." at 1/2 <SRC.w,SRCN.e>
766: .ft P
767: .ps +2
768: .PE
769: .ce
770: .ft R
771: Figure 7. Pi's object network
772: .KE
773: .PP
774: Of these three classes, only
775: .CW Process
776: opens a window, the Process window, from which the user may then open other
777: windows.
778: To display a callstack, the
779: .CW Process
780: obtains a
781: .CW CallStack
782: object from the
783: .CW Core
784: and extracts each activation record it needs as a
785: .CW Frame
786: object from the
787: .CW CallStack.
788: If a
789: .CW Frame
790: is referenced by the user, it opens a Frame window.
791: As the user creates expressions and derives new ones, each expression is an
792: .CW Expr
793: object which displays itself as a line in its
794: .CW Frame 's
795: window.
796: Figure 7 shows the network.
797: The lines indicate the primary permanent links between objects, but
798: pointers are passed around as needed.
799: .PP
800: It is the
801: .CW Process
802: that monitors the state of subject process.
803: When the subject is running, its state must be polled to see if it reaches a
804: breakpoint or some other exception.
805: The
806: .CW Process
807: therefore periodically executes an operation that reads the current
808: state of the subject from the
809: .CW Core .
810: This operation re-invokes itself by sending to the terminal
811: a message requesting that the terminal invoke it after a specified delay.
812: The invocation returned from the terminal is interleaved with, and
813: indistinguishable from, invocations made directly by the user.
814: The
815: .CW Process
816: can monitor the state of the subject process while the user asynchronously
817: evaluates expressions, sets breakpoints and so on.
818: This technique creates a few bytes per second of extra host-terminal traffic,
819: since it would be possible to use a clock interrupt on the host instead.
820: However, to guarantee that operations are invoked fairly is much simpler if
821: the only source of operations is a single stream coming from the terminal.
822: Also, the terminal is a better place for real-time programming, both in terms
823: of operating system support and expendable processor resources.
824: .NH
825: Symbol Tables
826: .PP
827: A debugger's needs of its symbol table are similar to those of a compiler,
828: for example, to determine the variables in scope at some point in the program.
829: If the symbol tables prepared by the translator suite reflect the data
830: structures used in the compiler,
831: the debugger is much simplified|reference(cardell swat).
832: If the tables have been ``flattened'' by an assembler or loader, the debugger
833: suffers the loss of information.
834: Reconstructing an acceptable data structure can be very time-consuming
835: |reference(beander).
836: Working with the flattened tables complicates those parts of the debugger
837: that make non-trivial use the symbol table|reference(blit debugger spe).
838: .PP
839: For Pi, it seemed likely that versions of the debugger would be used with
840: compilers and assemblers that produced at least two distinct flattened formats.
841: So the first goal was to find a format-independent internal representation
842: that could be built from either format and was well-suited to debugging.
843: Given that the debugger was being written in C++, it seemed a good
844: opportunity to experiment with object-oriented programming.
845: In contrast to the user interface, there was no overall design paradigm guiding the
846: symbol table; the software evolved as different ideas were tried.
847: .PP
848: From the outset, the symbol table was a sub-network of objects.
849: Its structure follows the abstract structure of the subject program \- a tree.
850: The root of the tree is the
851: .CW SymTab
852: object.
853: It reads the file prepared by the loader and builds a tree, as shown in Figure 8.
854: Below the
855: .CW SymTab
856: there is a
857: .CW SrcFile
858: object for each separately compiled source file that contributed to the
859: program.
860: (There is a one-to-one correspondence between the
861: .CW SrcFile s
862: and the
863: .CW SrcText s
864: of Figure 7; a
865: .CW SrcText
866: is created and opens a window when the user decides to examine the source text
867: from the corresponding
868: .CW SrcFile .)
869: Below each
870: .CW SrcFile
871: there is a
872: .CW Function
873: for each function defined in that file.
874: Below each
875: .CW Function
876: there is a
877: .CW Block
878: of arguments and a
879: .CW Block
880: of local variables.
881: Each
882: .CW Block
883: has a list of
884: .CW Variables .
885: Each
886: .CW Function
887: also has a list of
888: .CW Statements ,
889: the source statements in the function.
890: Some symbols can also be accessed directly from a hash table whose entries point
891: into tree's interior.
892: .PP
893: Like any object, a member of the symbol table can receive operations from the user.
894: For example, the object associated with a line of source text in a Source Window
895: is a
896: .CW Statement .
897: To set a breakpoint the user communicates directly with the symbol table.
898: .PP
899: Though not shown in the figure, each node in the tree has four pointers: to its
900: parent, leftmost child and right and left sibling.
901: These make it easy to traverse the tree.
902: C++'s type inheritance is used to capture the common properties of all symbol
903: nodes in a
904: .I base
905: .I class
906: called
907: .CW Symbol
908: from which other classes of symbols are
909: .I derived :
910: .P1 0
911: class Symbol {
912: public:
913: Symbol *parent;
914: Symbol *leftmost_child;
915: Symbol *right_sibling;
916: Symbol *left_sibling;
917: Address addr;
918: char *id;
919: virtual char *text();
920: };
921: .P2
922: .KF
923: .PS 4
924: .ft CW
925: .ps -2
926: SYMTAB: circle "SymTab" rad 0.4
927: SRC2: circle "SrcFile" at SYMTAB-(0,1.2) rad 0.4
928: SRC1: circle "SrcFile" at SRC2-(1.5,0) rad 0.4
929: SRC3: circle "SrcFile" at SRC2+(2,0) rad 0.4
930: arrow from SYMTAB to SRC1 chop 0.4
931: arrow from SRC1 to SRC2 chop 0.4
932: ". . ." at 1/2 <SRC2,SRC3>
933: FUNC2: circle "Function" at SRC2-(0,1.2) rad 0.4
934: FUNC1: circle "Function" at FUNC2-(1.5,0) rad 0.4
935: FUNC3: circle "Function" at FUNC2+(2,0) rad 0.4
936: arrow from SRC2 to FUNC1 chop 0.4
937: arrow from FUNC1 to FUNC2 chop 0.4
938: ". . ." at 1/2 <FUNC2,FUNC3>
939: ARGS: circle "Block" at FUNC2-(0,1.2) rad 0.4
940: LCLS: circle "Block" at ARGS-(0,1.2) rad 0.4
941: arrow from FUNC2 to ARGS chop 0.4
942: arrow from ARGS to LCLS chop 0.4
943: VAR1: circle "Variable" at LCLS+(1.5,0) rad 0.4
944: VAR2: circle "Variable" at VAR1+(1.5,0) rad 0.4
945: arrow from LCLS to VAR1 chop 0.4
946: ". . ." at 1/2 <VAR1,VAR2>
947: STMT2: circle "Statement" at LCLS-(0,1.2) rad 0.4
948: STMT1: circle "Statement" at STMT2-(1.5,0) rad 0.4
949: STMT3: circle "Statement" at STMT2+(1.5,0) rad 0.4
950: arrow from FUNC2 to STMT1 chop 0.4
951: arrow from STMT1 to STMT2 chop 0.4
952: ". . ." at 1/2 <STMT2,STMT3>
953: line dashed "" "\fPlazy\f(CW" from FUNC1-(0,0.6) to FUNC3-(0,0.6)
954: .ft P
955: .ps +2
956: .PE
957: .ce
958: .ft R
959: Figure 8. The symbol table hierarchy
960: .KE
961: The
962: .CW addr
963: and
964: .CW id
965: data members record address information and an identifier for each symbol.
966: The function
967: .CW text()
968: returns a textual representation of the node, which is just the identifier:
969: .P1 0
970: char *Symbol::text()
971: {
972: return id;
973: }
974: .P2
975: There are no instances of class
976: .CW Symbol
977: in the tree; each node of the symbol table is of a type derived from
978: .CW Symbol .
979: For example:
980: .P1 0
981: class Variable : public Symbol {
982: public:
983: Storage storage;
984: DataType type;
985: };
986: .P2
987: As a derived class,
988: .CW Variable
989: inherits all the data and functions of
990: .CW Symbol ;
991: it has two additional data members specific to its needs.
992: In
993: .CW Symbol ,
994: the function
995: .CW text()
996: is declared
997: .I virtual.
998: This means that a derived class may override the base version with its own.
999: .CW Variable
1000: has no need to do this; it is adequately served by the base version that returns
1001: the identifier.
1002: However,
1003: .CW Statement
1004: does define its own
1005: .CW text() :
1006: .P1 0
1007: class Statement : public Symbol {
1008: ...
1009: int line_number;
1010: char *text();
1011: };
1012: .P2
1013: Instead of returning the identifier (which is not used by
1014: .CW Statement ),
1015: .CW Statement::text()
1016: walks up the tree to find its
1017: .CW SrcFile
1018: node and returns a string identifying the statement's source file and line number,
1019: like:
1020: .P1 0
1021: "count.c:9"
1022: .P2
1023: In general, code traversing the tree to extract textual
1024: information does not depend on whether the class of a given node
1025: defines its own version of
1026: .CW text() .
1027: For example,
1028: .CW Statement::text()
1029: applies
1030: .CW text()
1031: to a
1032: .CW SrcFile
1033: pointer to obtain the pathname of the source file to embed in the string being
1034: built; it does not know whether
1035: .CW SrcFile
1036: has defined its own version of
1037: .CW text() .
1038: The choice of implementation of an operation by the object on which the operation
1039: is invoked, rather than the invoker, is common to all object-oriented
1040: programming.
1041: The technique is used throughout Pi.
1042: .NH
1043: Lazy Symbol Table Construction
1044: .PP
1045: If the symbol table as described above were really built it would consume
1046: enormous time and space.
1047: Early versions of Pi did build it and could only be applied to small programs.
1048: Time and space performance was improved dramatically by delaying construction
1049: of the subtree of local symbols below each
1050: .CW Function
1051: node until needed \- ``lazy'' construction.
1052: .PP
1053: When the debugger picks up a process it scans the flattened symbol table
1054: to build the tree only down to the
1055: .CW Function
1056: level \- the part of the tree above the dashed line in Figure 8.
1057: From a
1058: .CW Function
1059: the only public access to its sub-tree is through function members.
1060: The members of
1061: .CW Function
1062: detect that the sub-tree is missing and call on the
1063: .CW SymTab
1064: to build it before returning a pointer to a requested
1065: .CW Block
1066: or
1067: .CW Statement .
1068: Once the sub-tree has been built it remains; pointers into it may have been
1069: passed to, and retained by, other objects.
1070: .PP
1071: Lazy construction allows large symbol tables to be presented to the rest
1072: of the debugger in a manner that is encapsulated naturally and efficiently.
1073: The real-time initialization delay is about one second per thousand lines of source
1074: text, on a VAX-11/750\(dg processor.
1075: .FS
1076: \(dg VAX is a trademark of Digital Equipment.
1077: .FE
1078: The cost of building a sub-tree on demand is negligible and very
1079: few are built.
1080: Local tables are needed for only those functions on the callstack
1081: or visible in a Source window.
1082: It is exceptional to need tables for more than about 15 functions.
1083: When Pi, a 500-function program, is used to debug itself, it typically builds
1084: 1%\-5% of the local tables.
1085: .PP
1086: This style of lazy table construction could be implemented in any programming
1087: language, but reliably only in a language that enforces its data abstraction.
1088: In C++, because the only public access from a
1089: .CW Function
1090: to its sub-tree is through member functions, the lazy operation of the symbol
1091: table does not depend on cooperation from clients.
1092: Very few problems have arisen with this code.
1093: .PP
1094: A similar lazy method is used to defer the construction of the tables of
1095: user-defined types; the savings are comparable.
1096: .NH
1097: Generators that Traverse the Symbol Table
1098: .PP
1099: Clients of the symbol table need to perform various traversals to extract
1100: information.
1101: For example, a menu built by a
1102: .CW Frame
1103: contains the local variables visible from a function.
1104: This might be implemented by a natural traversal of the data structure
1105: in Figure 8.
1106: However, the symbol table can be better encapsulated by providing a class
1107: .CW VisibleVars ,
1108: an instance of which performs such a traversal:
1109: .P1 0
1110: class VisibleVars {
1111: ...
1112: public:
1113: VisibleVars(Block*);
1114: Variable *gen();
1115: };
1116: .P2
1117: The constructor takes an argument pointing to a
1118: .CW Block .
1119: The only public function,
1120: .CW gen() ,
1121: returns a pointer to a different
1122: .CW Variable
1123: on each call, ending with a null pointer.
1124: Client code with a pointer to a
1125: .CW Block
1126: from somewhere:
1127: .P1 0
1128: Block *b;
1129: .P2
1130: creates an instance of
1131: .CW VisibleVars
1132: and iterates through the generated variables:
1133: .P1 0
1134: {
1135: VisibleVars vv(b);
1136: Variable *var;
1137:
1138: while( var = vv.gen() ){
1139: ...
1140: }
1141: }
1142: .P2
1143: This iteration is built on general purpose data abstraction in C++,
1144: rather than a built-in iteration primitive|reference(icon book)
1145: |reference(alphard cacm).
1146: .PP
1147: Parts of Pi use nested iteration through the variables visible from a function.
1148: Nested iteration arises in the code that warns the user of ambiguity when an
1149: identifier occurs more than once in a menu.
1150: To determine if an identifier is non-unique, the menu builder searches
1151: the identifiers of the variables that its generator will produce later in the
1152: iteration by taking a
1153: .I copy
1154: of the generator in its current state and iterating through the copy:
1155: .P1 0
1156: {
1157: VisibleVars vv(b);
1158: Variable *v;
1159:
1160: while( v = vv.gen() ){
1161: ...
1162: VisibleVars copy(0); // initialized with null block
1163: copy = vv;
1164: Variable *w;
1165: while( w = copy.gen() ){
1166: if( strcmp(v->id, w->id) )
1167: ...
1168: }
1169: ...
1170: }
1171: }
1172: .P2
1173: (In this case quadratic running time is acceptable.)
1174: .NH
1175: Debugging Multiple Processes
1176: .PP
1177: The initial design of Pi did not consider letting the user examine
1178: more than a single process at a time.
1179: The
1180: .CW Process
1181: class (Figure 7) had been introduced in order to follow the object-oriented
1182: paradigm uniformly throughout the program.
1183: The intention was to instantiate
1184: .CW Process
1185: only once.
1186: Other parts of the debugger were made a little more complicated by this decision
1187: because they had to fetch data from the
1188: .CW Process
1189: that might otherwise have been stored in global variables.
1190: This version of the debugger did not have a master window or dynamic
1191: binding to processes; the subject process was fixed by a command line
1192: argument.
1193: .PP
1194: Once Pi was working reliably, I realized that with trivial modification
1195: it could instantiate an arbitrary set of
1196: .CW Process
1197: objects to examine an arbitrary set of subject processes.
1198: The user would need only one copy of the debugger, no matter how many processes
1199: were to be examined.
1200: It took only a few days to implement.
1201: A
1202: .CW Master
1203: object creates a
1204: .CW Process
1205: object for each subject process that the user chooses to examine,
1206: as shown in Figure 9.
1207: Each
1208: .CW Process
1209: and its sub-network knows nothing of any others that might exist.
1210: The user interface package sees no qualitative change \- just more objects.
1211: The technique whereby a
1212: .CW Process
1213: polls its subject is also unaffected; the delayed invocations of the
1214: polling operation from each
1215: .CW Process
1216: are interleaved with one another.
1217: None of the problems described in |reference(dbxtool) of implementing and using a
1218: multi-process debugger have been encountered.
1219: .KS
1220: .PS
1221: .ft CW
1222: .ps -2
1223: MAST: circle "Master" rad 0.3
1224: PROCSW: circle "Process" at MAST+(-1,-1) rad 0.3
1225: line from MAST to PROCSW chop 0.3
1226: PROCS: circle "Process" at MAST+(0,-1) rad 0.3
1227: line from MAST to PROCS chop 0.3
1228: PROCSE: circle "Process" at MAST+(1,-1) rad 0.3
1229: line from MAST to PROCSE chop 0.3
1230: .ft P
1231: .ps +2
1232: .PE
1233: .ce
1234: Figure 9. Object network for multi-process debugging.
1235: .KE
1236: .PP
1237: Multiple process debugging could also have been achieved by instantiating
1238: multiple debugger processes rather than multiple
1239: .CW Process
1240: objects within a single debugger.
1241: The advantage of instantiating only a single debugger is the reduced overhead
1242: for both the user and the computers.
1243: With only one instance of the debugger the user has less to manage on the screen.
1244: When multiple processes are debugged the set of debugging windows are the
1245: same whether they are together in one debugger's window or in separate instances
1246: of the debugger.
1247: But when the user wants to treat the debugging environment as a whole, it is
1248: better to deal with a single tool.
1249: For example, removing a single debugger is simpler than removing a set of
1250: debuggers.
1251: Less machine resources on the host and terminal are required to execute
1252: only a single process in each.
1253: In special circumstances (such as debugging a debugger), multiple instances
1254: of the debugger are needed.
1255: .NH
1256: Multiple Target Environments
1257: .PP
1258: The initial design did anticipate versions of Pi that would operate in
1259: different target environments.
1260: Those parts of the debugger dependent on the target processor were encapsulated in
1261: the
1262: .CW Core
1263: and
1264: .CW Assembler
1265: classes, those dependent on the operating system in
1266: .CW Core ,
1267: and those dependent on the external symbol table format in
1268: .CW SymTab .
1269: The original version of Pi was for a VAX processor running the Eighth Edition of
1270: the
1271: .UX
1272: system.
1273: The intention was to tailor versions to different target environments
1274: as the need arose.
1275: The first demand was for a version to debug processes in the DMD 5620 bitmap
1276: terminal: an AT&T WE32000 processor running a virtual terminal multiplexor
1277: called Mux.
1278: .PP
1279: As a further experiment with object-oriented programming, I decided
1280: to build both of these versions as a single program \- so that one instance
1281: of Pi could simultaneously examine processes in both target environments.
1282: For each target-dependent class there must be a base class, with a derived class
1283: for each target environment.
1284: Everywhere a target-dependent object is instantiated it must be of the
1285: appropriate derived class, but its target-independent clients need not know
1286: from which derivation.
1287: The classes derived from
1288: .CW Core
1289: are
1290: .CW HostCore
1291: and
1292: .CW TermCore ,
1293: for the host and terminal, respectively.
1294: The class hierarchy for
1295: .CW Core
1296: is shown in Figure 10.
1297: .KF
1298: .PS
1299: .ft CW
1300: CORE: "Core"
1301: HOSTCORE: "HostCore" at CORE+(-0.75,-0.75)
1302: TERMCORE: "TermCore" at CORE+( 0.75,-0.75)
1303: line from CORE.s-(0,.1) to HOSTCORE.n+(0,.1)
1304: line from CORE.s-(0,.1) to TERMCORE.n+(0,.1)
1305: .ft P
1306: .PE
1307: .ce
1308: Figure 10. Class hierarchy for \f(CWCore\fP.
1309: .KE
1310: .PP
1311: The main process of Pi is still a process in the timesharing host.
1312: To access memory in the terminal, an instance of
1313: .CW TermCore
1314: (executing on the host) communicates with an additional agent
1315: process in the terminal.
1316: (The agent process need not be in the same terminal as the
1317: real-time graphics process, but usually it is.)
1318: This communication is based on remote procedure calls from the host to
1319: the terminal.
1320: The debugger is now three processes altogether: the host process, the real-time
1321: graphics process and the terminal access agent, as shown in Figure 11.
1322: Implementation experience with a previous debugger |reference(blit debugger spe)
1323: indicated that
1324: bandwidth between the host and terminal would limit performance.
1325: This influenced the design of the host/terminal protocol and
1326: satisfactory performance was achieved.
1327: For example, when a
1328: .CW TermCore
1329: requests a callstack traceback from the terminal, the terminal computes the
1330: callstack locally, compares it to the last callstack sent to the host and
1331: transmits the difference \- usually only the deepest activation record has
1332: changed.
1333: .KS
1334: .PS
1335: HOST: circle "Pi and" "window" "package" rad .5
1336: TERM: circle at HOST+(2,0) "real-time" "graphics" rad .5
1337: TOTERM: arc -> cw from HOST.ne to TERM.nw
1338: TOHOST: arc -> cw from TERM.sw to HOST.se
1339: "definitions" at TOTERM.n+(0,.1)
1340: "operations" at TOHOST.s-(0,.1)
1341: LINE: line dashed from TOTERM.n-(0,.1) to TOHOST.s+(0,.1)
1342: ABOVE: line dashed from TOTERM.n+(0,.2) to TOTERM.n+(0,0.4)
1343: BELOW: line dashed from TOHOST.s-(0,.2) to TOHOST.s-(0,1.5)
1344: SUBJ: circle at HOST-(0,1.5) "host" "subject" "process" rad .5
1345: arrow " OS support" <-> from HOST.s to SUBJ.n
1346: PIDOTM: circle at HOST+(-2,0) "terminal" "agent" rad .5
1347: TOPIDOTM: line "remote" "proc call" <-> from HOST.w to PIDOTM.e
1348: TERMSUBJ: circle at PIDOTM-(0,1.5) "terminal" "subject" "process" rad .5
1349: arrow " OS support" <-> from PIDOTM.s to TERMSUBJ.n
1350: UP: line dashed from TOPIDOTM.c+(0,0.2) to TOPIDOTM.c+(0,1)
1351: DOWN: line dashed from TOPIDOTM.c-(0,0.2) to TOPIDOTM.c-(0,2.1)
1352: "host" at SUBJ.s-(0,0.2)
1353: "terminal" at TERM.s-(0,1.7)
1354: "terminal" at TERMSUBJ.s-(0,0.2)
1355: " . . ." at SUBJ.e
1356: " . . ." at TERMSUBJ.e
1357: .PE
1358: .ce
1359: Figure 11. Pi's three processes with two subject processes.
1360: .KE
1361: .PP
1362: How
1363: .CW Core
1364: finds the value of the subject's program counter is a simple example of
1365: inheritance and virtual functions at work.
1366: Consider part of the definition of
1367: .CW Core :
1368: .P1 0
1369: class Core {
1370: ...
1371: public:
1372: virtual int pc_index(); // register number for program counter
1373: virtual long reg_save(int r); // address at which register r saved
1374: virtual long peek_long(long a); // fetch value from memory at address a
1375: virtual long pc(); // fetch value of program counter
1376: ...
1377: };
1378: .P2
1379: The code here has been somewhat simplified to eliminate irrelevant complications;
1380: for example, it doesn't handle errors.
1381: .CW Core::pc()
1382: is target-independent, though it calls the target-dependent functions
1383: .CW pc_index() ,
1384: .CW reg_save() ,
1385: and
1386: .CW peek_long()
1387: to obtain its result:
1388: .P1 0
1389: long Core::pc()
1390: {
1391: return peek_long( reg_save( pc_index() ) );
1392: }
1393: .P2
1394: .CW pc_index() ,
1395: .CW reg_save()
1396: and
1397: .CW peek_long()
1398: must be implemented for each of
1399: .CW HostCore
1400: and
1401: .CW TermCore .
1402: For example, the program counter is register 15 on the VAX:
1403: .P1 0
1404: int HostCore::pc_index()
1405: {
1406: return 15;
1407: }
1408: .P2
1409: .CW reg_save()
1410: and
1411: .CW peek_long()
1412: have the target-dependent code to find the location at which an arbitrary register
1413: has been saved and read the contents of an arbitrary memory location, respectively.
1414: .PP
1415: Note that
1416: .CW Core::pc()
1417: is virtual; the derived classes
1418: .I may
1419: also redefine it.
1420: So, even though
1421: .CW TermCore
1422: could inherit this functionally correct
1423: .CW pc()
1424: from
1425: .CW Core ,
1426: it has its own version.
1427: The base version reads memory every time it needs the value of the program
1428: counter.
1429: For the terminal, this would mean a remote procedure call to the terminal every
1430: time.
1431: As an optimization,
1432: .CW TermCore
1433: keeps a copy of the program counter, updating it each time the state of the
1434: subject process is checked;
1435: .CW TermCore::pc()
1436: simply returns this cached value.
1437: The semantics are slightly different: if the program counter is manually patched
1438: while the program is halted,
1439: .CW TermCore::pc()
1440: will report the old value.
1441: In practice this discrepancy is less significant than the minor differences that
1442: arise from operating system idiosyncrasies.
1443: No user has ever noticed it.
1444: .PP
1445: Finding suitable target-independent base abstractions and implementing
1446: the derived classes took several months; simply building a new version of
1447: Pi specifically for the new target environment would have taken a few weeks.
1448: The
1449: .CW SymTab
1450: class was relatively straightforward, but tedious because of arbitrary
1451: differences in the detailed representation of the symbol tables.
1452: Two hard parts of finding an acceptable inheritance for
1453: .CW Core
1454: were byte ordering and function calling.
1455: The problems encountered with byte ordering are instructive \- the original
1456: scheme did not work on either machine, for reasons that no amount of forethought
1457: (by me) would have revealed.
1458: The scheme is to read memory from the subject process and create objects from
1459: which various types of data (byte, short, long, float, double) can be extracted
1460: later by clients of
1461: .CW Core .
1462: The VAX version failed when it tried to set up arbitrary bit patterns as
1463: candidates for extraction as floating point values; the operand of a floating
1464: move instruction must be a valid floating point number, of which 1 in 256 bit
1465: patterns is not.
1466: The WE32000 version did not work because the processor does not use the same
1467: byte ordering for code and data fetches; a multi-byte constant embedded in code
1468: is not the same bit pattern as that constant in data.
1469: Neither of these problems
1470: was hard to fix, but they indicate the difficulty of finding
1471: machine-independent abstractions for hardware.
1472: Harder was the interface through which the expression evaluator calls a function
1473: in the subject.
1474: The mechanisms for the host and terminal are quite different.
1475: For the host architecture, the debugger arranges that the subject process
1476: execute the function using the user's stack; in the terminal the function is
1477: executed directly by the debugger's agent process in the terminal on its own
1478: stack.
1479: The operation is broken down into a series of steps, each performed by
1480: a member of the respective derived
1481: .CW Core ,
1482: such that
1483: .CW Expr
1484: can detect no difference.
1485: The five steps supplied by
1486: .CW Core
1487: are: save context, allocate argument area, call function,
1488: determine location of returned result, restore context.
1489: .PP
1490: A further derivation from
1491: .CW HostCore
1492: has been added for examining core dumps from the
1493: .UX
1494: kernel on the VAX.
1495: .CW KernelCore
1496: differs very little from
1497: .CW HostCore ;
1498: the major change is that memory fetches must be mapped through the
1499: kernel's page tables.
1500: A derivation of
1501: .CW Core
1502: for S-Net, a multi-processor computer based on the Motorola MC68000,
1503: is being implemented at the time of writing.
1504: The current
1505: .CW Core
1506: hierarchy is shown in Figure 12.
1507: .KS
1508: .PS
1509: .ft CW
1510: CORE: "Core"
1511: HOSTCORE: "HostCore" at CORE+(-0.75,-0.75)
1512: TERMCORE: "TermCore" at CORE+( 0.75,-0.75)
1513: SNETCORE: "SNetCore" at CORE+( 0,-0.75)
1514: line from CORE.s-(0,.1) to HOSTCORE.n+(0,.1)
1515: line from CORE.s-(0,.1) to TERMCORE.n+(0,.1)
1516: line from CORE.s-(0,.1) to SNETCORE.n+(0,.1)
1517: KERNCORE: "KernelCore" at HOSTCORE+(0,-0.75)
1518: line from HOSTCORE.s-(0,.1) to KERNCORE.n+(0,.1)
1519: .ft P
1520: .PE
1521: .ce
1522: Figure 12. Current \f(CWCore\fP hierarchy.
1523: .KE
1524: .PP
1525: A single debugger that handles multiple target environments
1526: has been a success for both the users and the implementer.
1527: The user is guaranteed to see the same interface when debugging in all
1528: environments.
1529: When changes are made to target-independent parts of Pi
1530: they are usually only tested for one target before being installed
1531: \- they almost always work correctly for the others.
1532: This was true of adding a trace history of breakpoints, for example.
1533: More complicated changes, involving target-dependent parts, take some time
1534: before a clean compilation can be achieved, because several derived classes must
1535: be changed consistently.
1536: It often takes days to get an error-free compilation.
1537: It is frustrating to be unable to test new code for machine X because
1538: the code for machine Y is out of date and cannot compile.
1539: The discipline introduced is that thought must be given to all target
1540: environments simultaneously; this results in earlier exposure of
1541: target environment inconsistencies.
1542: .NH
1543: Deficiencies in C++
1544: .PP
1545: Though indispensable in the construction of Pi, C++ is deficient in two respects.
1546: First, derived classes may inherit from only a single base class.
1547: Second, the benefits provided by classes come at the expense of considerable
1548: compilation overhead.
1549: .PP
1550: Though each class derived from
1551: .CW Core
1552: is a single step from its parent in the type hierarchy, the step embodies
1553: several independent changes: the processor, the operating system and the
1554: compiler.
1555: If Pi had to support all four target environments possible with
1556: operating systems A and B on processors P and Q,
1557: the class hierarchy would be that of Figure 13.
1558: As a result, each derived class would contain target-dependent code replicated in
1559: two others.
1560: .KS
1561: .PS
1562: .ft CW
1563: CORE: "Core"
1564: AP: "APCore" at CORE+(-1.5,-1)
1565: BP: "BPCore" at CORE+( -.5,-1)
1566: AQ: "AQCore" at CORE+( .5,-1)
1567: BQ: "BQCore" at CORE+( 1.5,-1)
1568: line from CORE.s-(0,.1) to AP.n+(0,.1)
1569: line from CORE.s-(0,.1) to BP.n+(0,.1)
1570: line from CORE.s-(0,.1) to AQ.n+(0,.1)
1571: line from CORE.s-(0,.1) to BQ.n+(0,.1)
1572: .ft P
1573: .PE
1574: .ce
1575: Figure 13. Multiplicity of Derived Classes.
1576: .KE
1577: .LP
1578: This is a situation in which
1579: .I multiple
1580: .I inheritance
1581: could be used to eliminate replication.
1582: Under multiple inheritance a derived class may inherit from more than
1583: one base class.
1584: The derivation graph could be as shown in Figure 14.
1585: Target-dependent code would be confined to a single appearance in one of
1586: the base classes:
1587: .CW ACore ,
1588: .CW BCore ,
1589: .CW PCore
1590: and
1591: .CW QCore .
1592: The derived classes would need no additional code.
1593: .KF
1594: .PS
1595: .ft CW
1596: ACORE: "ACore"
1597: PCORE: "PCore" at ACORE+(1,0)
1598: BCORE: "BCore" at PCORE+(1,0)
1599: QCORE: "QCore" at BCORE+(1,0)
1600: AP: "APCore" at ACORE+(0,-1)
1601: BP: "BPCore" at PCORE+(0,-1)
1602: BQ: "BQCore" at BCORE+(0,-1)
1603: AQ: "AQCore" at QCORE+(0,-1)
1604: line from ACORE.s-(0,.1) to AP.n+(0,.1)
1605: line from ACORE.s-(0,.1) to AQ.n+(0,.1)
1606: line from BCORE.s-(0,.1) to BP.n+(0,.1)
1607: line from BCORE.s-(0,.1) to BQ.n+(0,.1)
1608: line from PCORE.s-(0,.1) to AP.n+(0,.1)
1609: line from PCORE.s-(0,.1) to BP.n+(0,.1)
1610: line from QCORE.s-(0,.1) to BQ.n+(0,.1)
1611: line from QCORE.s-(0,.1) to AQ.n+(0,.1)
1612: .ft P
1613: .PE
1614: .ce
1615: Figure 14. Multiple Inheritance.
1616: .KE
1617: .PP
1618: No pair of target environments have yet shared a common component,
1619: but time will certainly change that.
1620: Since C++ does not provide multiple inheritance, some other means of factoring
1621: the program must be found to avoid duplicating code.
1622: Of course, the success of multiple inheritance cannot be guaranteed without
1623: practical experience, but it is certainly worth pursuing.
1624: .PP
1625: A more severe problem that has been encountered is the cost of
1626: recompilation triggered by the modification of class declarations.
1627: The declaration of a class,
1628: .CW X ,
1629: places both the public and private components of its interface in
1630: a single syntactic unit.
1631: The declaration is usually stored in a ``header file,''
1632: .CW X.h .
1633: Two compilation problems arise with respect to
1634: .I clients
1635: of
1636: .CW X ,
1637: that is, other classes that depend only on
1638: .CW X 's
1639: public interface.
1640: Before processing the source text of a client the compiler must read the header
1641: file,
1642: .CW X.h .
1643: It therefore reads both the public and private declarations in
1644: .CW X ,
1645: even though the client's source is denied reference to the private declarations.
1646: (At the implementation level, the client might depend on this private information:
1647: to generate client code, the compiler needs to know the
1648: .I size
1649: of the private data in
1650: .CW X ,
1651: if an instance of
1652: .CW X
1653: appears in a client's stack frame.)
1654: If the private declarations in
1655: .CW X.h
1656: in turn depend on other header files, they must also be included, and so on.
1657: Compilation of the client therefore depends on many header files,
1658: even though the client does not need the information from those header files.
1659: This makes client compilation more expensive, because of the additional
1660: header files that must be processed.
1661: Moreover, using conventional Make |reference(feldman make) dependencies,
1662: client source is
1663: frequently recompiled after the modification of private declarations in
1664: classes unreferenced by the client.
1665: The subterfuge that partially overcomes this problem is unworthy of description.
1666: .NH
1667: Conclusion
1668: .PP
1669: Object-oriented programming in C++ has worked very well in Pi.
1670: Pi's ability to examine multiple processes over multiple target environments
1671: follows from the object-oriented model and class inheritance mechanism used in
1672: the implementation.
1673: At the outset the goal was to experiment with the user interface.
1674: Had an object-oriented programming language not been available,
1675: I doubt that Pi would have evolved beyond experiments at that level.
1676: .NH
1677: Acknowledgements
1678: .PP
1679: The success of Pi owes much to the ideas and software of
1680: Bjarne Stroustrup, Tom Killian and Rob Pike.
1681: Thanks also to Brian Kernighan, Doug McIlroy and John Linderman for
1682: their comments on drafts of this paper.
1683: .NH
1684: References
1685: .LP
1686: |reference_placement
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.