Home
Patent Search
IMT Blog
REGISTER
|
SIGN IN
United States Patent
5815653
You , ; et al.
September 29, 1998
Title
Debugging system with portable debug environment-independent client and non-portable platform-specific server
Abstract
A system for debugging software uses a portable debug environment-independent client debugger object and at least one non-portable server debugger object with platform-specific debugging logic. The client debugger object has a graphic user interface which allows a user to control and manipulate the server debugger object with debug environment-independent debug requests. The server debugger object performs a platform-specific debug operation on the software to be debugged. The platform-specific results generated by the debugging operation are translated to debug environment-independent results and returned to the client debugger object. This operation allows the same client debugger object to be used with one or more server debugger objects running on different platforms.
Inventors:
You; Lawrence L.
(San Jose,
CA
)
, Rajgopal; Narayan
(San Jose,
CA
)
, Wimble; Michael D.
(Sunnyvale,
CA
)
Appl. No.:
557989
Filed:
November 13, 1995
Current U.S. Class:
714/38
717/134
714/25
714/26
Current International Class:
G06F 11/36 (20060101)
Field of Search:
395/183.14,183.01,182.02,704,701,702,703,705,681,682,683 364/267,267.91,275.5
U.S. Patent Documents
4589068
May 1986
Heinen, Jr.
4821220
April 1989
Duisberg
4953080
August 1990
Dysart et al.
5041992
August 1991
Cunningham et al.
5050090
September 1991
Golub et al.
5060276
October 1991
Morris et al.
5075847
December 1991
Fromme
5075848
December 1991
Lai et al.
5093914
March 1992
Coplien et al.
5119475
June 1992
Smith et al.
5125091
June 1992
Staas et al.
5133075
July 1992
Risch
5140671
August 1992
Hayes et al.
5151987
September 1992
Abraham et al.
5181162
January 1993
Smith et al.
5297284
March 1994
Jones et al.
5315703
May 1994
Matheny et al.
5317741
May 1994
Schwanke
5325530
June 1994
Mohrmann
5325533
June 1994
McInerney et al.
5327562
July 1994
Adcock et al.
5339430
August 1994
Lundin et al.
5339438
August 1994
Conner et al.
5361352
November 1994
Iwasawa et al.
5371746
December 1994
Yamashita et al.
5421016
May 1995
Conner et al.
5423023
June 1995
Batch et al.
5428792
June 1995
Conner et al.
5432925
July 1995
Abraham et al.
5437027
July 1995
Bannon et al.
5446900
August 1995
Kimelman
5481708
January 1996
Kukol
5499343
March 1996
Pettus
5524253
June 1996
Pham et al.
5533192
July 1996
Hawley et al.
Foreign Patent Documents
03268033
Mar., 1990
JP
6465644
Mar., 1989
JP
Other References
Dumas, Joseph and Paige Parsons. "Discovering the Way Programmers Think: New Programming Environments." Communications of the ACM. Jun. 1995: pp. 45-56. .
Pascoe, Geoffrey A. "Encapsulators: A New Software Paradigm in Smalltalk-80." OOPSLA '86 Proceedings. Sep. 1986: pp. 341-346. .
Purtilo, James M. and Joanne M. Atlee. "Module Reuse by Interface Adaptation." Software--Practice and Experience. Jun. 1991: pp. 539-556. .
Lam, Siman S. "Protocol Conversion." IEEE Transactions on Software Engineering. Mar. 1988: pp. 353-362. .
Thatte, Satish R. "Automated Synthesis of Interface Adapters for Reusable Classes." POPL '94. Jan. 1994: pp. 174-187. .
Yellin, Daniel M. and Robert E. Strom. "Interfaces, Protocols, and the Semi-Automatic Construction of Software Adaptors." OOPSLA '94. Oct. 1994: pp. 176-190. .
Jacobson, Ivar and Fredrik Lindstrom. "Re-engineering of old systems to an object-oriented architecture." OOPSLA '91. pp. 340-350. .
Filman, Robert E. "Retrofitting Objects." OOPSLA '87. Oct. 1987: pp. 342-353. .
Dietrich, Walter C., Lee R. Nackman and Franklin Gracer. "Saving a Legacy with Objects." OOPSLA '89. Oct. 1989: 77-83. .
Dotts, Alan and Don Birkley. "Developments of Reusable Test Equipment Software Using Smalltalk and C." OOPSLA '92. Oct. 1992: pp. 31-35. .
Duntemann, Jeff and Chris Marinacci. "New Objects for Old Structures." BYTE. Apr. 1990: pp. 261-266. .
Alabiso, Bruno. "Transformation of Data Flow Analysis Models to Object-Oriented Design." OOPSLA '88. Sep. 1988: pp. 335-353. .
Madhavji, Nazim H., Jules Desharnais, Luc Pinsonneault, and Kamel Toubache. "Adapting Modules to an Integrated Programming Environment." IEEE International Conference on Programming Languages, 1988: pp. 364-371. .
Dutt, Nikil D. "Legend: A Language for Generic Component Library Description." IEEE International Conference on Computer Languages. 1990:198-207. .
Maybee, Paul. "NED: The Network Extensible Debugger." Summer '92 USENIX; Jun. 8-12 1992; pp. 145-156. .
Weissman, Ronald. "Unleashing the Power of client/server computing." Object Magazine, vol. 4, No. 1, pp. 38-39, Mar.-Apr. 1994. .
Complete Software; System Utilities Software Pkgs; Portable C Language Debugger for professional programmers, CDebug Version 2; Feb. 27, 1986; 1 page. .
Iyengar et al; "An event-based, retargetable debugger." Hewlett-Packard Journal, V.45, n6, p.(33)(11), Dec. 1994. .
Hood et al; Accommodating Heterogeneity in a Debugger--A client-Server Approach; IEEE Compt. Soc. Piers, Int. conference, Jan. 3-6/1995, vol. 2 pp. 252-253. .
Cheng et al; A Portable debugger for Parallel and Distributed program; IEEE Compt. Soc. Pres, Int. conference, Nov. 14-18/1994, pp. 723-732. .
Designing a Parallel Debugger for Portability; IEEE Compt. Soc. May et al.; Press; 1994; pp. 909-914. .
T.A. Cargil, PI:A Case Study in Object-Oriented Programming, Sep. 1986, pp. 350-360, OOPSLA '86 Proceedings. .
Borland International, Debugging in the IDE, Chapter 6, pp. 217-241, Turbo C.RTM.++, Version 3.0, User's Guide, 1992..~
Primary Examiner:
Le; Dieu-Minh
Claims
Having thus described our invention, what we claim as new, and desire to secure by Letters Patent is as follows:
1. A portable debugging system controllable by a user working at a client node for debugging one or more target processes, each of the target processes running on an associated platform-specific server node connected to the client node over a network wherein different server nodes can run on different platforms, the portable debugging system comprising:
(a) a portable client debugger object in the client node, the client debugger object having sender logic which transmits debug environment-independent debug requests over the network under control of the user and receiver logic which receives debug environment-independent debug results; and
(b) a non-portable server debugger object in each server node operating with the client debugger object, each server debugger object including platform-specific debugging logic which receives from the network the debug environment-independent debug requests generated by the client debugger object and performs a platform-specific debug operation on a target process running in the server node, which debug operation generates platform-specific results and answer logic which responds to the plafform-specific results by returning debug environment-independent debug results to the client debugger object.
2. The portable debugging system for debugging
one or more target processes according to claim 1, wherein said debug operation includes setting a breakpoint.
3. The portable debugging system for debugging one or more target processes according to claim 2, wherein said breakpoint is a logical breakpoint.
4. The portable debugging system for debugging one or more target processes according to claim 2, wherein said breakpoint is a transparent breakpoint.
5. The portable debugging system for debugging one or more target processes according to claim 1, wherein said debug operation includes setting a watchpoint.
6. The portable debugging system for debugging one or more target processes according to claim 5, wherein said watchpoint is a logical watchpoint.
7. The portable debugging system for debugging one or more target processes according to claim 1, further comprising logic in the client node which generates a visual display that can be manipulated by the user to control the sender logic to send the debug requests.
8. The portable debugging system for debugging one or more target processes according to claim 1, further comprising a display in the client node for displaying debug results received by the receiving logic.
9. A method practiced on a portable debugging system which is controllable by a user working at a client node for debugging one or more target processes, each of the target processes running on an associated platform-specific server node connected to the client node over a network wherein different server nodes can run on different platforms, the method comprising the steps of:
(a) creating a portable client debugger object in the client node, the client debugger object having sender logic which transmits debug environment-independent debug requests over the network under control of the user and receiver logic which receives debug environment-independent debug results; and
(b) creating a non-portable server debugger object in each server node operating with the client debugger object, each server debugger object including platform-specific debugging logic which receives from the network the debug environment-independent debug requests generated by the client debugger object and performs a platform-specific debug operation on a target process running in the server node, which debug operation generates plafform-specific results and answer logic which responds to the plafform-specific, results by returning debug environment-independent debug results to the client debugger object.
10. The method for debugging one or more target processes according to claim 9, comprising the step of:
setting a breakpoint in said one or more target processes.
11. The method for debugging one or more target processes according to claim 9, comprising the step of:
setting a logical breakpoint in said one or more target processes.
12. The method debugging one or more target processes according to claim 9, comprising the step of:
setting a transparent breakpoint in said one or more target processes.
13. The method debugging one or more target processes according to claim 9, comprising the step of:
setting a watchpoint in said one or more target processes.
14. The method debugging one or more target processes according to claim 9, comprising the step of:
setting a logical watchpoint in said one or more target processes.
15. The method for debugging one or more target processes according to claim 9, further comprising the step of:
(c) generating a visual display that can be manipulated by the user to control the sender logic to send the debug requests.
16. The method for debugging one or more targets processes according to claim 9, further comprising the step of:
(d) displaying debug results received by the receiving logic.
17. A portable debugging system controllable by a user working at a client node for debugging one or more target processes, each of the target processes running on an associated platform-specific server node connected to the client node over a network wherein different server nodes can run on different platforms, the portable debugging system (comprising:
(a) a graphic user interface in the client node which can be manipulated by the user to transmit debug environment-independent debug requests over the network;
(b) a non-portable server debugger in the server node which responds to debug environment-independent debug requests transmitted by the client node by performing a platform-specific debug operation on a target process, which debug operation generates platform-specific debug results;
(c) reply logic which responds to the platform-specific debug results by returning debug environment-independent debug results to the client node; and
(d) receive logic in the client node which receives and displays the debug environment-independent debug results.
18. A method practiced on a portable debugging system which is controllable by a user working at a client node for debugging one or more target processes, each of the target processes running on an associated platform-specific server node connected to the client node over a network wherein different server nodes can run on different platforms, the method comprising the steps of:
(a) manipulating a graphic user interface in the client node to transmit debug environment-independent debug requests;
(b) responding in the platform-specific server node to debug environment-independent debug requests transmitted by the client node by performing a platform-specific debug operation on a target process, which debug operation transmits platform-specific debug results;
(c) returning, in response to the platform-specific debug results, debug environment-independent debug results to the client node; and
(d) receiving in the client node and displaying the debug environment-independent debug results.
19. A computer program product portable debugging system controllable by a user working at a client node for debugging one or more target processes, each of the target processes running on an associated platform-specific server node connected to the client node over a network wherein different server nodes can run on different platforms, the computer program product comprising a computer usable medium having computer readable program code thereon, including:
(a) program code which generates a graphic user interface in the client node which graphic user interface can be manipulated by the user to transmit debug environment-independent debug requests;
(b) program code in the server node which responds to debug environment-independent debug requests generated by the client node by performing a platform-specific debug operation on a target process, which debug operation generates platform-specific results;
(c) program code in the server node which responds to the plafform-specific results by returning debug environment-independent debug results to the client node; and
(d) program code in the client node which receives and displays the debug environment-independent debug results.
Description
COPYRIGHT NOTIFICATION
Portions of this patent application contain materials that are subject to copyright protection. The copyright owner has no objection to the facsimile reproduction by anyone of the patent document or the patent disclosure, as it appears in the Patent and Trademark Office patent file or records, but otherwise reserves all copyright rights whatsoever.
RELATED APPLICATIONS
The following U.S. patent applications contain subject matter related to the subject matter of this application, U.S. patent application Ser. No. 08/556,318, filed Nov. 13, 1995 by Lawrence L. You and entitled PORTABLE DEBUGGING SERVICES UTILIZING A CLIENT DEBUGGER OBJECT AND A SERVER DEBUGGER OBJECT WITH FLEXIBLE ADDRESSING SUPPORT and U.S. patent application Ser. No. 08/557,660, filed Nov. 13, 1995 by Lawrence L. You et al. and entitled PORTABLE DEBUGGING SERVICE UTILIZING A CLIENT DEBUGGER OBJECT AND A SERVER DEBUGGER OBJECT, now U.S. Pat. 5,787,245.
BACKGROUND OF THE INVENTION
1. Field of the Invention
The present invention generally relates to computer aided software engineering (CASE) and, more particularly, to program debugging in an interactive and dynamic environment for computer program building and debugging. The invention allows a programmer to cross-debug programs from an interactive programming environment to target execution environments. These target environments can differ from the programming environment by their computer processor architectures, by their operating systems, by their runtime environments, by their code formats, by their symbolic debugging information formats. In addition, the invention allows a programmer to simultaneously cross-debug programs on more than one target environment using a single debugger.
Debuggers are highly dependent on the details of target execution environments. Debugging services, when they are provided, differ widely in their features; they are highly non-portable in their interface and their semantics. The invention establishes a program framework from which to build a set of services; it also establishes a portable client interface and nonportable client interface extensions. Portable debugging services benefit developers of debuggers by providing a single programming interface and programming model. The portable services benefit software developers using a programming environment by enabling debuggers to target multiple programs on heterogeneous environments simultaneously, and by allowing remote as well as local debugging.
2. Description of the Prior Art
Object oriented programming (OOP) is the preferred environment for building user-friendly, intelligent computer software. Key elements of OOP are data encapsulation, inheritance and polymorphism. These elements may be used to create program frameworks, which define abstractions for software objects that can be reused as common program code, and augmented through customization. While these three key elements are common to OOP languages, most OOP languages implement the three key elements differently.
Examples of OOP languages are Smalltalk and C++. Smalltalk is actually more than a language; it might more accurately be characterized as a programming environment. Smalltalk was developed in the Learning Research Group at Xerox's Palo Alto Research Center (PARC) in the early 1970s. In Smalltalk, a message is sent to an object to evaluate the object itself. Messages perform a task similar to that of function calls in conventional programming languages. The programmer does not need to be concerned with the type of data; rather, the programmer need only be concerned with creating the right order of a message and using the right message. C++ was developed by Bjarne Stroustrup at the AT&T Bell Laboratories in 1983 as an extension of C. The key concept of C++ is class, which is a user-defined type. Classes provide object oriented programming features. C++ modules are compatible with C modules and can be linked freely so that existing C libraries may be used with C++ programs.
The complete process of running a computer program involves translation of the source code written by the programmer to machine executable form, referred to as object code, and then execution of the object code. The process of translation is performed by an interpreter or a compiler. In the case of an interpreter, the translation is made at the time the program is run, whereas in the case of a compiler, the translation is made and stored as object code prior to running the program. That is, in the usual compile and execute system, the two phases of translation and execution are separate, the compilation being done only once. In an interpretive system, such as the Smalltalk interpreter, the two phases are performed in sequence. An interpreter is required for Smalltalk since the nature of that programming environment does not permit designation of specific registers or an address space until an object is implemented.
A compiler translates the information into machine instructions, which are defined by the processor architecture of the machine which will execute the program. The translation process varies based on the compiler program itself, the processor architecture, the target runtime execution environment, compiler options, target operating system, and on programming interfaces which define services or features available in software packages or libraries. These factors can prevent software portability due to incompatibilities including differences in language syntax, compiler features, compiler options, from ambiguities in the computer language definition, from freedom in the compiler's interpretation in the language, in processor memory addressing modes and ranges, in ordering of memory (byte ordering), and a lack of programming interfaces or features on a target machine. Software portability is a goal for programmers who desire that their software execute on a variety of target platforms. Nonportable programming interfaces hinder the porting process.
Once a program has been compiled and linked, it is executed and then debugged. Because logical errors, also known as "bugs," are introduced by programmers, they will want to detect and understand the errors, using a program debugger. After correcting the errors and recompiling, they use the debugger to confirm that those errors have been eliminated. Other uses for the debugger include inspecting executing programs in order to understand their operation, monitoring memory usage, instrumenting and testing programs, verifying the correctness of program translation by the compiler, verifying the correctness of operation of other dependent programs, and verifying the operation of computer hardware.
Debuggers provide the program with information about the execution state of the running program as well as control of it. Program state includes program and data memory; hardware registers; program stacks; and operating system objects such as queues, synchronization objects, and program accounting information. Debuggers control programs with operations to start, stop, suspend, terminate, step over instructions, step into branches, step over statements, step through subroutine calls, stop at breakpoints, and stop at data watchpoints. Source-level, or symbolic debuggers present the state of executing programs at a high-level of abstraction, closely representing the execution of the program as if the source code were native computer operations.
Noninteractive debuggers usually lack the ability to control programs. They often only allow the programmer to inspect the state after a program has terminated. These are generally called "postmortem debuggers."
Interactive debuggers provide the programmer access to the state of programs while they are running. They allow the programmer to interact with the running program and control its execution.
Hardware debuggers are another class of debuggers which are used to check the operation of programs at a primitive level. For instance, they allow a programmer to view the operation of the CPU as it executes each instruction, or to view data in memory with a limited presentation such as a binary or hexadecimal output. These debuggers are not usually useful to programmers using high-level languages such as C++ because the type of data they provide is highly mismatched with the source code a programmer is debugging.
High-level symbolic debuggers attempt to let a programmer view running programs with the same level of abstraction as the original source code. Because the source code is compiled into machine instructions, the running programs are not actually being executed in the CPU itself. Instead, machine code translations execute "as if" the source code were real operations that could be carried out by the CPU. In order to present the programmer with a view of their program that closely matches their own perception of how the program is operating, high-level symbolic debuggers must use symbolic debugging information provided by the compiler which let the debugger, in essence, perform a reverse translation from the machine instructions and binary data, back into the source code.
The symbolic information used to perform this translation exists in the form of maps which allow the debugger to translate addresses of machine instructions back into source code locations, or to translate data on a CPU program stack back into the local variables declared in the source code.
Traditional program debuggers are limited in many aspects. Some debuggers are dedicated to debug programs generated by a particular development environment. Others may be semiportable but may not be able to target more than one environment at a time. Still others can only debug single process programs, and are not truly interactive.
The difficulty for interactive software debuggers is providing a programmer with the ability to inspect and control all aspects of executing and nonexecuting software computer programs. Debuggers extract information from the host computer system. The host computer system often only provides this information in the form of operating system calls, low-level driver subroutines, memory, hardware registers, or through a wide range of services. Unfortunately, because engineering designs are not driven by a unifying debugging design, the services are often developed ad hoc, simply revealing the data in any manner that is convenient, or by defining control interfaces to executing programs that are convenient to the implementor of the operating system services. And although a general hardware design may consist of a CPU, memory, and I/O or peripheral devices, similarity in computer design may end there. Because of the extent of such disparity in design, very few debuggers have been designed with portability in mind.
Some debuggers, such as gdb or dbx, are said to be "portable" in that some common source code is shared among implementations of debuggers that target heterogeneous platforms. This category of program debuggers are limited for several reasons: they are designed to only allow debugging of a single target environment at a time; they do not have necessary features to support interactive debugging through asynchronous user operations; program reuse is limited in that abstractions created for one target environment cannot always be used widely across a wide host of target systems; and they require extensive configuration of the program source code before building.
The Portable Debugging Services approach solves these problems by specifying an architecture for the services, an implementation for the framework expressing the design and protocol of the architecture, and by using extensions to the framework via inheritance and polymorphism, two features of object-oriented programming principles.
The Pi debugger, by Thomas Cargill at AT&T Bell Labs, is implemented using object-oriented techniques, but not for the purposes of developing portable debugging services. Instead, Cargill created a set of programming abstractions which could be used to represent class and data structures of programs. The Pi debugger does not have abstractions to support debugging multiple target environments concurrently.
Program debuggers are often used for local debugging, meaning that the development programming environment running the full debugger is the same host running the target program. Some program debuggers allow remote debugging, but do not use a uniform communication model for both. The Portable Debugging Services approach is to use a unified model of client-server debugger communication. This mechanism does not only work on homogeneous environments, but it also functions transparently on heterogeneous environments.
SUMMARY OF THE INVENTION
It is therefore an object of the present invention to provide a portable and dynamic process for the debugging of computer programs which promotes better developer focus and concentration, and hence greater productivity.
The portable debugging system is composed of a client debugger object, a connection object and a server debugger object. Each server debugger object is responsible for a particular debug environment. The client debugger object is designed to execute in any client debugger environment. The connection object facilitates communication between a client debugger object and a server debugger object. The client debugger object transmits debug requests to a target server debugger object. The connection object is responsible for routing the request to the target server debugger object.
The client-server model typically operates under a common pattern: the client initiates a request to a server, the server services the request, and the server replies to the request. In a client-server model, the client may be registered for notification, in which case the server may handle events asynchronously with respect to client requests. These events, such as a target program stopping at a breakpoint, can be sent back to the client on a "reverse connection." This allows the debugger to be asynchronous at the client as well as the server. The information sent from server to client are notification objects.
BRIEF DESCRIPTION OF THE DRAWINGS
FIG. 1 illustrates a typical hardware configuration of a computer in accordance with the subject invention;
FIG. 2 is a block diagram showing a client/server connecting through a debugger server to target processes in accordance with a preferred embodiment;
FIG. 3 is a block diagram of an identifier-object and a full-object in accordance with a preferred embodiment;
FIG. 4 is a block diagram of portable collection objects in accordance with a preferred embodiment;
FIG. 5 is a block diagram of templatized class object and the scalar object in accordance with a preferred embodiment;
FIG. 6 is a linked list of linkable objects in accordance with a preferred embodiment of the invention;
FIG. 7 presents the control flow for two different cases of streaming in accordance with a preferred embodiment;
FIG. 8 shows the receiver side of connection processing in accordance with a preferred embodiment;
FIG. 9 shows the inheritance graph for sample classes in accordance with a preferred embodiment;
FIG. 10 shows the structure for a 64-bit address object in accordance with a preferred embodiment;
FIG. 11 shows the logic associated with preparing a client request in accordance with a preferred embodiment;
FIG. 12 shows the logic associated with request processing in accordance with a preferred embodiment;
FIG. 13 shows the logic associated with the main server dispatch loop in accordance with a preferred embodiment;
FIG. 14 illustrates the logic associated with event server processing in accordance with a preferred embodiment;
FIG. 15 shows the detailed logic associated with a client connecting to a debugger in accordance with a preferred embodiment;
FIG. 16 illustrates the memory layout for a breakpoint class in accordance with a preferred embodiment;
FIG. 17 illustrates the memory layout of the TPrimitiveWatchpoint object in accordance with a preferred embodiment; and
FIG. 18 is a linked list diagram of debug address and its associated universal address in accordance with a preferred embodiment.
DETAILED DESCRIPTION OF A PREFERRED EMBODIMENT OF THE INVENTION
Definitions
Portable Debugging Services
The Portable Debugging Services, hereafter called PDS, is made up of a programming architecture and implementations of the architecture.
The PDS architecture defines structure and protocol, but not the implementation of the debugging services. The architecture defines the abstractions of the individual object-oriented classes, the abstraction of the debugging services architecture, configuration, organization, and interaction of the instances of objects used within the program architecture. The architecture is portable and designed to serve the needs of a wide range of program analysis and debugging tools.
The implementations are concrete manifestations of the debugging services. An individual implementation of the debugging services is composed of the framework and target-specific code.
Object-Oriented Program Framework
The object-oriented program framework, or framework, is a single, primitive, and portable implementation of the abstractions that express program code that can be reused within the domain of a program that is specific to a particular problem. In this case, the individual problem domains are the target execution environments in which programs execute.
Debugger
A debugger, or interactive program debugger, is a programming tool which is instrumental in allowing a programmer to inspect and control the execution of a program. It is a programming tool that has many features and many uses; some example uses include:
A programmer may use the tool to gather information about the logical execution of an algorithm by watching the locations of code being executed during the prototyping stages of development. The debugger can provide information about the program, showing the lines or statements of source code being executed or program variables and their values. It can also be controlled through commands which let the programmer set breakpoints in memory such that the program will stop at those locations.
A program exhibits an error and is terminated abnormally. A programmer can use a debugger to inspect the state of the program.
A program built on top of a primitive debugger can exploit the capabilities of the debugger to gather dynamic information about the program. When a target program is instrumented in this fashion, a programmer can determine the number or frequency of program calls, subroutine calling patterns in the program, and other information which is not available statically. Further uses of dynamic analysis allows a programmer to determine memory usage, performance bottlenecks, and code coverage; this data is used to improve code quality.
Noninteractive Program Debuggers
Noninteractive program debuggers are used to analyze the programs without controlling their execution. One class of these debuggers are postmortem debuggers which allow a programmer to inspect the state of a program after its termination.
Interactive Program Debuggers
Interactive program debuggers allow the user to inspect and control the execution of programs. They may also allow the user to modify the program's state and change the program's execution to behavior that is different from the semantics of the original source programs. Interactive debuggers may have textual or graphical user interfaces.
Symbolic Debuggers
Symbolic Debuggers are a class of debuggers which allow a programmer to view his program through the representation of program symbols. The program symbols are typically the names of subroutines, classes, types, variables, and other program constructs as defined by a high-level programming language. The symbolic debugger typically allows a programmer to watch the correlation between their original source code and compiled code.
Source-Level Debuggers
The term source-level debugger is used interchangeably with symbolic debugger.
Machine Language
The machine language is composed of the instructions defined by a computer microprocessor. These instructions are the most primitive instructions and only true operations that can occur on the microprocessor.
Assembly Language
Assembly languages map directly to the machine language. They are generally defined in terms of mnemonics, which are names for individual machine language instructions; macros, which allow an assembly language programmers to compose their programs using groups of mnemonics. It is a low-level programming language which is useful in creating programs where the programmer selects individual instructions and their ordering.
High-Level Language
High-level programming languages are an improvement over machine or assembly language in their level of expression, readability and abstraction, whereby the syntax and semantics of the language may represent many machine language instructions with individual program statement. Additionally, data structures and algorithms are more clearly represented using the constructs of the language.
Source Code
The source code is the original data created by a programmer representing his program in any language. The source code is used as a starting point from which programs are built.
Object Code
When programs are compiled or assembled, i.e. they are translated from their original source code, the end result is the object code, or executable code. In some cases, programs may execute without ever being translated into object code, for example, interpreted programs.
Linker
Object code from individual pieces of source code may be combined to form subroutine and class libraries or they may be combined to form executable programs. This step is done by a program linker.
Compiler
A program compiler translates a programmer's source code into executable object code. The compiler interprets the source code, which is often text entered by a program editor by the programmer, using the rules of a language. The language defines the syntax and the semantics of the language; the compiler translates the code, which follows the syntax, into an executable program which exhibits behavior following the semantics. Because the translation process is inherently a modifying process, the compiler also generates symbolic debugging information, which is used by a symbolic debugger to interpret the executing program, thereby displaying program state and controlling program execution.
Interpreter
A program may be interpreted instead of compiled. The program interpreter will read the source code, and execute it as it reads it. This form of program execution is dynamic, in that binding of variables to their locations, binding callers of subroutines to the subroutines themselves, and other interpretation of the program occurs as the program is being executed, and not statically, as a compiler will do before a program starts execution.
Target
The target is a shorthand term for target process, target host or machine, target processor, or anything related to the execution of a debugged program.
Target Host
The target host is the CPU on which the target program and debugger server execute.
Target Execution Environment
When a program is executing, it exists within a target execution environment. The environment includes the hardware, operating system, runtime environment, and any other aspects of the computer on which a target process executes.
Client Debugger
A debugger can be composed of many pieces. In a separated client-server debugger architecture, the debugger server executes as a process on the target machine. The client of the debugger server, or client debugger, which is a part of the high-level debugger, is separated logically from the server. Location of this client debugger does not necessarily have to be executing on the same host as the server.
Client Debugger Execution Environment
When a client debugger is executing, it exists within a client debugger execution environment. The environment includes the hardware, operating system, runtime environment, and any other aspects of the computer on which a client debugger executes
Runtime Environment
The runtime environment is made up of resources and services available to the execution of a program. The environment exists within a process and may include a program heap for dynamic memory storage, a runtime type system for determining object types, shared libraries which contain code that can be used across address spaces, and other services specific to that particular instance of program execution.
Debugger Client
In the client-server debugger architecture, the debugger client is a set of programming interfaces which define the services provided by the server. The debugger client may also be the program which uses those interfaces.
Debugger Server
The major functionality in the portable debugging services are provided by a debugger server which is a process that executes on the target host. The server accepts requests from a debugger client, subsequently processing the request and replying to the request.
Synchronous Debugger
Program debuggers may be fully synchronized with the target programs they execute. These are called synchronous debuggers. When these debuggers execute, they are in one of two modes: an interactive mode, or a mode suspended until the target processes' changes their execution mode. When these debuggers execute, they may only debug a single target program at a time because they can only issue a single operation to start a target program at a time. In this mode of synchronous debugging, the operations are limited because the user of the debugger may not change the state or control flow of the target program without first stopping the program. It is also limited in its ability to debug multithreaded programs in as much as it prevents multithreaded programs to stop one thread at a time. Synchronous debuggers are generally not designed to be asynchronous debuggers.
Asynchronous Events
When an event occurs and is communicated without respect to the execution state of another process or thread, it is said to be an asynchronous event. For such a condition to occur, a system must be concurrent, in which there are multiple CPUs executing concurrently, or through simulated concurrency in which threads are switched during the program's execution. Multiple threads can execute concurrently or through simulated concurrency in separate processes.
Asynchronous Debugger
A program debugger that can process events from multiple target programs is said to be an asynchronous debugger. This model of debugging differs from synchronous debugging because multiple processes and threads can be executing under the control of a single debugger. Each thread or process can cause events which the asynchronous debugger processes. An asynchronous debugger is free to execute while a target thread or process is executing. Multithreaded programs can be debugged so that some threads may be stopped and others remain executing while the debugger is also executing. Synchronous debugging then is a subset of functionality provided by asynchronous debugging. The PDS is an asynchronous debugger.
Streaming
An object-oriented abstraction for reading and writing data is called streaming. A stream is an object that can be written into or read from; its protocol is ultimately made up of reading and writing primitive language types and blocks of data. A well-specified programming interface convention allows arbitrary objects to write their internal state into a stream. This process is called streaming out, as in "streaming out an object to a file-based stream." Conversely, another programming interface convention allows those objects to read their state from a stream. This process is called streaming in, as in "streaming in an object from a network-based stream."
Overview of the Invention
PDS is an architecture for debugging services that can be used for debugging. It is portable in that a single architecture and single framework can be executed on a wide variety of platforms, which may vary in their microprocessor family, number of processors, processor register sizes, numbers of processor registers, memory addressing, operating system, communication models, hardware debugging support, operating system debugging support, software and exception handling, runtime library and loader models, executable file formats, compilers, and a multitude of other parameters.
Client-Server Overview
The portable debugging services (PDS) architecture is composed of a client-server program model. In this model, a single debugger server executes on the target host. A host-specific implementation which uses the PDS framework uses features in the host operating system for determining debugging information.
The client debugger may execute in any client debugger execution environment provided there are connection objects supporting the client-server connection.
Connection objects are objects that allow a client and server to communicate. They are also used on a "reverse connection" which is used by a server to initiate delivery of notification messages to the client debugger.
FIG. 2 illustrates a client-server debugging system in accordance with a preferred embodiment. The client debugger user interface 201, provides a window into the target process 204 controlled by the debugger server 203 via the connection object
202.
The client and server communicate with these connection objects. The communication protocol allows primitive objects to stream themselves across from a sender side of the connection object across to the receiver side. The client and server use many abstractions to encapsulate various information about process state, thread state, execution state, and so on.
The client-server model typically operates under a common pattern: the client initiates a request to a server, the server services the request, and the server replies to the request. In the PDS client-server model, the client may be registered for notification, in which case the server may handle events asynchronously with respect to client requests. These events, such as a target program stopping at a breakpoint, can be sent back to the client on a "reverse connection." This allows the debugger to be asynchronous at the client as well as the server. The information sent from server to client are notification objects.
Client-Server Abstractions
The primitive objects containing the data that are exchanged between the client and server encapsulate a wide range of information which describe the state of the target host and server, the target processes, the target threads, runtime information, breakpoint and watchpoint descriptions, client identification, stacks and register values, and software and hardware exception handling parameters.
Collections
A basic object-oriented construct is the collection. This object is used as a container for other objects. The method for storage and retrieval is dependent on the uses for the objects and the common modes of access. A method call template classes is used to reuse code which is defined by template, i.e. a set of C++ code which can be used to operate on generic or parameterized types. This mechanism for reuse in the language differs from the polymorphic reuse. Instead of defining a function interface that is shared over many subclasses, the template class reuses interfaces by effectively substituting the parameterized type classes directly into the code, compiling instances of the template class.
MDebuggerCollectible
A primitive base class, used by many classes that are stored in collections, is the MDebuggerCollectible class. This class defines a protocol for streaming and storage in collections.
______________________________________ class MDebuggerCollectible { public: MDebuggerCollectible( ); virtual MDebuggerCollectible( ); virtual TDebuggerStream& operator>>=(TDebuggerStream&) const; virtual TDebuggerStream& operator<<=(TDebuggerStream&); virtual MDebuggerCollectible* X.sub.-- Clone( ) const; virtual long Hash( ) const; virtual bool IsEqual(const MDebuggerCollectible*) const; virtual bool IsSame(const MDebuggerCollectible*) const; bool operator==(const MDebuggerCollectible&) const; bool operator!=(const MDebuggerCollectible&) const; }; ______________________________________
The abstract base class defines no data members.
The operator>>= streaming function allows the object to stream itself out into the provided stream parameter. Likewise, the operator<<= streaming function allows the object to stream itself in from the provided stream parameter.
The x.sub.-- Clone function is used to create copies of the object being used. The implementation of this clone requires macros to perform the mechanical work of instantiating a copy of the object.
The Hash member function allows the object to describe itself in terms of a scalar hash value.
The IsEqual function allows comparison of objects. The test will return true if equal and false if not.
Another function, ISsame, is called for testing identity, meaning that if the object specified as the parameter is the same as the one whose IS same function is being called, the result will be true. Otherwise, the result is false.
The member function operator== is provided for testing equality (which calls the IsEqual function). Similarly, the member function operator!= is provided for testing inequality.
TPDCollection
The abstract base class used to define collections is called TPDCollection. This class defines protocol which is common to all collection classes.
In the following example C++ class interface, the collection class has a parameterized type named AObject. As defined by the C++ language standard, when the template class is instantiated, the compiler will pick up a parameterized type, inserting the formal type as necessary where the AObject symbol is used.
In FIG. 4, the collection 41 represents a possible layout for the memory locations in which pointers to the objects of type AObject are stored. Memory locations 411, 412, 413, 414, 415, and 416 contain pointers to the individual objects that are stored in the collection class. The actual implementation of the class is dependent on the type of class. The base class TPDCollection only specifies the protocol for access, metrics for sizing, and iteration. In FIG. 4, memory location 411 points to an object 42 which is said to be stored or owned by the collection class. Likewise, memory location 412 points to an object 43 and memory location 415 points to an object 44.
______________________________________ template<class AObject> class TPDCollection { public: TPDCollection( ); TPDCollection (const TPDCollection<AObject>& source); virtual TPDCollection( ); virtual TDebuggerStream& operator>>=(TDebuggerStream& dstStream) const = 0; virtual TDebuggerStream& operator<<=(TDebuggerStream& srcStream) = 0; virtual PrimitiveDbgCollCount GetCount( ) const = 0; virtual PrimitiveDbgCollCount GetSize( ) const = 0; virtual AObject* Get(const AObject&) const = 0; virtual AObject* Remove(const AObject&) = 0; virtual void DeleteAll( ) = 0; virtual TPDCollectionIterator<AObject>* CreateIterator( ) = 0; }; ______________________________________
The TPDCollection class can be constructed by default or by copying another TPDCollection class (also parameterized over the same type). The latter of these guarantees deep-copy semantics.
The operator>>= streaming function allows the object to stream itself out into the provided stream parameter. Likewise, the operator<<= streaming function allows the object to stream itself in from the provided stream parameter. The contained objects (of the parameterized type AObject) are streamed out monomorphically.
To determine the number of objects stored in the collection, a program would call the GetCount member function. To determine the effective storage size of the collection, i.e. the allocated space used by the collection, the Get Size function would be called.
Retrieval from the collection has a uniform, polymorphic interface. This is achieved by calling the Get function which takes an object of the parameterized type. FIG. 3 includes a description of two objects with the same structure containing differing amounts of data. Depending on the subclass, a mechanism is used whereby objects are constructed in one of two ways: an "identifier" style object 31 and a "full" style object 32. The "identifier" style of object contains an identification information 311 necessary to test that the object 31 being retrieved is the same as the one that is stored 32. In object 31, there may be no data stored. However in object 32, the data members stored as instance data is stored in the remainder of the object 322.
An object can be removed by specifying an identification object and passing it to the function Remove.
When the objects held by the collection class need to be deleted from memory, the DeleteAll member function is called. This deallocates and destructs each AObject object stored by the collection class and any pointer memory or storage that refers to the objects are also marked as empty.
Finally, the CreateIterator function polymorphically creates an iterator which is specific to the class and instance of collection class.
TPDCollectionIterator
______________________________________ template<class AObject> class TPDCollectionIterator { public: TPDCollectionIterator( TPDCollection<AObject>*); virtual .about.TPDCollectionIterator( ); virtual AObject* Next( ) = 0; virtual AObject* First( ) = 0; protected: TPDCollectionIterator( ); TPDCollection<AObject>* GetCollection( ); private: TPDCollection<AObject>* fCollection; }; ______________________________________
The PDS uses the collections to store, retrieve, and iterate through data. The act of iteration requires creating an iterator which is bound specifically to an instance of a collection class. Accordingly, the iterator must be created with the correct type. The AObject type for the iterator class must be the same as the AObject type specified for a collection class whose objects are to be iterated over.
The pointer to the collection over which iteration is to occur is specified when the iterator is constructed.
After construction, the owner of the iterator may call the First function, which returns a pointer to the first object in the list. It is not required that the iterator return objects in any particular order unless the contract of the collection class with the client of the class specifies one. For instance, an ordered set class may require that individual objects returned by the iterator are in a particular order but a set class would not. Whenever it is required that the iteration is to start over again, the First function is called again.
After receiving a pointer to the first iterated object, the client may call the Next function which will return the next object after the last one returned. Subsequent calls will return objects to subsequent objects in the collection.
The data member fCollection is a pointer to the collection over which is being iterated.
In FIG. 4, an example of an iterator is shown by iterator object 6, which contains an index to the memory locations which contain pointers to the objects 2, 3, and 4. As the iterator walks through the data structure, first 11, then 12, then 13, and so on until 16, it will only return when it returns values which contain non-NIL values.
TPDSet
One concrete implementation of the TPDCollection abstract base class is the TPDSet class. This is a very generic class which is also used to support storage of a wide variety of types. Objects can be of any type and are stored in an unordered manner.
______________________________________ template<class AObject> class TPDSet : public TPDCollection<AObject> { public: friend class TPDSetIterator<AObject>; TPDSet ( ); virtual .about.TPDSet( ); TPDSet<AObject>& operator=(const TPDSet<AObject>&); virtual TDebuggerStream& operator>>=(TDebuggerStream& dstStream) const; virtual TDebuggerStream& operator<<=(TDebuggerStream& srcStream); PrimitiveDbgCollCount GetCount( ) const; PrimitiveDbgCollCount GetSize( ) const; virtual void Add(const AObject&); virtual void Adopt(AObject*); virtual AObject* Get(const AObject&) const; virtual AObject* Remove(const AObject&); virtual void Delete(const AObject&); virtual void DeleteAll( ); virtual TPDSetIterator<AObject>* CreateIterator( ); protected: PrimitiveDbgCollCount FindEmptySlot( ) const; PrimitiveDbgCollCount FindObject(const AObject&) const; private: PrimitiveDbgCollCount fCount; PrimitiveDbgCollCount fCollectionSize; AObject** fObjectList; }; ______________________________________
This simple implementation is constructed with no arguments.
A set can be copied in whole using deep-copy semantics using the operator= member function.
The operator>>= streaming function allows the object to stream itself out into the provided stream parameter. Likewise, the operator<<= streaming function allows the object to stream itself in from the provided stream parameter. The contained objects (of the parameterized type AObject) are streamed out monomorphically.
To determine the number of objects stored in the set, a program would call the GetCount member function. To determine the effective storage size of the set, i.e. the allocated space used by the collection, the GetSize function would be called.
When an object is to be stored in the collection, the Add function is used. It will make a copy of the object which is passed in as a parameter and store the copy in the collection. Similarly, the Adopt function will take the object itself and store it directly into the collection, but without first making a copy.
Retrieval from the collection has a uniform, polymorphic interface. This is achieved by calling the Get function which takes an object of the parameterized type.
An object can be removed by specifying an identification object and passing it to the function Remove.
When the objects held by the collection class need to be deleted from memory, the DeleteAll member function is called. This deallocates and destructs each AObject object stored by the collection class and any pointer memory or storage that refers to the objects are also marked as empty.
Finally, the CreateIterator function polymorphically creates a TPDSetIterator (below).
The actual implementation of the set varies. The underlying implementation may use an array or a hash table.
TPDSetIterator
______________________________________ template<class AObject> class TPDSetIterator : public TPDCollectionIterator<AObject> { public: TPDSetIterator(TPDSet<AObject>*); virtual .about.TPDSetIterator( ); virtual AObject* Next( ); virtual AObject* First( ); private: void Reset( ); private: PrimitiveDbgCollCount fIndex; }; ______________________________________
The TPDSetIterator class derives from the TPDCollectionIterator class and uses an index into a hash table or array, stored in the f Index data member.
The pointer to the TPDSet over which iteration is to occur is specified when the iterator is constructed.
After construction, the owner of the iterator may call the First function, which returns a pointer to the first object in the list. It is not required that the iterator return objects in any particular order. Whenever it is required that the iteration is to start over again, the First function is called again.
After receiving a pointer to the first iterated object, the client may call the Next function which will return the next object after the last one returned. Subsequent calls will return objects to subsequent objects in the collection.
The Reset function is incidental to this particular implementation of the TPDsetIterator class and is called to reset the internal index before the First member function is called.
TScalarKeyValuePair
Although collections only store individual objects within them, the objects can be composed of multiple objects themselves.
One such class that describes this is the TScalarKeyValuePair, which contains a scalar identifier, or key, and a pointer to a value object. In FIG. 5, storage is allocated in this templatized class object 51 for the AScalarKey identifier 511, and the pointer to AValue, item 512. Elsewhere in memory is the actual object 52 which 512 refers to.
The C++ class interface for TScalarKeyValuePair is defined as:
______________________________________ template<class AScalarKey, class AValue> class TScalarKeyValuePair : public MDebuggerCollectible { public: TScalarKeyValuePair( ); TScalarKeyValuePair(AScalarKey, AValue*); TScalarKeyValuePair(const TScalarKeyValuePair< AScalarKey, AValue>&); virtual .about.TScalarKeyValuePair( ); TScalarKeyValuePair<AScalarKey, AValue>& operator=( const TScalarKeyValuePair< AScalarKey, AValue>&); virtual TDebuggerStream& operator>>=(TDebuggerStream&) const; virtual TDebuggerStream& operator<<=(TDebuggerStream&); virtual long Hash( ) const; virtual bool IsEqual(const MDebuggerCollectible*) const; virtual bool IsSame(const MDebuggerCollectible*) const; AScalarKey GetKey( ) const; AValue* GetValue( ) const; protected: AScalarKey fKey; AValue* fValue; }; ______________________________________
The constructor provides default construction, in which no key or value is specified. It also provides a constructor which takes both these values. And finally, it takes a copy constructor which deep-copies another TScalarKeyValuePair object.
The standard operator=, Hash, IsEqual functions are defined as specified by the base class protocol. The Hash function returns a value based on the fKey. The IsEqual function only tests the fKey for equality; the fValue portion is not tested as described above because only the identifier portion of the object is tested.
This class can be used when objects are not streamable. Because this class deviates from the base protocol, the operator>>= and operator<<= functions do not actually stream any objects.
Another function, IsSame, is called for testing identity, meaning that if the object specified as the parameter is the same as the one whose IsSame function is being called, the result will be true. Otherwise, the result is false.
To access the data within the object, the GetKey function returns the scalar value. To return a pointer to the value, the GetValue function is returned.
TStreamableScalarKeyvaluePair
The TStreamableScalarKeyValuePair class is identical to the TScalarKeyValuePair class except that the objects stored within are streamable.
______________________________________ template <class AScalarKey, class AValue> class TStreamableScalarKeyValuePair { public TScalarKeyValuePair<AScalarKey, AValue> { public: TStreamableScalarKeyValuePair( ); TStreamableScalarKeyValuePair( AScalarKey, AValue*); TStreamableScalarKeyValuePair(const TStreamableScalarKeyValuePair< AScalarKey, AValue>&); virtual .about.TStreamableScalarKeyValuePair( ); TDebuggerStream& operator>>=(TDebuggerStream&) const; TDebuggerStream& operator<<=(TDebuggerStream&); }; ______________________________________
TPDIDCollection
The server ensures uniqueness of the identifiers of objects stored in some of its data structures. To guarantee this uniqueness, the TPDIDCollection class specifies storage for an identifier and a function for generating new, unique identifiers.
______________________________________ template <class AObject, class AScalar> class TPDIDCollection : public TPDSet<AObject> { public: TPDIDCollection( ); TPDIDCollection(const TPDIDCollection< AObject, AScalar>&); virtual .about.TPDIDCollection( ); TPDIDCollection<AObject, AScalar>& operator=(const TPDIDCollection< AObject, AScalar>&); virtual AScalar MintID( ); private: AScalar fNextID; }; ______________________________________
In all respects, the TPDIDCollection is identical to the TPDSet class. An additional data member, fNextID, of templatized type AScalar, contains a scalar value which is initialized in the collections construction.
When the MintID function is called, an new identifier is generated and returned to the caller.
TPDArray
A simple collection for storing objects is the TPDArray class. It is used to easily access individual objects in the array. The array can grow as necessary, as more objects are added to the collection.
An initial size is specified by default:
______________________________________ const PrimitiveDbgCollCount kInitialPrimitiveArraySize = 20; ______________________________________
The C++ class interface for the TPDArray class is defined as follows:
______________________________________ template<class AObject> class TPDArray : public TPDCollection<AObject> { public: friend class TPDArrayIterator<AObject>; TPDArray (PrimitiveDbgCollCount initialSize = kInitialPrimitiveArraySize); TPDArray(const TPDArray<AObject>&); virtual .about.TPDArray( ); TPDArray<Object>& operator=(const TPDArray<AObject>&); virtual TDebuggerStream& operator>>=(TDebuggerStream&) const; virtual TDebuggerStream& operator<<=(TDebuggerStream&); PrimitiveDbgCollCount GetCount( ) const; PrimitiveDbgCollCount GetSize( ) const; virtual AObject* Get(const AObject&) const; virtual AObject* Remove(const AObject&); virtual void DeleteAll( ); virtual AObject* At(PrimitiveDbgCollCount index) const; virtual void AtPut(PrimitiveDbgCollCount index, AObject&); virtual void AtAdopt(PrimitiveDbgCollCount index, AObject*); virtual TPDArrayIterator<AObject>* CreateIterator( ); protected: PrimitiveDbgCollCount FindObject(const AObject&) const; AObject** Reallocate( PrimitiveDbgCollCount newSize, AObject* currentList[ ], PrimitiveDbgCollCount oldSize); private: PrimitiveDbgCollCount fSize; AObject** fObjectList; }; ______________________________________
All functions are implemented according to the base class protocol. Access to the objects within the TPDArray class can be achieved using the Get function, or the At function which specifies an index into the array.
To store an object of type AObject, the pointer to the object is passed into the AtAdopt function, storing the pointer at the index specified. Similarly, to copy an object first before storing, the AtPut function is called with an index and a pointer to the object.
Protected member function FindObject simply searches through the array of pointers to objects.
The Reallocate function will reallocate a pointer to the memory block containing the pointers to objects that are stored in fObjectList.
TPDArrayIterator
The TPDArrayIterator class derives from the TPDCollectionIterator class and uses an index into a hash table or array, stored in the fIndex data member.
The pointer to the TPDArray over which iteration is to occur is specified when the iterator is constructed.
After construction, the owner of the iterator may call the First function, which returns a pointer to the first object in the list. It is required that the iterator return objects in order of the array. Whenever it is required that the iteration is to start over again, the First function is called again.
After receiving a pointer to the first iterated object, the client may call the Next function which will return the next object after the last one returned. Subsequent calls will return objects to subsequent objects in the collection.
The Reset function is incidental to this particular implementation of the TPDArrayIterator class and is called to reset the internal index before the Firs t member function is called.
______________________________________ template<class AObject> class TPDArrayIterator : public TPDCollectionIterator<AObject> { public: TPDArrayIterator(TPDArray<AObject>*); virtual .about.TPDArrayIterator( ); virtual AObject* Next( ); virtual AObject* First( ); private: TPDArrayIterator( ); void Reset( ); private: PrimitiveDbgCollCount fIndex; }; ______________________________________
Linked Lists
A primitive linked list class is used by the debugger framework. The MDebuggerLinkable base class provides functionality which is similar to the MLinkable class found in the CommonPoint class frameworks. Objects that are required to be stored in linked list data structures derive from the MDebuggerLinkable class. A linked list class, TPDLinkedList contains the head of the linked list object that derives from MDebuggerLinkable. To iterate over all of the objects in a linked list, the TPDLinkedListIterator is used.
MDebuggerLinkable
Any object whose use requires storage within a linked list must derive from MDebuggerLinkable. This class provides, as data members fNext and fPrevious, pointers to the next and previous elements in the linked list.
The C++ class interface for the MDebuggerLinkable class follows:
______________________________________ class MDebuggerLinkable : public MDebuggerCollectible { public: MDebuggerLinkable( ); MDebuggerLinkable* Next( ) const; void SetNext(MDebuggerLinkable* next); MDebuggerLinkable* Previous( ) const; void SetPrevious(MDebuggerLinkable* previous); private: MDebuggerLinkable* fNext; MDebuggerLinkable* fPrevious; }; ______________________________________
In FIG. 6 the data structures for an example linked list are shown. Linkable objects 611, 62, 63, and 64 contain fPrevious data members 612, 621, 631, and 641, respectively. They also contain fNext data members 613, 622, 632, and 642, respectively. Each pointer for fPrevious points to the previous linked object in the list, and vice versa for fNext.
To traverse from one MDebuggerLinkable object to the next, the Next function is called, returning a pointer to the next object. Conversely, to get to the previous object, the Previous function is called.
When an MDebuggerLinkable object is being added within a linked list, the SetNext function is called to add a new object after the object whose function is being called. And of course, in reverse, the SetPrevious function is called when an object is inserted in the list before the current object whose function is being called.
TPDLinkedList
The templatized TPDLinkedList class is itself implemented as a single MDebuggerLinkable data member as well as a comparison function from which to use for comparisons. Its C++ class interface is defined as follows:
______________________________________ template<class AItem> class TPDLinkedList { public: TPDLinkedList( ); TPDLinkedList(const TPDLinkedList<AItem>&); TPDLinkedList( MDebuggerCollectibleCompareFn); virtual .about.TPDLinkedList( ); TPDLinkedList<AItem>& operator=(const TPDLinkedList<AItem>&); bool operator==(const TPDLinkedList<AItem>&); virtual void DeleteAll( ); virtual void RemoveAll( ); virtual void AddLast(AItem*); AItem* First( ) const; AItem* Last( ) const; virtual AItem* Find(const AItem&) const; virtual AItem* Remove(const AItem& key); virtual AItem* Remove(AItem*); TPDLinkedListIterator<class AItem>* CreateIterator( ); private: MDebuggerCollectibleCompareFn fTestFn; MDebuggerLinkable fSentinel; friend class TPDLinkedListIterator<AItem>; }; ______________________________________
The linked list provides functions for removing all elements out of the list, e.g. RemoveAll, as well as for deleting all elements out of the list, e.g. DeleteAll.
To add a new linkable item to the end of the list, the AddLast function is called with the object to be added.
The first item in the list is returned by the First function; the last item by the Last function.
To search for a specific object in the linked list, using an identifier object, the Find function is called. Items whose identifiers are specified by the key passed into the Remove function can be removed from the list; if the object is found in the list, it is returned otherwise a NIL pointer is returned.
The polymorphic CreateIterator creates an TPDLinkedListIterator<class AItem>object which can be used by the caller to iterate over all items in the linked list.
TPDLinkedListIterator
As with other iterator classes as defined above, the TPDLinkedListIterator templatized class also follows the same semantics of the TPDCollectionIterator class.
______________________________________ template<class AItem> class TPDLinkedListIterator { public: TPDLinkedListIterator( TPDLinkedList<AItem>*); virtual .about.TPDLinkedListIterator( ); AItem* First( ); AItem* Next( ); private: TPDLinkedListIterator( ); TPDLinkedList<AItem>* fLinkedList; AItem* fCurrent; MDebuggerLinkable* fSentinel; }; ______________________________________
A pointer to a linked list is used to construct an iterator object; it is stored in the fLinkedList data member.
When the First function is called, the first item in the linked list is returned. A pointer to the object being returned is stored in the data member fCurrent. To maintain currency checks, the fSentinel points to the same internal MDebuggerLinkable object stored in the TPDLinkedList object being iterated over. When the pointer to the fCurrent matches pointer in the fSentinel object, the Next function returns NIL.
Addressing
An addressing abstraction is presented which allows the use of target memory addresses in a portable fashion. The use of static compiler types such as void* in the C and C++ languages do not properly express this as a portable abstraction since they are limited to the static size as implemented by the compiler used to compile the debugger code.
Two major classes are used to describe the abstraction itself; other subclasses are created to describe processor-specific addressing data. The first class, a monomorphic class called TDebuggerAddress, represents a single target address. It does not restrict the size of addressing to an arbitrary size such as 32 bits, nor does it restrict the mode of addressing to a particular memory model such as a flat 32-bit address space. It is defined to be a statically fixed-sized object, encapsulating a polymorphic implementation whose size varies. This allows static allocation of the TDebuggerAddress object in fixed-size data structures or stack-based allocation.
Bits16 and Bits32
Two types are defined to provide an exact size of 16 and 32 bits. These data types depend on the compiler being used since languages such as C and C++ do not specify exact sizes of scalar types. By defining these types, the following address class are ensured of a guarantee that the data is ordered correctly.
TDebuggerAddress
Following is the C++ class interface to an embodiment of the TDebuggerAddress abstraction.
______________________________________ class TDebuggerAddress : public MDebuggerCollectible { public: TDebuggerAddress( ); TDebuggerAddress(const TDebuggerAddress&); TDebuggerAddress(const TUniversalAddress& address); // not adopted virtual .about.TDebuggerAddress( ); virtual TDebuggerStream& operator>>=(TDebuggerStream&) const; virtual TDebuggerStream& operator<<=(TDebuggerStream&); virtual long Hash( ) const; virtual bool IsEqual(const MDebuggerCollectible*) const; virtual int GetWidthInBytes( ) const; inline int GetWidthInBits( ) const; virtual int GetDataWidthInBytes( ) const; inline int GetDataWidthInBits( ) const; virtual ByteOrder GetByteOrder( ) const; virtual Segmentation GetSegmentation( ) const; virtual void* GetStorage( ) const; inline bool IsntNil( ) const; virtual bool IsNil( ) const; virtual operator Bits16( ) const; virtual operator Bits32( ) const; TDebuggerAddress& operator=(const TDebuggerAddress& source); virtual TDebuggerAddress& operator+=(const TDebuggerAddress& operand); virtual TDebuggerAddress& operator-=(const TDebuggerAddress& operand); virtual TDebuggerAddress operator+(const TDebuggerAddress& source); virtual TDebuggerAddress operator-(const TDebuggerAddress& source); TUniversalAddress* GetUniversalAddress( ) const; protected: virtual TUniversalAddress* InstantiateByType(long universalAddressType); public: static const TDebuggerAddress& fgInvalidAddress, protected: TUniversalAddress* fAddress; }; ______________________________________
FIG. 18 shows the object relationship between the TDebuggerAddress object and the TUniversalAddress it contains. The monomorphic TDebuggerAddress contains a pointer 1811 to a polymorphic TUniversalAddress 1802 which can define storage 1821 of any size. This affords unlimited flexibility in the ability to represent any address.
The object can be constructed with no parameters (default constructor), with a copy constructor, in which case it completely deep-copies the copy constructor parameter, or using a TUniversalAddress to create the object from a platform-specific debugger class.
The operator>>= streaming function allows the object to stream itself out into the provided stream parameter. Likewise, the operator<<= streaming function allows the object to stream itself in from the provided stream parameter. These operations provide polymorphic streaming such that in the operator>>= function the polymorphic TUniversalAddress object will write its own type out into the stream using, allowing the corresponding object read in during the operator<<= function to recreate the same type.
The mechanism to do this can be implemented in two ways. FIG. 7 describes the control flow for the two different cases of streaming. Streaming starts with a function call to the operator>>= 71. When using the Taligent CommonPoint system, the operator>>= function will call Flatten 73, which will write an encoded class name and library name into the stream. This case is shown in FIG. 7, number 72 "compiled/linked with CommonPoint is true." On the reading (operator<<=) side, the encoded class name and library name will be read out before the TUniversalAddress. An object is dynamically constructed using the library name and class name using the default constructor and the data is filled in.
The second method to achieve this in a manner that does not use the Taligent CommonPoint system is to encode a scalar value that corresponds to the type that is assigned to the particular TUniversalAddress class. This case is shown in FIG. 7, number 72 "compiled/linked with CommonPoint is false." Each TuniversalAddress subclass will have its GetType function which is called in 74 return a different scalar value based on its type. Each value is unique.
On the receiver side of the connection, the object is read out of the stream in FIG. 8. The two different cases, compiling with the CommonPoint system 83 which causes objects to be resurrected with their dynamic type; and without the CommonPoint system 84, 85, and 86 which show how to simulate this feature without using the Resurrect function.
The streaming class, TDebuggerStream, ensures scalar values are maintained correctly on different platforms. Even though a pointer class on one platform may exist in big-endian ordering, it will be correctly interpreted as a portable scalar value on all platforms.
The Hash member function allows the object to describe itself in terms of a scalar hash value. This is used when storing the addresses in collection classes which use the hashing function. The hash function uses a 32-bit scalar value which is returned by the fDebuggerAddress data member.
The IsEqual function allows comparison of objects. The test will return true if equal and false if not.
To determine the actual size of the underlying addressing width, the GetWidthInBytes returns a scalar value equivalent to the number of bytes (an 8-bit datum) used to form the address. Likewise, the number of bits are also returned by the GetwidthInBits function.
Addresses point to blocks of data. Each block of data, or the addressable unit, is determined by a processor architecture. The GetDataWidthInBytes function will return the total number bytes that are addressed by a single address. For instance, an address space that uses 32-bit addresses where each address can indicate a single byte in that address space will have a data width of one byte. Although a processor may be limited to reading and writing on boundaries on which the addressing may occur, e.g. "four-byte boundaries," the data width will still be one byte even though there is a processor limitation. However, if the individual size of a datum referred to by an address is a larger value, such as 32-bit words, the data width would be 32-bits; in this case pointers refer to 32-bits words, and not individual bytes.
The ordering of addresses vary across processors. Some processors place their addresses in "big endian" order, such as the Motorola M68000 family of microprocessors which specifies that a target address found in a target address space would be in the order most-significant bit through least-significant bit. In "little endian" order, as found in the Intel x86 family of microprocessors, the least significant byte is followed by the next-most significant byte, etc., followed by the most significant byte. Some processors support a mixed mode, such as the Motorola/IBM PowerPC MPC601 processor, which allows mixed addressing depending on a processor register. In this case, the debugger address class can support mixed modes of addressing dynamically, using a single debugger for both purposes. The ByteOrder enumeration defines two different ordering, kBigEndian, and kLittleEndian.
The GetSegmentation function returns the type of segmentation being used in the target address space; either the memory is kLinear (flat, uniform addressing), or kSegmented. Other enumerations can be added.
To test the pointer to determine it is NIL (address refers to a well-known NIL pointer), the IsNil and IsntNil functions can be called; boolean values of true or false are returned.
Conversion operators are provided for convenience. In many cases it is necessary to convert an abstract class into simple scalar values. For instance, the value for the length of functions may be defined by a compiler and runtime system to be limited to 32 bits. In this case, a subclass may use this additional information to turn the length of a function into a value that is a primitive scalar type. The conversion operators operator Bits16 and operator Bits32 can be called, and the result returned are values of the according 16- and 32-bit data types.
Assignment results in the return value when using the operator= function. Because the class is designed to be used as a monomorphic class, direct assignment from object to object affords a direct copy using a fixed object size. The underlying implementation is a polymorphic class, which will copied polymorphically.
Addition and subtraction of addresses are frequent operations in a debugger. The operator+= and operator-= functions allow lvalue addition and subtraction, respectively. The parameter is added to the object whose member function is called. Operations can only occur in a manner whereby the underlying TuniversalAddress object can take an addition to its value without changing the original semantics. By checking using types, the operand as specified by the parameters to operator+= and operator-= are tested so that they will not cause overflow. For instance, if a TDebuggerAddress containing a 64-bit address is passed as an argument to TDebuggerAddress:: operator+= on an object which contains a 32-bit address, the operation will be invalid because of the potential for overflow. An example of this is found in the implementation for T32BitAddress::operator+=:
______________________________________ TUniversalAddress& T32BitAddress::operator+=(const TUniversalAddress& operand) if (operand.IsA(this)) { const T32BitAddress& operand32 = (const T32BitAddress&) operand; fStorage += operand32.fStorage; } else { fail; } } ______________________________________
Creating expressions composed of TDebuggerAddress objects and addition or subtraction operations is possible by calling the operator+ and operator- functions. An 40 example of C++ code which might be used to determine the address of an instruction within which is calculated by adding an address and an offset is:
______________________________________ TDebuggerAddress functionStart = GetFunctionStart( ); TDebuggerAddress functionOffset = GetFunctionOffset( ); TDebuggerAddress instructionAddress = functionStart + functionOffset; ______________________________________
The code to perform the above addition of the address and offset is abstract, expressive, and portable. It is abstract in that the TDebuggerAddress objects express the individual data elements being used; they represent individual addresses and also define a protocol for addition; the syntax allows a programmer to form an expression in the language which is identical to the syntax used with expressions of primitive types. It is expressive because of ability to concisely combine expressions formed by the addition of two operands and a single assignment within a program statement. Finally, it is portable in that the single line of code is not only compilable on different target platforms to execute with different processor address classes, but it can be compiled on platforms other than the target host platform for cross-development. In addition, because of the polymorphic nature of the underlying TUniversalAddress object, the single line of code is used for not just a single target execution environment, but for all target execution environments. This allows a single instance of a debugger program, i.e. a single process with a single set of code, to target multiple environments simultaneously.
The last public function, GetUniversalAddress is provided to return a pointer to the underlying TUniversalAddress object for future extension.
When developing without the CommonPoint system, the InstantiateByType function allows a standalone debugger to dynamically instantiate objects based on a streamed scalar value.
TUniversalAddress
Following is the C++ class interface to an embodiment of the TUniversalAddress abstract base class.
The concept of a universal address is that it can represent any possible processor address. The address itself can be unbounded in size, and of any byte ordering, and can refer to data of arbitrary size.
Although there are no size restrictions, the compiler used on all platforms must at least specify a type that is capable of storing 32 bits in a scalar datum. If this is not the case, operations which require the Bits32 address type will be truncated to the largest type.
The TUniversalAddress class exhibits polymorphism. As one well-versed in the art understands, a function call is made and then bound at runtime such that the called member function is determined by rules of the language such that the most derived definition of the member function in a class hierarchy will be called. The dynamic nature of the call allows a base framework to utilize code that was created by a different source than the original framework designer. It is in this capacity that allows the debugger framework--in this case, the debugger address abstraction--to be extended to function on target processor architectures beyond what was originally defined when the framework was first created.
__________________________________________________________________________ class TUniversalAddress : public MDebuggerCollectible { public: virtual int GetWidthInBytes( ) const = 0; inline int GetWidthInBits( ) const; virtual int GetDataWidthInBytes( ) const = 0; inline int GetDataWidthInBits( ) const; virtual ByteOrder GetByteOrder( ) const = 0; virtual Segmentation GetSegmentation( ) const = 0; virtual void* GetStorage( ) const = 0; inline bool InstNil( ) const; virtual bool IsNil( ) const = 0; virtual long Hash( ) const = 0; virtual bool IsEqual(const MDebuggerCollectible*) const = 0; virtual TDebuggerStream& operator>>=(TDebuggerStream&) const = 0; virtual TDebuggerStream& operator<<=(TDebuggerStream&) = 0; virtual operator Bits16( ) const = 0; virtual operator Bits32( ) const = 0; virtual TUniversalAddress& operator+=(const TUniversalAddress& operand) = 0; virtual TUniversalAddress& operator-=(const TUniversalAddress& operand) = 0; virtual bool IsA(const TUniversalAddress*) const = 0; virtual int GetType( ) const = 0; }; __________________________________________________________________________
The abstract base class TUniversalAddress defines the abstraction of an address. It does not contain any data, nor does it define implementation for many of the function. However, the interface defines a protocol in which a framework or client code can use the functions with polymorphic behavior; when a polymorphic function call (as labeled with the C++ keyword, virtual, before the type and member function name in the class declaration) is made, the runtime system will correctly bind the caller with the called function in an implementation subclass.
The class hierarchy, FIG. 9 shows the inheritance graph for sample classes.
As defined earlier in TDebuggerAddress, the interfaces defines the member function protocol for determining the size of address pointers in bytes (GetWidthInBytes), the size of address pointers in bits (GetwidthInBits), the size of data addressed by the pointers in bytes (GetDataWidthInBytes), and the same in bits (GetDataWidthInBits). Further, the protocol is also identical for the hashing function (Hash), the equality testing function (IsEqual), streaming operators (operator>>= and operator<<=), lvalue addition and subtraction (operator+= and operator-=), and 16- and 32-bit conversion operators (operator Bits16 and operator Bits32).
The GetType function allows individual classes to return a value for its type, which is a scalar value. Each class is assigned a constant value for this. Two objects' types can be compared by comparing the results returned from the objects' GetType function. Although the Taligent CommonPoint type system provides this functionality, the explicit interface in this class allows the debugger to execute independent of the CommonPoint execution environment.
A function which provides a way of determining an objects' relative location in TUniversalAddress class hierarchy, is the IsA function. Each class defines its own IsA implementation which determines if the object is the same as another object, or a direct descendent of the object.
For example, the following lines of code show to determine whether one object is in a class that derives from another class, a T 32BitAddress 92 is a base class for 32-bit address values. A subclass of this, TM68000Address 95, implements the functions which describe addresses on the Motorola M68000 family of microprocessors. The following lines show this example in the implementation of the TM68000Address: IsA function.
______________________________________ bool TM68000Address::IsA(const TUniversalAddress* addr) const return (addr->GetType( ) == this->GetType( )) .parallel. T32BitAddress::IsA(addr); } ______________________________________
TInvalidUniversalAddress
While TUniversalAddress subclasses generally represent real addresses on real microprocessors, they can represent values which don't exist at all. The TInvalidUniversalAddress 93 is one such class. This class allows a TDebuggerAddress to be defined as "invalid" meaning that it doesn't point to any memory. This is different from pointers using primitive types such as the C++ type, void*, which can only represent the address space as defined by the type; therefore it is not possible to represent all values in the data type and an invalid value as well. With TInvaliduniversalAddress, this class will not compare to any other address types.
T32BitAddress
A base class for 32-bit addresses 92 is provided. It contains the base functionality defining 32-bit address types. It defines a 32-bit address range with one-byte data sizes. By default, it defines a big-endian addressing mode (which is overridden in various subclasses).
______________________________________ class T32BitAddress : public TUniversalAddress { public: T32BitAddress( ); T32BitAddress(const T32BitAddress& src); T32BitAddress(Bits32 addr); const T32BitAddress& operator=(Bits32); TUniversalAddress& operator=(const TUniversalAddress& source); virtual TUniversalAddress& operator+=(const TUniversalAddress& operand); virtual TUniversalAddress& operator-=(const TUniversalAddress& operand); virtual TDebuggerStream& operator>>=(TDebuggerStream&) const; virtual TDebuggerStream& operator<<=(TDebuggerStream&); virtual long Hash( ) const; virtual bool IsEqual(const MDebuggerCollectible*) const; virtual bool IsA(const TUniversalAddress*) const; virtual int GetType( ) const; virtual int GetWidthInBytes( ) const; virtual int GetDataWidthInBytes( ) const; virtual ByteOrder GetByteOrder( ) const = 0; virtual Segmentation GetSegmentation( ) const = 0; virtual void* GetStorage( ) const; virtual bool IsNil( ) const; virtual operator Bits16( ) const; virtual operator Bits32( ) const; public: static const int& kType; private: static const int gType; private: Bits32 fStorage; }; ______________________________________
Because this class requires actual storage for a 32-bit value, it contains the fStorage data member which is defined as a 32-bit data type (Bits32).
TM68000Address
______________________________________ class TM68000Address : public T32BitAddress { public: TM68000Address( ); TM68000Address(const T32BitAddress& src); TM68000Address(Bits32 addr); virtual ByteOrder GetByteOrder( ) const; virtual Segmentation GetSegmentation( ) const; virtual bool IsA(const TUniversalAddress*) const; virtual int GetType( ) const; public: static const int& kType; private: static const int gType; }; ______________________________________
The TM68000Address 95 defines a subclass of T32BitAddress. It overrides the GetType function to return a unique scalar type, the GetByteOrder function to ensure that the kBigEndian ordering is returned, the GetSegmentation function to return kLinearSegmentation, and the IsA function to test the parameter's type for inheritance.
The static data members, kType, is used to define a reference to the constant value. An additional static data member contains the actual value for the datum. This indirect mechanism allows compilation of the class into a shared library and is implementation-specific.
TiAPX86Address
Another example of a 32-bit address is the TiAPX86Address 96 which is used to represent 32-bit addresses on the Intel 80.times.86 family of microprocessors.
TPowerPCAddress
Another example of a 32-bit address is the TPowerPCAddress 97 which is used to represent 32-bit addresses on the PowerPC family of microprocessors.
T64BitAddress
The address classes are not limited to 32-bit address sizes. The following class interfaces define a 64-bit class which can be used to represent addresses on 64-bit processors.
The first class, T64Bits, is just a storage type. Because some compilers may not be able to properly express a 32-bit scalar as a primitive type, this class defines storage and protocol for handling 64-bit values in a portable manner.
______________________________________ class T64Bits { public: T64Bits( ); T64Bits(Bits32 ms, Bits32 ls); const T64Bits& operator=(const T64Bits&); TDebuggerStream& operator>>=(TDebuggerStream&) const; TDebuggerStream& operator<<=(TDebuggerStream&); inline Bits32 GetMostSignificant( ) const; inline void SetMostSignificant(Bits32 msw); inline Bits32 GetLeastSignificant( ) const; inline void SetLeastSignificant(Bits32 lsw); public: Bits32 fStorageWords[2]; }; ______________________________________
The two constructors can initialize an instance of the T64Bits class either by default or with two 32-bit values.
The operator= function allows assignment from one T64Bits object into another.
FIG. 10 shows the structure for a 64-bit address object. To access the 64-bit object as 32-bit low- and high-order parts, (also called least significant 1011 and most significant 1012), the members functions GetMostSignificant and GetLeastSignificant will return the respective portions of the 64-bit scalar. Likewise, the 64-bit value can be stored as two 32-bit halves using the SetMostsignificant and SetLeastSignificant member functions.
The T64BitAddress 94 subclass of TUniversalAddress provides an abstract base class for 64-bit addresses. Its C++ class declaration follows:
______________________________________ class T64BitAddress : public TUniversalAddress { public: T64BitAddress( ); T64BitAddress(const T64BitAddress& src); T64BitAddress(const T64Bits& addr); TUniversalAddress& operator=(const TUniversalAddress& source); virtual TUniversalAddress& operator+=(const TUniversalAddress& operand); virtual TUniversalAddress& operator-=(const TUniversalAddress& operand); virtual TDebuggerStream& operator>>=(TDebuggerStream&) const; virtual TDebuggerStream& operator<<=(TDebuggerStream&); virtual long Hash( ) const; virtual bool IsEqual(const MDebuggerCollectible*) const; virtual bool IsA(const TUniversalAddress*) const; virtual int GetType( ) const; virtual int GetWidthInBytes( ) const; virtual int GetDataWidthInBytes( ) const; virtual ByteOrder GetByteOrder( ) const = 0; virtual Segmentation GetSegmentation( ) const = 0; virtual void* GetStorage( ) const; virtual bool IsNil( ) const; virtual operator Bits16( ) const; virtual operator Bits32( ) const; public: static const int& kType; private: static const int gType; private: T64Bits fStorage; }; ______________________________________
Note that the storage data member, fStorage, contains the datum representing the address bits.
TMIX64Address
The class TMIX64Address 98 shows an example implementation using the T64BitAddress class.
Lengths and Pointer Differences
Sizes of objects and differences between pointers are represented as TDebuggerAddress objects as well. Since the address object represents a scalar value, it can be used for these two purposes as well. To explicitly declare that the scalar is used for length, the following type is defined:
______________________________________ typedef TDebuggerAddress TDebuggerLength; ______________________________________
Names and Strings
Text strings are used in PDS to represent names of functions, libraries, processes, threads, and exceptions. The TPrimitiveString class provides functionality independent of other classes. It is able to encode a 16-bit Unicode strings of arbitrary length. The string can be converted to and from primitive 8-bit character strings.
The type called PSLength defines a length type:
______________________________________ typedef unsigned long PSLength; ______________________________________
and a 16-bit Unicode character type:
______________________________________ typedef unsigned short UniChar; ______________________________________
TPrimitiveString
The C++ class declaration follows:
______________________________________ class TPrimitiveString : MDebuggerCollectible { public: TPrimitiveString( ); TPrimitiveString(const char* text); TPrimitiveString(const UniChar* text, PSLength length); TPrimitiveString(const TPrimitiveString&); virtual .about.TPrimitiveString( ); virtual TPrimitiveString& operator=(const TPrimitiveString&); virtual TDebuggerStream& operator>>=(TDebuggerStream& dstStream) const; virtual TDebuggerStream& operator<<=(TDebuggerStream& srcStream); PSLength Length( ) const; virtual PSLength Extract(PSLength start, PSLength length, char result[ ]) const; virtual PSLength Extract(char result[ ]) const; protected: virtual UniChar* CreateUniCharBuffer(PSLength resultLength) const; virtual UniChar* CreateUniCharString(const char* text, PSLength& resultLength) const; virtual UniChar* CreateUniCharString(const UniChar* text, const PSLength length, PSLength& resultLength) const; virtual void DeleteUniCharString(UniChar*) const; private: UniChar* fString; PSLength fLength; }; ______________________________________
The TPrimitiveString class derives from MDebuggerCollectible. The default constructor creates an empty object. The constructor taking a const char* parameter will convert the argument, a zero-terminated C string, into the 16-bit storage. Each individual 8-bit character is extended into a 16-bit character. The third constructor will take a pointer to a UniChar* string array and a length; these will copied into the object's own storage. Finally, the fourth/last constructor will copy the contents of the argument, which is another TPrimitiveString.
The operator= function follows the convention of the C++ language. It will deep-copy the contents of the data stored within the object argument.
To provide monomorphic streaming, the length and 16-bit UniChar data are streamed out in the operator>>= function. The stream data are written into the TDebuggerStream argument provided. An example of this code is shown below.
______________________________________ TDebuggerStream& TPrimitiveString::operator>>=(TDebuggerStream& dstStream) const fLength >>= dstStream; dstStream.Write((void*) fString, (size.sub.-- t) fLength * 2); return dstStream; } ______________________________________
In the operator<<= function, the length is read, and the rest of the data is streamed into a buffer pointed to by the data member fString. An example of this is shown below.
______________________________________ TDebuggerStream& TPrimitiveString::operator<<=(TDebuggerStream& srcStream) fLength <<= srcStream; DeletUniCharString(fString); fString = CreateUniCharBuffer(fLength); srcStream.Read((void*) fString, (size.sub.-- t) fLength * 2); return srcStream; } ______________________________________
The TPrimitiveString class provides functions to determine length (Length), and extract the contents of the string as primitive 8-bit char* types, or as 16-bit Unichar* types.
Internal allocation of data occurs using the CreateUniCharBuffer functions. Internal deallocation of data is provided via the DeleteunicharString function.
These two functions encapsulate the class' data storage needs.
Type definitions are provided for library names, function names, process names, and thread names. These are named TLibraryName, TFunctionName, TProcessName, and TThreadName, respectively:
______________________________________ typedef TPrimitiveString TLibraryName; typedef TPrimitiveString TFunctionName; typedef TPrimitiveString TProcessName; typedef TPrimitiveString TThreadName; ______________________________________
Process identification varies on different operating systems. Generally they are identified as scalar types such as 16-bit values, but they may also be represented by memory locations or by objects. The token of communication for representing processes on all operating systems is the TargetProcess identifier type:
______________________________________ typedef unsigned long TargetProcess; // ID type ______________________________________
This type will encode a process identifier that is unique to the debugger server that a client is registered with. The identifier is guaranteed uniqueness such that two processes on the same server will not use the same identifier. Use of a single TargetProcess type throughout the client-side debugger ensures portability and reuse of the PDS software. The defining type must meet the requirements of having sufficient T-stability for the defined needs of the debugger application. A compiler that defines a 32-bit value will ensure an extremely high T-stability if the C and C++ long type is used.
Uniqueness is not guaranteed across multiple servers. For instance, a debugger server may start allocating TargetProcess values starting at zero, incrementing them as programs are attached or started. Another debugger server on a different host may use the same allocation strategy. The TargetProcess values cannot be compared directly, therefore, to detect equality of target processes. Instead, an additional name qualification of the TargetHost would need to be compared first. An example of the TargetHost class is:
______________________________________ typedef unsigned long TargetHost; ______________________________________
TTargetProcess
The TargetProcess type only defines an identifier for processes. The closely related TTargetProcess class defines a protocol which can be used by both debugger client and debugger server to further describe additional information about a target process.
The C++ class interface for TTargetProcess follows:
______________________________________ class TTargetProcess : public MDebuggerCollectible { public: TTargetProcess( ); TTargetProcess(TargetProcess processID); TTargetProcess(const TTargetProcess& targetProcess); virtual .about.TTargetProcess( ); virtual TDebuggerStream& operator>>=(TDebuggerStream& dstStream)const; virtual TDebuggerStream& operator<<=(TDebuggerStream& srcStream); virtual long Hash( ) const; virtual bool IsEqual(const MDebuggerCollectible*)const; // // Process information // TargetProcess GetProcessID( ) const; void SetProcessID(TargetProcess); virtual void GetName(TProcessName&) const; virtual void SetName(const TProcessName&); virtual void GetThreadList(TTargetThreadList& resultList); virtual void GetAddressSpace( TTargetAddressSpace&); private: TargetProcess fProcessID; TProcessName fName; }; ______________________________________
The TTargetProcess class can be constructed using the default constructed, or from a TargetProcess. It can be copied using the copy constructor.
The operator>>= streaming function allows the object to stream itself out into the provided stream parameter. Likewise, the operator<<= streaming function allows the object to stream itself in from the provided stream parameter.
The Hash member function allows the object to describe itself in terms of a scalar hash value. This is used when storing the addresses in collection classes which use the hashing function. The hash function returns the TargetProcess scalar value.
The IsEqual function allows comparison of objects. The test will return true if equal and false if not.
To get or set the process identifier, the GetProcessID and SetProcessID functions are provided.
The name of the process being executed may be stored and retrieved from within the TTargetProcess. This storage provides a simple mechanism by which to transfer the name between the server and client. Its use is explained in detail in the methods for accessing program name.
The base class only provides a simple identification protocol, but it can be further extended within the debugger server through subclassing by using inheritance.
TTargetProcessList and TTargetProcessListIterator
Collections of process objects may be stored within a list. These lists are used in enumerating a list of processes. The primitive TPDSet class is parameterized with the TTargetProcess class; likewise, the TPDSetIterator class is-parameterized to return TTargetProcess objects when iterating through the list. Type definitions are declared for use of these classes:
______________________________________ typedef TPDSet<TTargetProcess> TTargetProcessList; typedef TPDSetIterator<TTargetProcess> TTargetProcessListIterator; ______________________________________
TargetThread
______________________________________ typedef unsigned long TargetThread; // ID type ______________________________________
This type will encode a thread identifier that is unique to the debugger server that a client is registered with. The identifier is guaranteed uniqueness such that two threads on the same server will not use the same identifier. Use of a single TargetThread type throughout the client-side debugger ensures portability and reuse of the PDS software. The defining type must meet the requirements of having sufficient T-stability for the defined needs of the debugger application. A compiler that defines a 32-bit value will ensure an extremely high T-stability if the C and C++ long type is used.
Uniqueness is not guaranteed across multiple servers. For instance, a debugger server may start allocating TargetThread values starting at zero, incrementing them as threads are attached, started, or discovered. Another debugger server on a different host may use the same allocation strategy. The TargetThread values cannot be compared directly, therefore, to detect equality of target threads. Instead, an additional qualification of the TargetHost and TargetProcess would need to be compared first.
Two constants are defined to describe undefined threads and to name all threads within a process:
______________________________________ const TargetThread kUndefinedThread = 0; const TargetThread kAllThreads = -2; ______________________________________
TTargetThread
The TargetThread type only defines an identifier for thread. The closely related TTargetThread class defines a protocol which can be used by both debugger client and debugger server to further describe additional information about a target thread.
The C++ class interface for TTargetThread follows:
______________________________________ class TTargetThread : public MDebuggerCollectible { public: TTargetThread( ); TTargetThread(const TTargetProcess&, const TargetThread& threadID); TTargetThread(const TTargetProcess&, const TargetThread& threadID, const TThreadName& threadName); TTargetThread(const TTargetThread&); virtual .about.TTargetThread( ); virtual void PrintDebugInfo(bool verhose = false) const; virtual TDebuggerStream& operator>>=(TDebuggerStream& dstStream) const; virtual TDebuggerStream& operator<<=(TDebuggerStream& stream); virtual long Hash( ) const; virtual bool IsEqual(const MDebuggerCollectible*) const; // // Thread information // TargetProcess GetProcessID( ) const; void SetProcessID(TargetProcess); TargetThread GetThreadID( ) const; void SetThreadID(TargetThread); virtual void GetName(TThreadName& threadName); private: TargetProcess fProcessID; TargetThread fThreadID; TThreadName fName; }; ______________________________________
The TTargetThread class can be constructed using the default constructed; from a TargetProcess process identifier and TargetThread thread identifier; and also from a TargetProcess process identifier, TargetThread thread identifier and a name. It can be copied using the copy constructor.
The operator>>= streaming function allows the object to stream itself out into the provided stream parameter. Likewise, the operator<<= streaming function allows the object to stream itself in from the provided stream parameter.
The Hash member function allows the object to describe itself in terms of a scalar hash value. This is used when storing the addresses in collection classes which use the hashing function. The hash function returns the TargetThread scalar value.
The IsEqual function allows comparison of objects. The test will return true if equal and false if not.
To get or set the process identifier, the GetProcessID and SetProcessID functions are