Author: Arnaud PICHERY (aranud@mail.dotcom.fr)
Date: November 3, 1999
The
aim was to be able to debug frozen features as well as melted features. Before,
only “supermelted” features could be debugged which means that every feature
that contained a breakpoint was supermelted and the resulting byte code sent to
the debugged application. Even melted feature had to be supermelted. A supermelted
feature is a feature that is melted on-the-fly at the beginning of an
application.
The
following table summarizes the differences between
|
Melted |
Supermelted |
Byte
code to start the feature |
BC_START |
BC_DEBUGGABLE |
Generation
of debugger hooks (BC_NEXT) |
No |
Yes |
BYTE_CONTEXT .
debug_mode |
False |
True |
The
way breakpoints were implemented was the following: by default, a debugger hook
was placed at each breakable line in the byte code (code: BC_NEXT). If there was a breakpoint, a break was placed instead of the hook (BC_BREAK instead of BC_NEXT). At the start of the application and at each time
the application was resumed, EiffelBench was sending the byte code of
supermelted feature to the application. As a result of this mechanism it was
impossible to put a new breakpoint in the current routine if you were already
stopped in it because EiffelBench couldn’t change the byte code you were
executing.
To
improve the new debugger, I first have added the debugger hook in frozen
features. The corresponding macro is RT_HOOK(current line number). Then I have added in the run-time a mechanism to set and remove a
breakpoint during the execution of the application. Finally I have removed the
“supermelt” notion. Now every melted feature contains debugger hook and start
with a BC_START byte code.
Another
main improvement in the debugger is the possibility to step-into a routine, and
to step-out of the current routine. It is now possible because frozen feature
now contain debugger hooks. Debugger hooks allow testing after each line if a
breakpoint is set, or if the stack depth is too swallow, or…
Another improvement is the possibility to disable breakpoints separately. A new button in the toolbar has been added. By clicking on it, the user can disable all breakpoints, and he can disable the breakpoints he desires by clicking on a breakable point, a feature or a class and dropping it onto this button.
The next improvement planned was to be able to interrupt the application and start debugging it. This functionality was already implemented but with serious limits. The application could only be interrupted inside melted code, which means that it was impossible to interrupt a frozen application. Moreover the mechanism used to interrupt the application consumed a lot of CPU. After each Eiffel melted instruction, the application asked the daemon if it had to stop or not. It means there were 2 socket connections (one for the question, one for the answer) for each line of Eiffel code. It was quite fast because there are generally not many melted feature, but using this mechanism for very each line of Eiffel (frozen or melted) was out of order. Tests have shown that an application normally that needs 2s to execute can take more than 3 minutes to ends.
At each line, the application wonders if it has to stop. The idea was to increase the speed of the wondering. There was two ways to do so under Windows. The first idea was to use Maillots. They are very convenient and very easy to use but they are slower than the second approach. The second approach consists in having a simple flag inside the application. The application tests the flag each time it think it should stop. It the flag is set the application stops. If the flag is not set the application simply goes on. The tricky part was to be able to change the value of the flag from the daemon. It was possible through C function WriteProcessMemory.
An extention of the ability to interrupt an application is to add/remove a breakpoint “on-the-fly”. Assume that your application is executing a big loop, with the new mechanism you are able to put a breakpoint inside the loop while the application is executing. Of course, once the breakpoint is set, the application will stop its execution the first time it encounters the new breakpoint. To do so, I have used the same mechanism that the one used to interrupt an application: When the user sets a breakpoint while the application is running, EiffelBench modify the interruption flag of the application. The application then stops itself and listens to EiffelBench. Then EiffelBench sends the modified breakpoints to the application and resume it.
The last important improvement added to the debugger was the possibility to retrieve the context of a frozen feature. Before, when the application was stopped in a frozen feature, it was impossible to see the local variables, the Result or the arguments. It was already implemented, but only for melted feature. Starting from now, when the application is stopped in a melted or frozen feature, the user can see the local variables, the arguments of the feature and its Result if the feature is a query. To do so, each feature push on the “frozen operational stack” the address of all local variables, all arguments, the current object and the Result before starting. When the application is stopped, the application send to EiffelBench the content of all address found on the “frozen operational stack”.
Another new feature I have implemented is the automatic loading and saving of all breakpoints when the user open or close a project. Currently, the breakpoints are saved in a file called “options.edb” situated in the EIFGEN directory of the project.
The
debugger mainly relies on two data structures, which are the execution stack
and the list of breakpoints. The execution stack is defined in the file “eif_types.h”
as below:
/* Structure used as the execution vector. This is for both the execution
* stack
(eif_stack) and the exception trace stack (eif_trace).
*/
struct ex_vect {
unsigned
char ex_type; /* Function call, pre-condition, etc... */
unsigned
char ex_retry; /* True if function has been retried */
unsigned
char ex_rescue; /* True if function entered its rescue clause */
#ifdef WORKBENCH
int ex_linenum; /*
current line number (line number <=> breakpoint slot) */
int ex_bodyid; /* body id of the feature */
unsigned char ex_locnum; /*
number of local variables in the function */
unsigned
char ex_argnum; /* number of arguments of the function */
#endif
union {
unsigned
int exu_lvl; /* Level for multi-branch
backtracking */
int
exu_sig; /* Signal number */
int
exu_errno; /* Error number reported
by kernel */
struct {
char
*exua_name; /* The assertion tag */
char
*exua_where; /* The routine name where assertion
was found */
int
exua_from; /* And its origin (where it
was written) */
char
*exua_oid; /* Object ID (value of Current)
*/
} exua; /* Used by assertions */
struct {
char
*exur_jbuf; /* Execution buffer address,
null if none */
char
*exur_id; /* Object ID (value of
Current) */
char
*exur_rout; /* The routine name */
int
exur_orig; /* Origin of the routine */
} exur; /* Used by routines */
} exu;
};
During the execution of both melted and frozen features, ex_linenum and ex_body_id are updated. ex_linenum contains the current breakable line number, which means the current line number in the stop points view under EiffelBench. ex_body_id contains the real_body_id of the current feature (in fact, ex_body_id contains EiffelBench’s real_body_id - 1).
NB:
A similar structure called debug_ex_vect exists. The only difference between ex_vect and debug_ex_vect is that debug_ex_vect contains ex_linenum, ex_body_id,… even in finalized mode. I
was obliged to add this structure to be able to compile EiffelBench in
finalized mode. In finalized mode, the call stack of EiffelBench is of type ex_vect (no line number information)
but the run-time must be able to handle the ex_vect of the debugged application
which contains line number information. So, dumped call stack are now of type debug_ex_vect.
The
ex_body_id is set up at the creation of a new execution vector during the
execution of the C-routine new_exvect. Actually new_exvect is called
through the RTEAA C-macro for frozen feature and through the C-routine
interp (located in file “interp.c”) for melted feature. For melted
feature, the real_body_id of the feature can be found after the BC_START byte code.
In
order to add and remove a breakpoint, the application needed to store somewhere
the location of the breakpoints. The best place to store all the data the
application needed was in the d_data variable which type is struct dbinfo. d_data is a global variable defined in the file “debug.c”.
The new variables I have added are the following;
struct offset_list {
uint32
offset;
struct
offset_list *next;
};
/* breakpoint table. there is one entry per feature
where
* you can
find a breakpoint
*/
struct db_bpinfo {
int body_id; /* body_id of the feature where
breakpoints are located */
struct
offset_list *first_offset; /* list of all
offset where a breakpoint is */
struct
db_bpinfo *next; /* next feature that
contains breakpoint(s) */
};
struct dbinfo {
char
*db_start; /* Start of current byte
code (dcall) */
int
db_status; /* Execution status
(dcall) */
uint32
db_callstack_depth; /* number of
routines on the eiffel stack */
uint32
db_callstack_depth_stop; /* depth from
which we must stop (step-by-step, stepinto..) */
char
db_stepinto_mode; /* is stepinto
activated ? */
char
db_discard_breakpoints; /* when set,
discard all breakpoints. used for run-no-stop, */
/*
and after the end of the root creation. it avoids the */
/*
application to stop after its end when garbage collector */
/*
destroys objects */
struct
db_bpinfo *db_bpinfo; /* breakpoints
table */
};
In
order to stop the application when we want, I have added a debugger hook before
each instruction (and before the final end of the feature). The debugger hook
contains the current line number. For example a frozen feature with 3 lines
will produce the following C code when frozen:
RT_HOOK(1); INSTRUCTION 1; RT_HOOK(2); INSTRUCTION 2; RT_HOOK(3); INSTRUCTION 3; RT_HOOK(4) |
do Instruction 1 Instruction 2 Instruction 3 end |
For
frozen feature, the debugger hook is the C macro RT_HOOK that calls the
C-function dnext_frozen. For melted feature, the debugger hook is the BC_NEXT byte code that calls when interpreted the C-function dnext_melted. Starting from now, BC_NEXT has an argument
which is the current line number (an Eiffel INTEGER, a C long). dnext_frozen and dnext_melted do the same job: they check whether a breakpoint,
the stack depth stop or the stepinto flag are set or not. In the positive case,
the application stops.
Setting
a breakpoint is very simple on the run-time side. When the application is
listening to EiffelBench, EiffelBench send a message to the application with
the real_body_id of the
feature of the breakpoint, its line number within the feature and its mode (if
the application must set or remove the breakpoint). All we have to do is to
create a new cell for the feature in the bp_info structure if it does not exist, and to create a new
cell for the line number. That’s all. This is made by the dsetbreak C function.
To
implement the step-into mechanism I have used a simple flag. Actually stepping
into the next function means stopping to the next instruction executed: If we
can step into the function, the following instruction to be executed will be
the first instruction of the function where we want to step into. If we can’t
step into the function (because it’s just a basic instruction), we will stop to
the following instruction of the current routine, which is in fact the next
instruction we will execute.
So, to step into next function, we just set the step-into flag. When it is set, application will stop at the next debugger hook encountered.
To
implement the step-out mechanism I have used the notion of stack depth. Assume
that the application is stopped in a feature. The current call stack depth is
3. The idea is to stop the application at the first following debugger hook
with a current call stack depth of 2. So we set the callstack_depth_stop variable to 2. The application will stop as soon as current_callstack_depth <=
callstack_depth_stop.
The
way the step-by-step mechanism is implemented is the same as the step-out
mechanism is. The only difference is that we set the callstack depth stop to
the current callstack depth: Assume that the application is stopped in a
feature. The current call stack depth is 3. The idea is to stop the application
at the first following debugger hook with a current call stack depth of 3. So
we set the callstack_depth_stop variable to 3. The application will stop as soon as current_callstack_depth <=
callstack_depth_stop. If the application
start to step into a function, it will increase the current stack depth and the
application will not stop inside the function.
There
are two different operational stacks: one for melted features, and one for
frozen feature. The operational stack for melted feature contains not only the
local variables, arguments, … but also the whole bytecode for the feature. So,
it was impossible to reuse the melted operational stack to store the local
variables for frozen feature. A new operational stack (called c_opstack for
frozen features, opstack for melted one) was created. A new file ‘newdebug.c’
has been created where one can find all the classic functions dealing with
stacks: c_opush, c_opop, …
Now,
when a function starts, it pushes the Result (NULL is pushed if the feature is
a command), then the arguments (if any), then the Current object, and then the
local variables (if any). Before returning, the feature pop all pushed items.
When
the applications stops, EiffelBench asks it for each feature on the callstack
to returns the locals variables, the arguments, and the Result (if any). To avoid
destroying the operational stack, no pop are made. Instead, I used c_oitem(i)
which returns the i-th item starting from the top. Assume that we have the
following application:
feature2(arguments…): … is
local
…
do
feature1
end
feature1(arguments…): … is
local
…
do
io.putstring(“Hello World!”)
end
The
application is stopped in feature1. The operational stack will look like the
following drawing. The locals and the arguments are sorted in reverse order
(i.e. localn, localn-1, …, local2, local1,
Current, argp, argp-1, …, arg2, arg1,
Result). To get the i-th local variable or the i-th argument , the application
simply calls cloc(i) or carg(i). cloc and carg are two C macros defined as
below.
#define cresult c_oitem(start + clocnum + cargnum + 1)
#define cloc(x) c_oitem(start + clocnum - (x))
#define carg(x) c_oitem(start + clocnum + cargnum + 1 - (x))
#define ccurrent c_oitem(start + clocnum)
start is an usefull
variable that always points on the first item of the current feature in the operational
stack (see the arrows on the drawing). For feature1, start is equal to 0, for
feature 2, start is equal to “local variable number“ + “argument number” + 2
(Result + Current), etc…
Once the local variable/argument retrieved, we have to update the structure to send to EiffelBench the value of the variable instead of its address. An item is defined as below:
struct item {
uint32 type; /* Type of the item (SK_INT, SK_BOOL, SK_DOUBLE, ...) */
union {
/* values (melted feature) */
char itu_char; /* SK_CHAR, SK_BOOL - a character value */
long itu_long; /* SK_INT - an integer value */
float itu_float; /* SK_FLOAT - a real value */
double itu_double; /* SK_DOUBLE - a double value */
char *itu_ref; /* SK_REF - a reference value */
char *itu_bit; /* SK_BIT - a bit reference value */
char *itu_ptr; /* SK_PTR - a routine pointer */
} itu;
/* address (frozen feature) - should not be part of the union since we are filling */
/* the union from the address in the function 'ivalue' */
void *it_addr; /* SK_INT, SK_CHAR, ... - address where value can be found */
};
So, to update the item, we just fill in the itu field with the value found at the address it_addr.
The
Eiffel part of the new debugger is mainly located in the following four
classes: BREAKPOINT,
DEBUG_INFO, APPLICATION_EXECUTION and EWB_REQUEST.
The
BREAKPOINT class deals with the handling of one breakpoint. It has only a few
attributes:
breakable_line_number: INTEGER;
-- line number of the breakpoint in the stoppoint view under EiffelBench
routine: E_FEATURE;
-- feature where this breakpoint is situated
real_body_id: REAL_BODY_ID;
-- real_body_id of the feature where this breakpoint is situated
body_index: BODY_INDEX;
-- real_body_id of the feature where this breakpoint is situated
The
breakpoint has also two other attributes: the bench_status and the application_status. The bench_status reflects the current status of the breakpoint under
EiffelBench. It can have 3 different values: set, disabled, not_set. Set means
that the breakpoint is set and active, disabled means it is set but inactive
(application will not stop), and not_set means that the breakpoint has been
removed. The application_status reflects the current status of the breakpoint under
the application. It can have a different value than the bench status because we
only resynchronize the two states when we are resuming or starting the
application. The application_status can have 2 different values: set or not_set.
bench_status: INTEGER;
-- current status within EiffelBench
--
-- see the private constants at the end of the class to see the
-- different possible value taken
application_status: INTEGER;
-- last status sent to the application, this is the current
-- status of this breakpoint from the application point of view
--
-- see the private constants at the end of the class to see the
-- different possible value taken
A user can set a breakpoint, remove a breakpoint,
disable a breakpoint but also switch a breakpoint. The behavior of the switch
feature is the following:
·
if the breakpoint is
set, we remove it.
·
If the breakpoint is
not set, we set it
·
If the breakpoint is
disabled, we enable it
The
DEBUG_INFO class deals with the handling of the breakpoints of the application. It
contains an attribute called breakpoints which is an hash_table of breakpoints(HASH_TABLE[BREAKPOINT,BREAKPOINT]). This class allows to perform a wide range of
operation on breakpoints.
Remark:
to find a specified breakpoint in the hash table, we always use the same
method. We first create a ”fake” breakpoint with the same body_index and the
same line number than the breakpoint we want to get, and then we call the
hash_table feature get with the fake breakpoint as argument to retrieve the
good breakpoint. To do so, the BREAKPOINT
class redefines the feature is_equal. Two breakpoints are equal if they have the same body index and the
same breakable line number.
wipe_out is
-- Empty Current.
restore is
-- reset information about breakpoints set/removed during execution
update is
-- remove breakpoint that no more useful from the hash_table
-- see BREAKPOINT/is_usefull for further comments
has_breakpoints: BOOLEAN is
-- Does the program have a breakpoint (enabled or disabled) ?
has_enabled_breakpoints: BOOLEAN is
-- Does the program have a breakpoint set?
has_disabled_breakpoints: BOOLEAN is
-- Does the program have an enabled breakpoint?
resynchronize_breakpoints is
-- Resychronize the breakpoints after a compilation.
feature_bp_list (list: LINKED_LIST [E_FEATURE]): FIXED_LIST [CELL2 [E_FEATURE, LIST [INTEGER]]] is
-- Create a list with features and breakpoints
features_with_breakpoint_set: LIST [E_FEATURE] is
-- list of all feature with a breakpoint set (enabled or disabled)
Changing all breakpoints
remove_all_breakpoints is
-- Remove all breakpoints which are currently set (enabled/disabled)
enable_all_breakpoints is
-- disable all breakpoints which are currently set and enabled
disable_all_breakpoints is
-- disable all breakpoints which are currently set and enabled
Changing all breakpoints for a feature
add_breakpoints_for_feature (feat: E_FEATURE; a_list: LIST [INTEGER]) is
-- Add all the breakpoints a_list for feature feat.
remove_breakpoints_in_feature (f: E_FEATURE) is
-- remove all breakpoints set for feature 'f'
disable_breakpoints_in_feature (f: E_FEATURE) is
-- disable all breakpoints set for feature 'f'
enable_breakpoints_in_feature (f: E_FEATURE) is
-- enable all breakpoints set for feature 'f'
Getting breakpoints status for a feature
has_breakpoint_set (f: E_FEATURE): BOOLEAN is
-- Has f a breakpoint set to stop?
breakpoints_set_for (f: E_FEATURE): LIST [INTEGER] is
-- Breakpoints set for feature f
breakpoints_disabled_for (f: E_FEATURE): LIST [INTEGER] is
-- Breakpoints set for feature f and disabled
breakpoints_enabled_for (f: E_FEATURE): LIST [INTEGER] is
-- Breakpoints set for feature f and enabled
Changing breakpoints for a class
remove_breakpoints_in_class (c: CLASS_C) is
-- remove all breakpoints set for class 'c'
disable_breakpoints_in_class (c: CLASS_C) is
-- disable all breakpoints set for feature 'f'
enable_breakpoints_in_class (c: CLASS_C) is
-- enable all breakpoints set for feature 'f'
Changing a specified breakpoint
switch_breakpoint (f: E_FEATURE; i: INTEGER) is
-- Switch the i-th breakpoint of f
remove_breakpoint (f: E_FEATURE; i: INTEGER) is
-- Switch the i-th breakpoint of f
disable_breakpoint (f: E_FEATURE; i: INTEGER) is
-- disable the i-th breakpoint of f
-- if no breakpoint already exists for 'f' at 'i', a disabled breakpoint is created
enable_breakpoint (f: E_FEATURE; i: INTEGER) is
-- enable the i-th breakpoint of f
-- if no breakpoint already exists for 'f' at 'i', a breakpoint is created
Getting the status of a specified breakpoint
is_breakpoint_set (f: E_FEATURE; i: INTEGER): BOOLEAN is
-- Is the i-th breakpoint of f set? (enabled or disabled)
is_breakpoint_enabled (f: E_FEATURE; i: INTEGER): BOOLEAN is
-- Is the i-th breakpoint of f enabled
is_breakpoint_disabled (f: E_FEATURE; i: INTEGER): BOOLEAN is
-- Is the i-th breakpoint of f disabled
The APPLICATION_EXECUTION encapsulates most of the breakpoint features of the DEBUG_INFO class. There is nothing particular to explain here. It also allows loading and saving the breakpoint list. Theses feature are used in the class OPEN_PROJECT to load the breakpoints from a previous session and in QUIT_PROJECT to save the breakpoints for the following session.
This is the class where breakpoints are sent to the application. The feature responsible for it is send_breakpoints. It always send request of type rqst_break to the application even for sending a stepinto flag, or a new limit for the depth stack.
The prototype for sending a breakpoint is the following:
send_rqst_3 (rqst_break, real_body_id - 1,
what_to_do, line_number)
what_to_do can take 4 values which are
· break_set: to set a new breakpoint
· break_remove: to remove a breakpoint
· break_set_stack_depth: to set a new limit for the stack depth (step-out/step-by-step). Supplying the body_id is senseless, so we put zero instead. And instead of the line number, we give the new stack depth limit.
· break_set_stepinto: to activate the step-into flag. Here both body_id and line number are useless. So we put zero instead.
Communication Process
Es4 |
The user changes the status of one breakpoint |
Es4àDaemon |
Es4 sends an EWB_NEWBREAPOINT message to the Daemon |
DaemonàApplication |
The Daemon writes into Application’s interrupt_flag the value 2. The value 1 is reserved for a classic interruption |
ApplicationàEs4 |
The application put itself into server mode (it now listens to EiffelBench requests) and send an APP_NEWBREAKPOINT message to Es4 |
Es4àApplication |
Es4 sends the modified breakpoints to the Application and then sends the RESUME message causing the application to resume it’s execution |