Learning the Debugger, Part One

    I think you know that hacking software is not carried out by some mystical "hackers" - it is carried out by the same programmers as the majority of readers of this article. Moreover, they use the same tools as the software developers themselves. Of course, with reservations, since for the most part the toolkit is quite specific, but one way or another, a debugger is used in the analysis of software.

    Since most of my articles are aimed at people who are interested in using security in their software, I decided that presenting material with specific pieces of a security code (like those published earlier) would only confuse the reader. It is much easier to start from the basics and slowly give new material on an already finished base.

    Therefore, this article will discuss one of the basic tools of a programmer - a debugger.
    The purpose of the article: to consider the basic methods of working with the debugger, to show its advanced and rarely used features, to give an understanding of the work of the debugger mechanisms using examples and to consider a certain set of counteraction methods.

    The volume of the article turned out to be unexpectedly large, so I broke it into three parts:

    • In the first part, we will consider the capabilities of the debugger integrated into the IDE Delphi, give recommendations for its most optimal use and general tips for configuring the environment. The material in this section is intended for both novice developers and more trained specialists.
    • In the second part of the article, the underside of the debugger’s work will be considered using the example of its source code, the mechanisms used by it when debugging the application will be examined in detail, and options for the application memory modifications made by the debugger during operation will be shown.
    • The third part of the article will discuss the practical use of the debugger as an example of bypassing the protection of an application that uses some set of anti-debugging tricks.

    Actually, let's get started.


    1.1. Using breakpoints and modifying local variables


    One of the most commonly used tools of the built-in debugger is a breakpoint (BreakPoint - hereinafter BP). After installing BP, the program will work until it reaches a breakpoint, after which its work will be interrupted and control will be transferred to the debugger.

    The easiest way to install and remove BP is the hotkey “F5” (or its analogue in the menu “Debug-> Toggle breakpoint”). There are other ways, but more about them later.

    After the program is stopped, we can examine the values ​​of the local variables of the procedure in which the application was stopped, and also analyze the call stack that precedes the call to this procedure. Here we can change the values ​​of these variables.

    Where to put BP - of course there is no general answer. In essence, BP is designed to facilitate studying the operation of code, the correctness of which we are not sure of, or that explicitly contains an error that we cannot immediately detect.

    It is much easier to set a breakpoint and execute each line of code sequentially than spend hours studying the same code trying to figure out where it started to work differently than we intended.

    Let's look at the following example.

    There is a task: write code that will increase the value of the initially nullified variable 5 times and one more time by 123, after which it will output the result in the form of a 10-decimal and a 16-decimal value. The expected values ​​will be as follows: 128 and 00000080.

    Suppose the code is written with an error:

    var
      B: Integer = 123;
    procedure TForm1.FormCreate(Sender: TObject);
    var
      A: Integer;
    begin
      Inc(A);
      Inc(A);
      Inc(A, B);
      Inc(A);
      Inc(A);
      Inc(A);
      ShowMessage(IntToStr(A));
      ShowMessage(IntToHex(A, 8));
    end;
    


    This code will output any values ​​you like, but not the ones we wanted, because we did not initialize the variable “A” with zero. And since the variable “A” is local, it means that it is located on the stack, and we can never predict what value it will take at the beginning of this procedure. But we will assume that it is already the end of the working day, we are really tired (the eye is blurred) and just forgot to write a line with the variable initialization.

    As a result, we have code that displays incorrect values, and we want to quickly figure out the reason for this behavior. We put BP in the body of the procedure and run the program to execute:

    image

    It should be something like in the picture. BP is installed on the line Inc (A). At the bottom left, you can observe the value of all local variables of the FormCreate procedure (the window is called “Local Variables”), namely, the Self variable (it is passed implicitly and is always present in class methods), the Sender parameter, and the local variable “A” itself. Its value is 19079581. To the left in the center of the “WatchList” is the value of the variable “B”.

    Even a quick glance at the values ​​of both variables and the completed three lines of code, we can understand that the value of the variable "A" does not match the expected. Since two increments per unit and another increase by 123 were to be performed, we should have seen the value of the variable “A” number 125, and since there is a different value, this can only mean one thing - the initial value of the variable “A” was not true.

    To verify the correctness of our assumption, let's change the current value of the variable “A” to the correct one and continue the program to check if the results returned by the procedure were expected.

    There are two tools in the debugger for changing the values ​​of variables.

    The first - “Evaluate / Modify”, is called either through the menu or via the hot key “Ctrl + F7”. This is a very simple tool with a minimum of functionality, it is most often used.

    It looks like this:

    image

    To change the value of a variable in it, just specify the new value in the “New value” field and press the “Enter” key or the “Modify” button.
    The second tool - “Inspect”, is also available either through the “Run” menu, or already directly from the “Evaluate / Modify” dialog. This is a more advanced parameter editor, about it a little later.

    After changing the value of the variable “A”, pay attention to the changes in the list of values ​​of local variables:

    image

    Variable “A” has taken the correct value, and now we can continue the execution of our application by pressing “F9” or through the menu, selecting “Run”. As a result of such an intervention with the help of a debugger, the procedure will give us the expected numbers 128 and 00000080, and we can safely correct the procedure code, since we found the cause of the error in it and checked its execution with the correctly set value of the variable “A”.

    Now back to Inspect. In addition to the two indicated methods of calling it, it is also called by double-clicking on a variable in the "Local Variables" window, either through the context menu when you right-click on it, or by using the hot key "Alt + F5".

    This is a more “advanced” variable properties editor, but its use is justified when changing the properties of objects. It is somewhat inconvenient for changing a normal variable, and here's why.

    When you call it, you will first see this dialog:

    image

    It will describe the variable, its location in memory and its current value, and to change it, you will need to click the ellipsis button again, after which an additional window will appear:

    image

    Extra body movements for such a simple operations, like changing the value of an ordinary variable, are clearly redundant. But if you use it to change the properties of objects, then the picture will change a little.

    Through "Evaluate / Modify" access to the properties of the object is somewhat complicated in that it does not provide information directly about the object under study. For example, to get the handle of the canvas form, we will have to type the following text in it: “(Sender as TForm1). Canvas.Handle” - which is somewhat inconvenient, because we can be sealed up and it’s just corny to forget the name of this or that property.

    In the case of Inspect, there will be no such problem.

    For example, let's open the “Inspect” dialog not for the variable “A”, but for the variable Self (which, as I said earlier, is always implicitly present for all methods of objects).

    image

    As you can see, in this case we have access to almost all fields of the object, which we can change as we please, and we will not get confused in the names of the properties.

    1.2. Tracing (step-by-step debugging)


    The essence of tracing is the step-by-step execution of each line of code.

    Suppose we stopped at a previously set BP, we analyzed the code, and want to move on to the next line. In principle, we can also put BP on it and run the program. And for the next, and for those after her.
    In practice, setting BP on each line of the procedure code, we manually simulate what the debugger itself can do (more in the second section).

    And he knows the following:

    1. Command “Trace Into” (“F7”) - the debugger will execute the code of the current line of code and stop at the next. If the current line of code calls a procedure, the next line will be the first line of the called procedure.
    2. The “Step Over” command (“F8”) is similar to the first command, but no entry to the body of the called procedure occurs.
    3. The “Trace to Next Source Line” command (“Shift + F7”) is also an almost complete analogue of the first command, but it is used in the “CPU-View” window (this debugging mode is not considered in the article).
    4. Command “Run to Cursor” (“F4”) - the debugger will execute the program code up to the line where the cursor is located (provided that no other BPs were encountered during execution).
    5. Command “Run Until Return” (“Shift + F8”) - the debugger will execute the code of the current procedure until it exits. (Often used as a counter to the accidentally pressed "F7" and also under the condition that no other BPs were encountered during execution).
    6. In older versions of Delphi, the “Set Next Statement” command is available, with which we can change the progress of the program by setting any line of code as the current one. This feature is also available in the code editor where you can drag the arrow pointing to the current active line to a new position.

    These teams do not require detailed consideration. Let us dwell only on the “Trace Into” (“F7”) team.

    For example, take the following code:

    procedure TForm1.FormCreate(Sender: TObject);
    var
      S: TStringList;
    begin
      S := TStringList.Create;
      try
        S.Add('My value');
      finally
        S.Free;
      end;
    end;
    


    When performing the trace, at the moment when we are on the line S.Add (), we can have two options for the debugger reaction:

    1. we will go inside the TStringList.Add method,
    2. we won’t enter there.

    This behavior is caused by the settings of your compiler. The fact is that Delphi ships with two sets of DCUs for system modules. One with debug information, the second without. If we have a second module connected, then the “Trace Into” (“F7”) command in this case will work as “Step Over” (“F8”). Switching between modules in the compiler settings is configured:

    image

    And the parameter “Use Debug DCUs” is responsible for this functionality.

    1.3. More on compiler settings


    The options in the tab with the compiler settings directly affect what code will be generated when building your project. It is very important not to forget that when changing any of the items on this tab, a complete reassembly of the project (“Project> Build”) is required for the changes to take effect. These settings directly affect the behavior of your code in various situations, as well as the composition of the information available to you when debugging a project.

    Let's consider them in more detail:

    Group “Code generation”

    image

    Parameter “Optimization”

    This parameter directly affects the optimization of the code: when the parameter is enabled, the code will be generated in the most optimal way, taking into account both its size and execution speed. This can lead to a loss of access (even read) access to some local variables, because due to code optimization they can already be deleted from memory at the moment when we broke off on BP.

    As an example, take the code from the first chapter and dwell on the same BP, but with optimization turned on.

    image

    As you can see, the values ​​of previously available Self and Sender variables are no longer available. Also, due to the disabled “Use Debug DCUs” parameter, a dramatic change occurred in the “Call Stack” window, previously filled with more detailed information about the call list.
    Moreover, the Inspect tool also refuses to work with the Self object, giving the following error:

    image

    Parameters Stack Frames and Pentiom-safe FDIV

    I will skip the description of these parameters - they are not interesting at the debugging stage. In short: the first will help with self-analysis of the stack, the second is responsible for the nuances when working with a mathematical coprocessor. If someone is interested in the nuances, then my coordinates for communication in the profile.

    Parameter “Record field alignment”

    Global setting for alignment of unpacked records, which can be changed locally within the module using the directive “{$ Align x}” or “{$ A x}”

    For example, consider the following code:

    type
      T = record
        a: Int64;
        b: Byte;
        c: Integer;
        d: Byte;
      end;
    


    The size of this record, which we can get through SizeOf (T), will be different for each of the alignment settings:

    {$ Align 1} = 14
    {$ Align 2} = 16
    {$ Align 4} = 20
    {$ Align 8} = 24

    Syntax options group.

    image

    It’s better not to touch anything at all. For, if you try, you can even make the standard VCL refuse to assemble.

    I will only dwell on the “Complete boolen eval” parameter, because from time to time some of them turn it on. It faces an error when executing the following code:

    function IsListDataPresent(Value: TList): Boolean;
    begin
      Result := (Value <> nil) and (Value.Count > 0);
    end;
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      if IsListDataPresent(nil) then
        ShowMessage('boo...');
    end;
    


    Since, when this setting is turned on, the Boolean expression will be checked in its entirety, an error will occur when accessing Value.Count, despite the fact that the first check determined that the Value parameter is nullified. And if you enable (for example) the “Extended syntax” parameter, then this code will not be collected at all by complaining about the undeclared Result variable.

    Runtime errors group

    image

    Range checking parameter

    This is one of the most popular parameters when debugging an application. He is responsible for checking boundaries when accessing the data array.

    In the simplest case, an exception will be thrown when you execute this code:

    const
      A: array [0..1] of Char = ('A', 'B');
    procedure TForm1.FormCreate(Sender: TObject);
    var
      I: Integer;
    begin
      for I := 0 to 100 do
        Caption := Caption + A[I];
    end;
    


    Here we are simply trying to access the element of the array, and in principle, with the “Range checking” option disabled, if we do not go beyond the allocated memory, this code only threatens us with a strange line in the form header.

    image

    Which is unpleasant, but uncritical for program execution. It is much worse if you made a mistake with the boundaries of the block when trying to write to it - in this case, the memory of the application may be destroyed.

    Consider this example, we turn off optimization:

    type
      TMyEnum1 = (en1, en2, en3, en4, en5);
      TMyEnum2 = en1..en3;
    procedure TForm1.FormCreate(Sender: TObject);
    var
      I: TMyEnum1;
      HazardVariable: Integer;
      Buff: array [TMyEnum2] of Integer;
    begin
      HazardVariable := 100;
      for I := Low(I) to High(I) do
        Buff[I] := Integer(I);
      ShowMessage(IntToStr(HazardVariable));
    end;
    


    What do you think will be the value of the number HazardVariable after the execution of this code? No, not 100. It will be 4. Since we made a mistake when choosing the type of iterator and wrote TMyEnum1 instead of TMyEnum2, the range of the boundaries of the array was exceeded and the data on the stack was lost, changing the values ​​of local variables stored on it.

    With optimization enabled, the situation will be even worse. We get the following error:

    image

    According to this description, we won’t even be able to guess exactly where the exception occurred, and why it occurred, since the addresses mentioned in the error text do not belong to the application’s memory, and if, God forbid, such an error occurs at the client - we cannot correct it according to this description.

    Therefore, take it as a rule - application debugging should always happen with the “Range checking” setting turned on!

    Also, this parameter controls the excess of permissible values ​​when changing the value of variables. For example, an exception will be raised when trying to assign a negative value to unsigned types like Cardinal / DWORD, or when trying to assign a value greater than a variable of this type can contain, for example, when assigning a 500 variable to a Byte type, etc. ...

    Parameter “I / O cheking »It is

    responsible for checking the I / O results when working with files in the Pascal style.

    I'm not sure that there is still software that uses this approach, but if you suddenly still work with Append / Assign / Rewrite, etc., then enable this option when debugging the application.

    Parameter “Overflow cheking”

    Controls the results of arithmetic operations and raises an exception when the result is out of the range of the variable.

    To make it easier to understand the differences between this parameter and “Range checking”, consider the following code:

    procedure TForm1.FormCreate(Sender: TObject);
    var
      C: Cardinal;
      B: Byte;
      I: Integer;
    begin
      I := -1;
      B := I;
      C := I;
      ShowMessage(IntToStr(C - B));
    end;
    


    This code will not raise an exception when the “Overflow cheking” option is enabled. Although unacceptable values ​​are assigned to variables here, mathematical operations are not performed on them. However, an exception will be raised when the “Range checking” option is enabled.

    Now consider the second version of the code:

    procedure TForm1.FormCreate(Sender: TObject);
    var
      C: Cardinal;
      B: Byte;
    begin
      B := 255;
      Inc(B);
      C := 0;
      C := C - 1;
      ShowMessage(IntToStr(C - B));
    end;
    


    There will no longer be a reaction from the Range checking parameter, but an EIntegerOverflow exception will be raised, which Overflow cheking is responsible for, on the lines Inc (B) and C: = C - 1 due to the fact that the result of the arithmetic operation cannot be stored in the corresponding variable.
    Thus, when working with variables, both parameters complement each other.

    Overflow cheking is not as critical as Range checking, but it is still advisable to keep it turned on when debugging the application.

    A small nuance: if you suddenly implement cryptographic algorithms, then in them, as a rule, the overflow operation is standard. In such situations, transfer the code to a separate module and write the “{$ OVERFLOWCHECKS OFF}” directive at the beginning of the module to disable overflow checking in the current module.

    Debugging group

    image

    This tab is very simple. All parameters except the “Assertions” parameter in no way affect the final code of your application. Depending on the activity of certain parameters, the completeness of the debugging information in the DCU file for each module changes. Based on this information, the debugger synchronizes the assembler listing of the program with its real code implemented by the programmer, recognizes local variables, etc. When compiling the application, this debugging information does not fit into the body of the application.
    The only exception is the “Assertions” parameter - it is responsible for the operation of the Assert () procedure. If this parameter is disabled, Assert is not executed; otherwise, it is executed, and its code will also be placed in the application body at the compilation stage.

    In summary.

    At the stage of debugging the application, it is advisable to keep all parameters from the “Runtime errors” and “Debugging” groups enabled, and to disable them during the final compilation of the release application. In Delphi 7 and below, this will have to be done by hand, but starting with Delphi 2005 and above, normal support for project builds has appeared, in which you can specify these flag combinations personally for each type of assembly.

    1.4. Call Stack Window


    If BP is our main tool when debugging an application, then Call Stack is the second most important.

    This window looks as follows:

    image

    It contains a full description of calls that were made before the debugger interrupted the program on the installed BP (or stopped due to an error). For example, the screenshot shows a stack of calls that occurred when a button on the form was clicked. It began with the arrival of the WM_COMMAND (273) message in the TWinControl.DefaultHandler procedure.

    Having this list in hand, we can quickly switch between calls by double-clicking (or through the “View Source” menu), view a list of local variables for each call (“View Locals”), set BP on any call.

    Of course, it does not have many possibilities, but nevertheless it greatly facilitates the work when debugging, since in most cases it allows you to quickly localize the place where the error occurred.

    For example, the call stack will look like this when an EAbstractError error occurs:

    image

    In this case, it’s enough to find the very first call from above, the code of which is not located in Delphi system modules, in order to most likely say that the error is in it. Such a call is Unit1.TForm1.Button1Click () - this is the Button1 button handler in which the following code was executed:

    procedure TForm1.Button1Click(Sender: TObject);
    begin
      TStrings.Create.Add('qwe');
    end;
    


    Another use case is tracing the calls of certain functions. For example, we have a very large application code, and somewhere deep inside it a MessageBox is called, but we cannot find this place with a hitch to localize the place of the call of this MessageBox. To do this, you can use the following method:
    1. go to the module where the call of the function of interest to us is declared (in this case, windows.pas),
    2. find its declaration (line with a blue dot function MessageBox; external user32 ...),
    3. install BP on this line and run the program.

    As soon as a MessageBox call is made from anywhere in the program, our BP will work and we will be able, based on the Call Stack data, to find out the exact place of its call.

    1.5. Working with advanced breakpoint properties


    Suppose we know exactly where in the algorithm we have an error, but this error does not always happen, and when the algorithm performs operations with only certain data, in this case it makes no sense to look at all the BP triggers in anticipation of the moment when the algorithm started to work True, it’s much easier to tell our BP to expect the necessary data, skipping all the excess.

    This is done through the breakpoint properties dialog. It is called either through the BP properties in the application code.

    image

    Or in the Breakpoint list window also through the properties of the selected BP.

    image

    The settings dialog looks like this:

    image

    The Condition parameter is responsible for the condition for the breakpoint to trigger.
    The “Pass count” parameter indicates how many such conditions must be skipped before the BP is activated, and the number of operations is counted from the very first, taking into account the value of the “Condition” parameter.

    Consider an abstract example:

    procedure TForm1.FormCreate(Sender: TObject);
    var
      I, RandomValue: Integer;
    begin
      RandomValue := Random(100);
      for I := 1 to 100 do
        RandomValue := RandomValue + I;
      ShowMessage(IntToStr(RandomValue));
    end;
    


    Let's say BP is installed on the seventh line (RandomValue: = ...). If the program is easy to run, then we get exactly 100 BP operations on hand. In order for this BP to work every tenth call, you need to set the value 10 in the “Pass count” property. In this case, we get exactly ten BP operations, at that moment the code iterator “I” will be a multiple of ten.

    Suppose now we want to start the analysis after iteration 75 inclusive, for this we set the following condition in the “Condition” parameter: I> 75. In this case, this BP will work only two times: at the moment when the iterator “I” is 85, and the second time, with a value of 95. This

    happened for the following reasons:

    In the first case, when we did not have a condition, BP worked at each iteration of the cycle, but since the “Pass count” parameter was specified, control did not go to the debugger, and there was only an increase in the number of BP operations until their number did not become equal to the specified in the “Pass count”. Therefore, we received control only every tenth iteration (after which the counter was reset to zero).

    In the second case, an increase in the counter of trips began to occur only after the initial condition “Condition” was fulfilled, that is, while the “I” iterator was less than or equal to the number 75, the debugger considered that the condition was not fulfilled and continued to execute the program. As soon as the first condition is fulfilled, an increase in the number of trips began, which became equal to the value of the “Pass count” parameter exactly at the moment when the iterator “I” reached 85.

    Naturally, if we want the BP to start working immediately after the iterator exceeds “I "Of the number 75, then the parameter" Pass count "must be set to zero.

    By grouping these two parameters, we can more flexibly adjust the triggering conditions of our BPs.

    Now consider one small nuance.

    The conditions for transferring control to the debugger specified in the BP properties are calculated by the debugger itself. Those. roughly (taking the above example as a basis), despite the fact that we interrupted BP only two times, in fact, the interrupt occurred all 100 times , we just did not get control at those moments that did not meet the conditions we set.

    Why this is bad: if you analyze a sufficiently long cycle in this way (for example, from ten thousand iterations and above), then debugging the program can slow down your work very much.

    You can check on the following code:

    procedure TForm1.FormCreate(Sender: TObject);
    var
      I, RandomValue: Integer;
    begin
      RandomValue := Random(100);
      for I := 1 to 10000 do
        RandomValue := RandomValue + I;
      ShowMessage(IntToStr(RandomValue));
    end;
    


    Set BP on the same seventh line and indicate in the parameter “Condition” the value I = 9999. Even on such a small cycle, we will have to wait for the condition to work in the region of 3-5 seconds. Of course, this is not convenient. In such cases, it is easier to modify the code as follows:

    procedure TForm1.FormCreate(Sender: TObject);
    var
      I, RandomValue: Integer;
    begin
      RandomValue := Random(100);
      for I := 1 to 10000 do
      begin
        RandomValue := RandomValue + I;
        {$IFDEF DEBUG}
        if I = 9999 then
          Beep;
        {$ENDIF}
      end;
      ShowMessage(IntToStr(RandomValue));
    end;
    


    ... and put BP on Beep, than wait for such a long time. In this case, we will get control on almost instantly.
    (The DEBUG directive will not be present in the release build of the project and the debugging code will not get into it, but it’s better, after debugging, to remember to delete all these debugging Beeps.)

    Such “brakes” are caused by the fact that all the debugger’s interactions with the debugged application occurs through the structural exception handling (SEH) mechanism, better known to Delphi programmers through a scanty wrapper over it in the form of try..finally..except. Working with SEH is one of the most "heavy" operations for the application. In order not to be unfounded and to show its influence on the program’s work visually, consider this code:

    function Test1(var Value: Integer): Cardinal;
    var
      I: Integer;
    begin
      Result := GetTickCount;
      for I := 1 to 100000000 do
        Inc(Value);
      Result := GetTickCount - Result;
    end;
    function Test2(var Value: Integer): Cardinal;
    var
      I: Integer;
    begin
      Result := GetTickCount;
      for I := 1 to 100000000 do
        try
          Inc(Value);
        finally
        end;
      Result := GetTickCount - Result;
    end;
    procedure TForm1.FormCreate(Sender: TObject);
    var
      A: Integer;
    begin
      A := 0;
      ShowMessage(IntToStr(Test1(A)));
      A := 0;
      ShowMessage(IntToStr(Test2(A)));
    end;
    


    In the functions Test1 and Test2, the transmitted value is incremented 100 million times.

    In the first case, it runs in the region of 210 milliseconds, while in the second case it is four or a half times longer, and there are essentially no changes between them, it's just a dull try..finally.

    This, by the way, is also in your “piggy bank” - if possible, do not insert exception handling inside loops, it is better to take it out of limits ...

    We have not considered the “Group” parameter, it is responsible for including BP in a certain group of breakpoints. A group is a conditional concept, in fact it is a kind of identifier arbitrarily set by the developer, but it is convenient in that you can apply group operations to these identifiers by controlling the activity of all BPs in this group.

    Group operations are configured in the advanced BP

    image

    settings: The parameters are responsible for this: “Enable group” - activating all BP groups, and “Disable group” - disabling all BP included in the group.

    Also, in group operations, the “Break” parameter is often used, which is responsible for the actions of the debugger when it reaches BP. If this parameter is not active, then interruption of program execution when this BP is reached does not occur.
    Important - this option does not disable BP itself.

    In the general case, the use of group operations is used in cases where debugging using conventional BPs is not convenient.

    Let's look at the use of groups in the following example, it just shows the inconvenience of using conventional BPs and working with group operations.

    Before compiling the example, be sure to enable the “Overflow cheking” option in the compiler settings and disable optimization.

    function Level3(Value: Integer): Integer;
    var
      I: Integer;
    begin
      Result := Value;
      for I := 0 to 9 do
        Inc(Result);
    end;
    function Level2(Value: Integer): Integer;
    var
      I: Integer;
    begin
      Result := Value;
      for I := 0 to 9 do
        Inc(Result, Level3(Result) shr 1);
    end;
    function Level1(Value: Integer): Integer;
    var
      I: Integer;
    begin
      Result := Value;
      for I := 0 to 9 do
        Inc(Result, Level2(Result) shr 3);
    end;
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      ShowMessage(IntToStr(Level1(0)));
    end;
    


    After running this code, an exception will occur on the sixteenth line

    Inc(Result, Level3(Result) shr 1);.
    


    Usually, in order to understand the causes of the error, they put BP on the line where the exception was raised, but in this specific example this will not help us. The reason is simple: this line will be executed successfully dozens of times in a row, and if we put BP on it, then all these dozens of times we will have to press “F9” until we reach the exception itself.

    In the process of debugging, the application usually restarts many times, and to make it convenient to debug, we need to install BP on this line so that it works immediately before the error.

    Let's do it as follows:

    1. We put BP on the sixteenth line and assign the group “level2BP” to it.
    2. Отключим данную группу, чтобы установленная ВР не срабатывала раньше времени. Для этого в процедуре FormCreate поставим новую ВР на ShowMessage и в параметре «Disable group» укажем группу «level2BP». Чтобы не прерываться на новой ВР, в его настройках отключим параметр «Break».
    3. В функции Level1 устанавливаем ВР на строчке №25. Посчитаем, сколько раз выполнится данная ВР перед появлением ошибки.
    4. Выясняем, что было 9 прерываний (итератор I в этот момент равен восьми). Значит, нам нужно пропустить первые 8 прерываний, в которых ошибок не обнаружено, и на девятом включить ВР из группы «level2BP». Для этого заходим в свойства текущей ВР и выставляем в параметре «Condition» значение I=8, после чего исключаем его из обработки через отключение параметра «Break» и в настройках «Enable group» прописываем «level2BP».
    5. Перезапустив приложение, мы сразу прервемся в процедуре Level2, но не в момент самой ошибки – ошибка произойдет через несколько итераций. Несколько раз нажмем F9, считая количество итераций, и выясним, что это происходит в тот момент, когда итератор I был равен 5. В параметре «Condition» текущей ВР установим условие I=5, после чего можно смело перезапускать приложение.
    6. Первое же прерывание в отладчике произойдет непосредственно в месте возникновения ошибки, откуда и можно приступать к разбору причин ее возникновения.


    If not everything is clear from the description of the example, watch a video demonstrating the whole sequence of actions:
    rouse.drkb.ru/blog/bp3.mp4 (17 Mb).
    (I’m sorry, but I couldn’t insert the link so that the video was displayed directly in the body of the article, therefore, only the link)

    Thus, using only three BP and group operations on them, we accurately localized the place of the error and ensured the convenience of debugging the code .

    Why wasn’t the “Pass Count” parameter used in the example, but the conditions were set through the “Condition” parameter? The fact is that the “Pass Count” just turns off the interrupt on BP. BP itself is executed (because the conditions for its fulfillment are described in the “Condition” parameter) and once it is fulfilled, its group operations are also performed.

    It remains to consider a few more parameters.

    The “Ignore subsequent exceptions” parameter disables the debugger’s response to any exception that occurs after BP execution with this parameter enabled.

    The “Handle subsequent exceptions” parameter cancels the previous parameter, returning the debugger to normal operation mode.

    To see how it looks, create the following code:

    procedure TForm1.Button1Click(Sender: TObject);
    begin
      ShowMessage('All exceptions ignored');
    end;
    procedure TForm1.Button2Click(Sender: TObject);
    begin
      PInteger(0)^ := 123;
    end;
    procedure TForm1.Button3Click(Sender: TObject);
    var
      S: TStrings;
    begin
      S := TStrings.Create;
      try
        S.Add('abstract')
      finally
        S.Free;
      end;
    end;
    procedure TForm1.Button4Click(Sender: TObject);
    begin
      ShowMessage('All exceptions handled');
    end;
    


    On the first ShowMessage, put BP, turn it off by unchecking the “Break” parameter, and turn on the “Ignore subsequent exceptions” parameter.

    On the second ShowMessage do the same, just enable the “Handle subsequent exceptions” parameter.

    Launch the application from the debugger and click on the buttons in the following order:

    1. Button1
    2. Button2
    3. Button3
    4. Button4
    5. Button2
    6. Button3

    Despite the fact that the Button2 and Button3 buttons generate an exception, at stages 2 and 3 the debugger will not react to them in any way, we will wait for the reaction from it only at stages 5 and 6 after we activate the normal exception handling by pressing the Button4 button.

    There are 2 parameters left:

    “Log message” - any text line that will be displayed in the event log when BP is reached.

    “Eval expression” - when BP is reached, the debugger calculates the value of this parameter and (if the “Log result” flag is enabled) displays it in the event log. The value for the calculation can be any, at least the same "123 * 2".

    1.6. Using Data Breakpoint, Watch List, and Call Stack


    Everything that we considered earlier belonged to the so-called “Source Breakpoint”. That is, to breakpoints set directly in the application code.

    But, in addition to the code, we work with data (variables, arrays, just sections of allocated memory) and the debugger has the ability to set BP to the addresses at which this data is located in memory using the “Data breakpoint”.

    BP is set to the memory address via the “Watch List” (not in all Delphi versions) or in the “Breakpoint List” window using “Add Breakpoint-> Data Breakpoint”, where, in the dialog that appears, specify the desired address, size of the area to be monitored, or variable name If the variable name is specified, the debugger will try to calculate its location in memory and (if possible) install BP.

    The trouble is that getting this value is quite difficult due to the fact that every time the application starts, the address at which the variable is located will be different each time.

    What is the scope of a variable - you should know. Global variables are always available to us, and even without launching the application, the debugger provides us with the ability to set the “Data breakpoint” to changes in such variables. True, in this case, it calculates the address of such a variable based on the previous build of the application, and not the fact that it will coincide with its location at the next start. The situation is much worse with local variables. The scope of a variable is not just an introduced concept, local variables are located on the stack, and as soon as they leave the scope, the place they occupied earlier is used to store completely different data. Thus, you can set the "Data breakpoint" to a local variable only at that moment, until it goes out of scope.

    Those who previously worked with professional debuggers will probably recognize in “Data breakpoint” one of the basic application analysis tools - “Memory Breakpoint”.

    Unfortunately, the Delphi debugger is not positioned as a professional tool for debugging third-party applications, so a useful tool like “Memory Breakpoint” is presented in it in a cropped version, where only the ability to control the memory address for recording is left from it.

    But even in this limited form, it remains an excellent tool for monitoring changes in the application’s memory, especially for tracking errors in address arithmetic.

    Consider the following code:

    type
      TTest = class
        Data: array [0..10] of Char;
        Caption: string;
        Description: string;
        procedure ShowCaption;
        procedure ShowDescription;
        procedure ShowData;
      end;
      TForm1 = class(TForm)
        procedure FormCreate(Sender: TObject);
      private
        FT: TTest;
        procedure InitData(Value: PChar);
      end;
    var
      Form1: TForm1;
    implementation
    {$R *.DFM}
    { TTest }
    procedure TTest.ShowCaption;
    begin
      ShowMessage(Caption);
    end;
    procedure TTest.ShowData;
    begin
      ShowMessage(PChar(@Data[0]));
    end;
    procedure TTest.ShowDescription;
    begin
      ShowMessage(Description);
    end;
    { TForm1 }
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      FT := TTest.Create;
      try
        FT.Caption := 'Test caption';
        FT.Description := 'Test Description';
        InitData(@FT.Data[0]);
        FT.ShowCaption;
        FT.ShowDescription;
        FT.ShowData;
      finally
        FT.Free;
      end;
    end;
    procedure TForm1.InitData(Value: PChar);
    const
      ValueData = 'Test data value';
    var
      I: Integer;
    begin
      for I := 1 to Length(ValueData) do
      begin
        Value^ := ValueData[I];
        Inc(Value);
      end;
    end;
    


    At first glance, everything seems to be correct, but when we run the program we will get something like this:

    image

    By clicking on “Break” we will end up somewhere inside the “system” module:

    image

    The code we broke on cannot tell us anything about the reason an error occurred, but we have a “Call Stack” window, based on which we can conclude that the error occurred when the ShowCaption procedure was called in the main program module.
    If you install BP in this procedure and restart the program, and then, when BP fires, check the value of the Caption variable, it turns out that this variable is not available:

    image

    This means that somewhere there was a memory corruption and the data at the Caption variable address was lost. “Data breakpoint” will help us determine this place.

    1. Дождемся инициализации переменной Caption, для этого установим ВР на строчке FT.Description := 'Test Description';.
    2. При срабатывании ВР, добавим переменную FP.Caption в «Watch List» и в свойствах этой переменной выберем «Break When Changed». Если данного пункта меню у вас нет (например, в Delphi 2010 он отсутствует), то установим «Data breakpoint» немного другим способом. В «Breakpoint List» выбираем «Add->Data Breakpoint», в появившемся диалоге указываем имя переменной FP.Caption и нажимаем ОК.
    3. Запускаем программу на выполнение.


    After completing these steps, the program will stop at line number 68 - Inc (Value). The peculiarity of the “Data breakpoint” is that the stop occurs immediately after the changes that have occurred, and not when trying to change the controlled memory, therefore the place where the recording takes place at the address of the FP.Caption variable is located at the line above - this is the line Value ^: = ValueData [I ].

    Now, having found the problem place, we can correct the error itself. It lies in the fact that the length of the ValueData string that we write to the Data buffer exceeds the size of the buffer, which overwrites the memory in which the Caption and Description variables are located.

    1.7. Finally


    This concludes my brief overview of the capabilities of the integrated debugger. There are several unexplored nuances, such as: setting ignored exceptions, BP when loading a module, etc., but they are insignificant and are rarely used in practice.
    The debugging mode in the “CPU-View” window and the associated Address Breakpoint remained unexamined. I also decided to skip it, because to my readers who are not familiar with assembler, my explanation will not give anything, and more savvy specialists know what CPU-View is and how to use it correctly without me :)

    In the second part of the article, software debugger implementation will be considered. It will show what exactly happens when installing BreakPoint, shows the back side of the Data Breakpoint, not implemented in the Delphi debugger, shows how the tracing is actually done (using two methods, classic via TF flag and based on GUARD pages), as well as a mechanism Hardware Breakpoint, also missing in the Delphi integrated debugger.

    Special thanks to the Delphi Masters forum community for their help in preparing the article, as well as personal thanks to Andrei Vasiliev aka Inovet, Timur Vildanov aka Palladin and Dmitry aka Brother Ptiburdukov for proofreading and valuable advice.

    Alexander (Rouse_) Bagel
    Moscow, October 2012

    Also popular now: