Author: Arnaud PICHERY (aranud@mail.dotcom.fr)

Date: September 21, 1999

 

 

 

Inside the new Eiffel debugger

 

 

 

Main Improvements

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.

 

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 feature. 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 a breakable point, a feature or a class and droping it onto this button.

I. Run-time side

The execution vector

 

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... */

   int     ex_linenum;   /* current line number (line number <=> breakpoint slot) */

   int     ex_bodyid;   /* body id of the feature */

   unsigned char   ex_retry;   /* True if function has been retried */

   unsigned char   ex_rescue;   /* True if function entered its rescue clause */

   unsigned char   ex_locnum;   /* number of local variables in the function */

   unsigned char   ex_argnum;   /* number of arguments of the function */

   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).

The debug information

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 kind of 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 */  

};

 

The debugger hooks

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 and removing breakpoints on the fly

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.

Stepping into

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 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.

Stepping out

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.

Step-by-step

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.

 

 

II. Eiffel side

 

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

The BREAKPOINT class deals with the handling of one breakpoint. It has only a few attributes:

The breakable line number is the number of the line in the stop point view under EiffelBench where the breakpoint is set. The routine is the feature where the breakpoint is located. We also provide the real_body_id and the body_index which are extracted from the feature at the creation of the breakpoint and are updated after recompilation. real_body_id is useful because we need to send it to the application when we are setting a new breakpoint. body_index is used as a super-key (in the SQL sense). To get a breakpoint from a list of breakpoints, you only need its body_index and its line number; to compare to breakpoints, we will check whether they have the same body_index and the same line number. We can’t use the real_body_id to do this because the real_body_id may change during a recompilation.

 

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 not disabled, 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

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.

 

General Operations

 

   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 class

The APPLICATION_EXECUTION encapsulates most of the breakpoints features of the DEBUG_INFO class. There is nothing particular to explain here.

 


The EWB_REQUEST class

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.