Skip to content

fix(stepping): Step Over no longer runs past a procedure's own prologue - #25

Merged
ClarionLive merged 2 commits into
ClarionLive:mainfrom
geircodes:fix/step-over-prologue-and-armed-bp-restore
Aug 21, 2026
Merged

fix(stepping): Step Over no longer runs past a procedure's own prologue#25
ClarionLive merged 2 commits into
ClarionLive:mainfrom
geircodes:fix/step-over-prologue-and-armed-bp-restore

Conversation

@geircodes

Copy link
Copy Markdown

Fix Step Over running past a procedure's own prologue, and a stale breakpoint byte on stepi

Summary

  • Step Over (source-level stepover and disassembly nexti) ran an entire procedure to completion instead of stopping at the next line/instruction, whenever the step began exactly at a procedure's raw entry point (i.e. right after a prior Step Into landed there). Root cause: the ESP-depth gate in IsStepStop used a fixed 0x10-byte slack, far smaller than the local-frame reservation a procedure's own prologue performs (push ebp/mov ebp,esp/sub esp,N, or a folded ENTER N,0), so every statement in the body looked "too deep" and never qualified as a stop.
  • Separately: the disassembly view's raw single-instruction step ("stepi") could land exactly on an armed user breakpoint's address without restoring the original byte (every other stepping path does this via StopStepAndPause). The next step from there then hit the still-planted 0xCC as a real breakpoint exception, reporting reason "breakpoint" instead of "stepi" and spuriously jumping the host UI to source mid-disassembly-session.

Changes

  • BeginStep computes _startAtProcEntry (ResolveSymbol + a PROLOGUE_WINDOW-fuzzy range check against the entry RVA).
  • IsStepStop's StepMode.Over and StepMode.OverInstr cases bypass the ESP gate when _startAtProcEntry is set.
  • Extracted armed-breakpoint restore-and-reschedule into a shared RestoreIfArmed(tid, va), used by both StopStepAndPause and the raw stepi path in OnSingleStep.

Files: src/ClarionDbg.Cli/DebugEngine.cs, src/ClarionDbg.Cli/DebugEngine.Stepping.cs

Test plan

  • Builds clean (dotnet build on ClarionDbg.Cli)
  • Live-verified against a real target application: stepped into a procedure with several local objects and an ENTER N,0 prologue, confirmed Step Over now stops per line/instruction instead of running to completion
  • Live-verified the breakpoint-restore fix with a real breakpoint planted on the procedure's own entry line

Geir Ståle Eidissen and others added 2 commits July 23, 2026 15:52
…rionLive#19, ClarionLive#21)

BuildSymbols previously rejected nameRef == 0 as invalid, silently dropping
_main's definition (its record legitimately points at pool offset 0) and
leaving stack frames in _main mis-bound to the preceding module's last
symbol, with a phantom SELF local appearing. Also adds a name-recovery
heuristic for GROUP members whose nameRef is zeroed by the compiler despite
a valid pool string existing (e.g. StringTheory.Value), recovering ~99.5%
of confirmed cases by reading backward from a known-good neighbor.

Adds temporary diagnostic CLI commands (scanrva, scanname, typemembers,
typechain, poolback, scanmissingnames, surveymissingnames) used to
investigate both issues, plus related locals/stack-walker/eval fixes and
debugger.html/webview tweaks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both StepMode.Over (source-level) and StepMode.OverInstr (disassembly
"nexti") gated their stop decision on `esp + ESP_SLACK >= _startEsp`,
with ESP_SLACK a fixed 0x10 bytes -- enough only for alignment noise
between statements at the same frame depth. When a step began exactly
at (or within a callee's PROLOGUE_WINDOW of) a procedure's raw entry --
precisely where a prior Step Into lands, since Into's own stop check is
ESP-agnostic -- the callee's own prologue (whether spread across
`push ebp/mov ebp,esp/sub esp,N`, or folded into a single `ENTER N,0`)
legitimately drops ESP by N bytes reserving its local frame, almost
always far more than 0x10. That made the ESP gate reject every
candidate stop point inside the procedure body, so Over/OverInstr never
found a valid stop and the call-skip machinery (meant to hop over
local-object constructor calls) ran the whole procedure to completion
instead.

Added `_startAtProcEntry`, computed once in BeginStep via
ResolveSymbol + a PROLOGUE_WINDOW-fuzzy range check (not strict
entry-address equality, since a symbol's EntryRva can sit below its
first genuine line record). Both Over's and OverInstr's stop checks now
bypass the ESP gate for the one step session that began inside a
procedure's own prologue.

Separately, fix a real breakpoint left un-restored: the disassembly
view's raw single-instruction step ("stepi", StepMode-independent)
paused via PausedWait directly, without the "landed exactly on an armed
breakpoint -> restore original byte + reschedule rearm" handling every
other stepping path gets via StopStepAndPause. When stepping into a
procedure whose entry also carries a user breakpoint, the still-planted
0xCC then fired as a genuine EXCEPTION_BREAKPOINT on the very next
step/resume, reporting reason "breakpoint" instead of "stepi" and
spuriously jumping the host UI to source. Extracted the restore logic
into a shared RestoreIfArmed(tid, va) helper used by both
StopStepAndPause and the stepi path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@geircodes
geircodes force-pushed the fix/step-over-prologue-and-armed-bp-restore branch from 2e57587 to fef16da Compare July 27, 2026 14:12
ClarionLive pushed a commit that referenced this pull request Aug 21, 2026
…#19, #21)

Lands the commit shared by PRs #23, #24 and #25 on its own, so each of those
three collapses to just its unique commit instead of carrying ~1100 duplicated
lines.

Fixes:
  - #19 phantom SELF local in main() — BuildSymbols' `nameRef < 1` guard dropped
    _main's definition record (which legitimately points at pool offset 0),
    leaving _main a symbol-less hole that stack frames mis-bound to the previous
    module's last method. Adds ResolveSymbolVerified, cross-checking a candidate
    against the +0x1C line table rather than the non-comparable +0x28 moduleIdx.
  - #21 (unnamed+N) GROUP members, e.g. StringTheory.Value — recovers the name by
    reading backward in the symbol pool from the group's smallest-mNameRef member,
    with duplicate / '@' / '$' rejection. ~99.5% of confirmed cases.

Also carries temporary diagnostic CLI commands used in the investigation, related
locals/stack-walker/eval fixes, and debugger.html/webview tweaks.

Original author: Geir Stale Eidissen <ML-DEV-GSE@ml.no>
Verified: ClarionDbg.Cli + ClarionDebugger.Addin build clean (0 warnings).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ClarionLive
ClarionLive merged commit e9cfdb2 into ClarionLive:main Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants