Difference between revisions of "Lazarus Debugger Implementation"

From Lazarus wiki
(Debugger-Backend)
Line 1: Line 1:
 
{{Lazarus_Debugger_Implementation}}
 
{{Lazarus_Debugger_Implementation}}
  
== Introduction ==
+
= Introduction =
  
 
This page provides an overview of how the debugger is implemented in the Lazarus IDE.
 
This page provides an overview of how the debugger is implemented in the Lazarus IDE.
Line 11: Line 11:
 
;Debugger-Backend: Any code to control the debugged app, and retrieve/modify data from/in that app. The back-end may itself be split into an '''IDE back-end''' and an '''external back-end''' (e.g. gdb).
 
;Debugger-Backend: Any code to control the debugged app, and retrieve/modify data from/in that app. The back-end may itself be split into an '''IDE back-end''' and an '''external back-end''' (e.g. gdb).
  
== Debugger-Frontend ==
+
= Debugger-Frontend =
  
 
The code for the debugger frontend belongs currently directly to the IDE (to the project ide/lazarus.lpi). It is not a package of it's own.
 
The code for the debugger frontend belongs currently directly to the IDE (to the project ide/lazarus.lpi). It is not a package of it's own.
Line 24: Line 24:
 
BaseDebugManager is currently based on components\debuggerintf\dbgintfdebuggerbase.pp
 
BaseDebugManager is currently based on components\debuggerintf\dbgintfdebuggerbase.pp
  
== Debugger-Interface ==
+
= Debugger-Interface =
  
 
   Location: components/debuggerintf/
 
   Location: components/debuggerintf/
Line 30: Line 30:
 
{{Note|The debugger interface is still subject to change. It was created by just extracting code from the GdbMiDebuggers (gdb/server/ssh) - which were at the time the only debuggers. The IDE now having several other debuggers may in future warrant a redesign of this interface }}
 
{{Note|The debugger interface is still subject to change. It was created by just extracting code from the GdbMiDebuggers (gdb/server/ssh) - which were at the time the only debuggers. The IDE now having several other debuggers may in future warrant a redesign of this interface }}
  
=== TDebuggerIntf - main class ===
+
== TDebuggerIntf - main class ==
  
 
TDebuggerIntf provides the communication between front ad back-end.
 
TDebuggerIntf provides the communication between front ad back-end.
  
==== State ====
+
=== State ===
  
 
Note: Currently State contains some value internal to the backend. (dsIdle/dsInit). Those may vary from back-end to back-end
 
Note: Currently State contains some value internal to the backend. (dsIdle/dsInit). Those may vary from back-end to back-end
Line 44: Line 44:
 
See documentation in source code.
 
See documentation in source code.
  
==== RequestCommand ====
+
=== RequestCommand ===
  
 
The front-end can send asynchronous commands to the back-end. A result - if any - will be provided via an event.
 
The front-end can send asynchronous commands to the back-end. A result - if any - will be provided via an event.
Line 66: Line 66:
 
They will take additional care of calling '''Init''' if needed.
 
They will take additional care of calling '''Init''' if needed.
  
==== Events ====
+
=== Events ===
  
=== TDebuggerDataMonitor and TDebuggerDataSupplier - stack, watches, .... ===
+
== TDebuggerDataMonitor and TDebuggerDataSupplier - stack, watches, .... ==
  
 
The debugger can supply data like Threads, Stack, Locals, Watches... Any of this data is provided by a ___DataSupplier. On the IDE site to each Supplier, one ___DataMonitor is attached.
 
The debugger can supply data like Threads, Stack, Locals, Watches... Any of this data is provided by a ___DataSupplier. On the IDE site to each Supplier, one ___DataMonitor is attached.
Line 97: Line 97:
 
The validity is internally also used to avoid duplicate triggers. The IDE can ask for a value as often as it wants. When it asks for the first time the validity changes to "requested", preventing further triggers to the back-end. Validity is usually reset, when the debugger leaves the dsPause state. (dsPause is a state related to the debuggers execution of the debuggee).
 
The validity is internally also used to avoid duplicate triggers. The IDE can ask for a value as often as it wants. When it asks for the first time the validity changes to "requested", preventing further triggers to the back-end. Validity is usually reset, when the debugger leaves the dsPause state. (dsPause is a state related to the debuggers execution of the debuggee).
  
== Debugger-Backend ==
+
= Debugger-Backend =
  
 
;"Command Queue":
 
;"Command Queue":
Line 106: Line 106:
  
  
=== GDB / GDBMI based ===
+
== GDB / GDBMI based ==
  
 
   Location: components/lazdebuggergdbmi
 
   Location: components/lazdebuggergdbmi
  
=== LLDB ===
+
== LLDB ==
  
 
   Location: components/lazdebuggers/LazDebuggerLldb
 
   Location: components/lazdebuggers/LazDebuggerLldb
 
             components/lazdebuggers/LazDebuggerFpLldb
 
             components/lazdebuggers/LazDebuggerFpLldb
  
=== FpDebug ===
+
== FpDebug ==
  
 
   Location: components\lazdebuggers\LazDebuggerFp
 
   Location: components\lazdebuggers\LazDebuggerFp

Revision as of 23:15, 11 February 2021

English (en)

Introduction

This page provides an overview of how the debugger is implemented in the Lazarus IDE.

The debugger comprises the following parts:

