FpDebug

From Lazarus wiki

English (en)

About

FpDebug is a debugger written in Pascal for Pascal.

In detail:

  • FpDebug is written in Pascal
  • FpDebug uses the DWARF debug info format
    • The current implementation is a subset of DWARF with support for DWARF version 2 and 3. However the "subset" is merely the state of implementation. The aim is to extend this to implement the as much of the standard as possible.
    • The current implementation covers the subset that is used by FPC
    • FpDebug has special implementations to deal with FPC specific DWARF usage (bugs, missing info, implementation details)

Different meanings of "FpDebug"

FpDebug can refer to

The package "FpDebug"

This is the engine of the debugger. It is not IDE specific. The package is used by several IDE backends:

  • LLDB Debugger (with fpdebug): This debugger uses LLDB as a backend. In order to display Pascal-style results, it uses the "fpdebug" engine to show locals/watches.
  • GNU Debugger (with fpdebug): Using GDB for stepping/breakpoints/... and FpDebug for watches.
  • FpDebug internal Dwarf debugger: Pure FpDebug. Stepping,Running,Breakpoint,Watches,Locals,Stack....

FpDebug as reference to the IDE backend

The actual name of the IDE backend is LazDebuggerFp. This is the name of the Package that needs to be installed to use the debugger. However as seen above, in none technical context (such as captions in the IDE) this is referred to as "FpDebug ..."

Other FpDebug projects

The name is used for another project with the same goal.

Installation in the IDE

Install the package LazDebuggerFp

General Debugging Issues

These issues can not be fixed by the debugger alone. They need changes in FPC too. Some may also be affected by lack of support in DWARF.

These issues apply to all debugger backends.

Properties

Properties are one of the most wanted features for (any of) the debugger(s). Here is why they can not yet be inspected, and why it will still be a good while before this happens.

Before going into details: For "Properties without getter function" (i.e., "with direct field access") FPC (as means to a workaround of the issue) currently adds an entry for a variable with the property name. This means such properties can be displayed. They can also be written by the debugger (if the debugger supports changing data), and if they are written this is done bypassing any Setter procedure, since the debugger has no clue there even is a setter.

There are several missing pieces for allowing the debugger to show (and maybe modify) properties.

  • The debugger needs support to call functions
  • FPC needs a way to actually add info about the "properties" to the debug info.

As for calling function. This may just be missing implementation in the debugger. This has not yet been investigated in any more depth. It may or may not require additional info in the DWARF generated by FPC

As for adding "properties" to the debug info: DWARF (at least version 2 and 3 / not checked 4 or 5) does not actually have any way to encode this info. Consequently FPC currently does not add it either.

In case that DWARF does not add this in time, DWARF allows for vendor specific data (afaik). Up until now, no debugger would have known about any "private fpc data", so there was no point. In future such data could be agreed between FPC and FpDebug. But that is still a good way off. FpDebug does not even do function calling yet.


Strings (vs PChar and Array of Char)

Originally DWARF had no concept to encode strings. This has been added in DWARF 4 (need verification), but not yet been investigated for usability with FPC. Also FPC currently only implements DWARF 2 and 3 (with first steps towards version 4).

This leaves only 2 ways to encode a string in DWARF.

  • As pointer to char
  • As array of char

Which one is used depends on the version of FPC, and if DWARF 2 or 3 is used.

If it is "pointer to char", then this makes in indistinguishable from actual PChar. This is the reason all debuggers in Lazarus sometimes for a watch like "SomeString[4]" show the result "PChar: 'a'; String: 'b'". Because PChar is zero-based and string one-based, the index "4" can mean the 4th or the 5th char.

Recent FPC use the "array of char" when they generate DWARF 3. In that they have (currently) implementation detail differences between how they encode strings and real array of char. (Such as the omission of an optional/redundant bit of information. In this case: DW_AT_byte_stride). Those details are meaningless from the point of any other debugger, but FpDebug can detect them as it knows about those implementation details. Of course this is extremely fragile, the next version of FPC can always change this.

In a similar way FPC handles ShortStrings internally as record, and encodes them as this. Again distinguishing this is down to implementation details.

Modifying managed types

This applies to any debugger that can set new values to a watch (modify variable in the debugged exe).

Managed types can not be correctly modified.

To modify managed types the debugger would need to know about:

  • the ref count
  • copy on write behaviour
  • how to allocate/free memory / or better how to call the inc/dec_ref methods for each types, so they will do the work correctly

None of that info is available.

If you assign a new value to a string or dynamic array, then the ref count of the old and new value must be adjusted. If needed memory must be allocated or freed. If that is not done correctly it will at least cause a memory leak. But it can also cause crashes or random other buggy behaviour when the debugged exe continues.

In an array it is possible to change a single element. "SomeArray[5] := 7;". If the array is referenced from several variables, this will affect all of them. The same happens if you execute this statement in your app.

String (AnsiString) have copy-on-write. If you do "SomeString[4] := 'a'" then the variable from which you accessed the string gets a new copy, and other variables (that share the memory) should not be affected. The debugger does not have that info. Nor does it have access to the refcount and memory alloc/de-alloc (or internal string procs).

Calling function with managed types as param or result

Assuming function calling was implemented in general....

For the same reason as in "Modifying managed types" the debugger could not create strings or arrays in order to call functions that expect them as param.

Result values could be accepted, but would cause a memory leak.

Scope / Variable search order

There are various issues where FPC does not put enough information into the DWARF info so that a debugger could compute the correct scope for all variables. In same cases this may be due to DWARF not offering this, or not offering it at the time where it was implemented or last updated in FPC. Some of it may be fixable, if FPC would change the way it encodes the info.

Nested Procedures

var SomeVar: Integer; // global var

procedure Outer(NumText: string);
var 
  SomeVar: Integer; // local var, declared above "Nested"

  procedure Nested;
  var 
    I: Integer;
  begin
    WriteLn(OuterVarInScope);  
  end;

//var 
//  SomeVar: Integer; // local var, declared below "Nested"
begin
  Nested;
end;

The local var "SomeVar" hides the global var of the same name, if you are in the procedure "Outer". The local var is also visible (and hides the global) if you are in "Nested".

But if you comment the local var "declared above Nested" and uncomment the local var "declared below Nested", then the local var is no longer in the scope of "Nested". "Nested" would now see the global var. ("Outer" still sees the local).

The DWARF info currently only tells the debugger that "SomeVar" is declared as local var in "Outer". And that "Nested" is inside "Outer". But the DWARF info does not include if "SomeVar" is declared above or below "Nested". So the debugger can not tell the difference. The debugger will in both cases show the local "SomeVar" when paused in "Nested"


Global vars from other units

The DWARF info contains a section for each unit. All Symbols of a unit are listed in the correct section. However there is no info, which unit is in the "uses" clause of which other unit. And evidently even less, in which order they are used.

If there are several global variables of the same name, one in each of several units, then FPC uses the order of the uses clause (right to left) to find the variable that will be seen from code in the current unit.

If there is code in a unit FooBar that access a variable named NotInFooBar (that is not declared in unit FooBar), then this variable must be from one of the other units. If there are several variables named NotInFooBar in different units, then the debugger has no information which one FPC did use. The debugger is currently showing the first it finds. (Which is kind of random)

FPC Issues

This affects all debuggers

  • resourcestring

Currently not implemented https://bugs.freepascal.org/view.php?id=35571

  • const Foo = unicodestring('abc')

No debug info implemented https://bugs.freepascal.org/view.php?id=35569

  • type Foo = object (Dwarf 3) - Fixed in 3.3.3

Incorrectly encoded as class-instance https://bugs.freepascal.org/view.php?id=36017
The debugger can not display the data.

  • Dynamic array when cross compiling

https://bugs.freepascal.org/view.php?id=35567
The debugger may calculate the length wrong.

  • Wrong stride for bitpacked array - Fixed in 3.3.3

https://bugs.freepascal.org/view.php?id=36144
FpDebug currently has a workaround

Status

FpDebug supports Linux, Windows and MacOs.

Any functionality regarding execution control (stepping/pausing...) is only tested for Linux and Windows. Support for this on Mac is incomplete/fragile.

The information below is indicative and subject to updates / subject to further testing

Run / Stop

Implemented on all platforms.

Attach / Detach

Implemented on Windows and Linux.

Stepping

Implemented on all platforms, but unstable on MacOs

"Stepping out" is currently only implemented for functions that are compiled with "stackframe". This is the default, but if compiled with optimization (or hints in code) the compiler can produce function that omit there frame. In such function step-out may step out twice.

"Stepping out" has not yet any safety nets. Attempting to use it in code that is not produced by fpc, and has not the assumed stack layout may misbehave. In the worst case it may continue the app without any stepping.

Breakpoints

Implemented, including Conditions and other properties

Hardware Watchpoints (aka Data Breakpoints)

Partly implemented for most data types (subset of the types supported by the hardware), including Conditions and other properties.

All watchpoints will have global scope. Watching a local var will alert you the next time the variable's memory is overwritten (e.g. if another variable takes the memory).

"Write only" watchpoints act as "read/write"

Watchpoints also trigger if they are overwritten with the value they already have.

Watchpoints do not show the before/after dialog.

Watchpoints that are not aligned to an address that is a multiply of their size, may be triggered by neighbouring values. E.g., a Word starting at an odd address, can be triggered by the byte before or after it. A 64 bit pointer must be aligned to an 8 byte boundary, or can be triggered by neighbours.


Exceptions

Will stop at raise.

No stepping to except or finally block.

Thread and Stack

Displayed.

Does not yet deal with frames from code outside the app. If stopped in a library or kernel the debugger may not always be able to find the calling frames and display the stack up to the application.

Watches / Locals / Inspect

Implemented

  • Any missing types need to be discovered (i.e. not aware of any type that does not display, if not listed under "fpc issues"
  • currency types may not display the decimal dot (they will show value * 1000)
  • Not all setting from the watch properties are implemented (most are not)
  • Expressions:
    • The debugger accepts expressions that it will calculate. Not all Pascal operators have been implemented.
    • Expressions are calculated without range checks
      • simple terms (just an identifier)
        • Includes the value identifier "self" (if in a method), and the constants nil, true, false
      • typecasts
      • @ and ^ (address of and deref)
      • "^TFoo(x)" typecast x, as pointer to TFoo
      • () bracketed expression "a*(b+c)"
      • [] index for arrays, pointers, and pchar
      • . Foo.Bar access of members
      • constant numbers in decimal, hex ($), oct (&), bin (%)
      • constant string (ansistring)
      • +-*/ for int and float
      • mod for int
      • + for string
      • +- for pointer+int
      • div for int
      • = <> < > <= >= for int, float, string
      • not and or xor: for int and boolean

Modify variables (not implemented)

Not Done

Console Output

On Windows console is opened by OS. On Linux the output goes to the debugger window "Console output"

Cross bit-ness Debugging (32/64 bit)

FpDebug has no cross debugging between different platforms.

However, if your IDE has a different bitness that your project FpDebug may be able to debug it.

Windows

A 64 bit IDE
can debug both 64bit and 32bit applications
A 32 bit IDE
can only debug 32bit applications

Linux

Cross debugging has not yet been tested, but may be possible.

Not implemented yet

  • Modify watches
  • Changing Location of execution (while paused) / Resume execution from different line
  • Properties
  • Function calls
  • ...