Debugger-Frontend
The controls (run, step, stop buttons) and dialogs (watches, stack) in the IDE.
Debugger-Interface
A set of base-classes defining the communication between the front-end and each back-end
Debugger-Backend
Any code to control the debugged app, and retrieve/modify data from/in that app. The back-end may itself be split into an IDE back-end and an external back-end (e.g. gdb).

Debugger-Frontend

The code for the debugger frontend belongs currently directly to the IDE (to the project ide/lazarus.lpi). It is not a package of it's own.

Some of this code may one day be moved into a package. And maybe even introduce a package DebuggerFrontendIntf, to allow alternative 3rd party front-ends.

The code currently is located in

  • folder /debugger
  • folder /ide, files: debugmanager.pas and basedebugmanager.pas
  • folders ide/frames and debugger/frames for configuration settings.

BaseDebugManager is currently based on components\debuggerintf\dbgintfdebuggerbase.pp

Debugger-Interface

 Location: components/debuggerintf/
Note-icon.png

Note: The debugger interface is still subject to change. It was created by just extracting code from the GdbMiDebuggers (gdb/server/ssh) - which were at the time the only debuggers. The IDE now having several other debuggers may in future warrant a redesign of this interface

TDebuggerIntf - main class

TDebuggerIntf provides the communication between front ad back-end.

State

Note: Currently State contains some value internal to the backend. (dsIdle/dsInit). Those may vary from back-end to back-end

There are 2 pause states.

  • The proper pause (debugged app is paused) = dsPause.
  • And dsInternalPause used for example for breakpoints that will automatically continue to run the debuggee.

See documentation in source code.

RequestCommand

The front-end can send asynchronous commands to the back-end. A result - if any - will be provided via an event.

Commands are (some entries of the list are outdated)

 TDBGCommand = (
   dcRun, dcPause,  dcStop,
   dcStepOver,  dcStepInto,  dcStepOut,  dcStepTo,
   dcRunTo,  dcJumpto, dcAttach,  dcDetach,
   dcBreak,  dcWatch,  dcLocal,  dcEvaluate,  dcModify,
   dcEnvironment, dcSetStackFrame,  dcDisassemble,
   dcStepOverInstr,  dcStepIntoInstr,
   dcSendConsoleInput  //, dcSendSignal
   );

The back-end does not need to implement all commands. (see procedure SupportedCommands) The availability of commands also depends on the State

There are helpers like

 procedure Run;

They will take additional care of calling Init if needed.

Events

TDebuggerDataMonitor and TDebuggerDataSupplier - stack, watches, ....

The debugger can supply data like Threads, Stack, Locals, Watches... Any of this data is provided by a ___DataSupplier. On the IDE site to each Supplier, one ___DataMonitor is attached. The Supplier/Monitor concept was introduced to allow both sides to have their own, yet synchronized list of items for the list-entries.


For an example: Watches.

The IDE creates a Watch as "TIdeWatch". The Ide can stick any data to a watch that the IDE may require. For example watch-properties (display, enabled, ...) or streaming.

The back-end may need different information, such as internal details retrieved from the external-backend (gdb). So the back-end uses "TGDBMIWatches = class(TWatchesSupplier)". And other back-ends have their own sub-classes.

The supplier/monitor concept keeps the two lists synchronized. For Watches, the IDE side (the monitor) creates and deletes the entries. For Stack and locals, it is the back-end that creates/deletes entries.

When the IDE needs a value then the request is forwarded to the data-supplier. A value in this context can be anything read from any of the involved objects. The count of thread/stackframes is a value, the frame itself is a value (which is an object with multiple data fields). And the evaluated result of a watch is a value too.


All data is retrieved event driven. Asking for Stackcount, or watch-result will not return the value immediately. It will trigger its evaluation, and the data-supplier will sent an update event when the value (any value) becomes available.

For this to work, each value has a validity "TDebuggerDataState".

  • AWatch.Values[ThreadId, Stackframe].Value and AWatch.Values[ThreadId, Stackframe].Validity
  • In other cases validity is not exposed. Count may return 0, if it is not yet valid. (Maybe a TODO, to expose the validity? Just maybe.)

There is no rule how many update-events may be triggered before some value becomes valid. Any code listening to events from the monitor simply has to start over when it gets an event. Events may occur nested, code must protect itself again re-entrance in such a case.


The validity is internally also used to avoid duplicate triggers. The IDE can ask for a value as often as it wants. When it asks for the first time the validity changes to "requested", preventing further triggers to the back-end. Validity is usually reset, when the debugger leaves the dsPause state. (dsPause is a state related to the debuggers execution of the debuggee).

Debugger-Backend

"Command Queue"

This is an item most/all backends have in common. Commands are received via RequestCommand and may take any amount of time to execute. Other work for the backend comes from the DataMonitor/Suppliers. And Backends may also schedule work of there own (for example retrieving the current location when an app enters pause). All this work is usually serialized in the backend (though backends are free to process multiple items paralel). For this backends have (at least) one CommandQueue. Usually this queue supports some sort of priority, so items in response to a user action (e.g. evaluating an expression for source editor hint) can be executed first. This means the user does not need to wait unnecessary long for this.


GDB / GDBMI based

 Location: components/lazdebuggergdbmi

LLDB

 Location: components/lazdebuggers/LazDebuggerLldb
           components/lazdebuggers/LazDebuggerFpLldb

FpDebug

 Location: components\lazdebuggers\LazDebuggerFp