-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathengine.go
More file actions
1316 lines (1226 loc) · 40.7 KB
/
Copy pathengine.go
File metadata and controls
1316 lines (1226 loc) · 40.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package debugger
import (
"encoding/binary"
"errors"
"fmt"
"log/slog"
"runtime"
"sync"
"github.com/bingosuite/bingo/pkg/protocol"
)
const (
eventBufSize = 64
maxStackDepth = 64
stepOverNextFile = "<stepover-next>"
stepOutReturnFile = "<stepout-return>"
)
type engineState uint8
const (
stateNoProcess engineState = iota
stateRunning
stateSuspended
stateExited
)
// engine implements Debugger. See AGENTS.md → engine concurrency model and
// shutdown sequence for the loop / waitLoop / dispatch invariants.
// bpResumeAction is what to do after stepping past a software breakpoint
// (restore bytes → single-step → reinstall trap → action).
type bpResumeAction uint8
const (
bpResumeContinue bpResumeAction = iota // ContinueProcess and keep running
bpResumeStep // emit EventStepped (machine-instruction)
bpResumeSourceStep // set temp BP at next source line, then continue
bpResumeStepOut // set return-addr BP, then continue
)
type engine struct {
backend Backend
proc process
bps *breakpointTable
dw *dwarfReader
events chan protocol.Event
cmdCh chan engineCmd
stopCh chan stopResult
// done is closed by the loop on exit; waitLoop selects on it to abandon
// pending sends to stopCh.
done chan struct{}
seq uint64
state engineState
mu sync.Mutex
// Software-breakpoint step-over state. lastBP is the BP the process
// stopped at; on next resume we restore bytes, single-step, reinstall
// the trap, then perform bpResumeAction. steppingOverBP is non-nil
// during the in-flight single-step.
lastBP *breakpointEntry
lastBPTID int // thread that hit lastBP (Mach port on Darwin)
steppingOverBP *breakpointEntry
// curTID is the thread the user is currently stopped on — the one that hit
// the last breakpoint or completed the last step. Updated on every
// user-visible suspend. Step primitives must target this thread, never
// threads[0]: darwin's task_threads returns creation order, so threads[0]
// is frequently an idle runtime M, and single-stepping the wrong thread
// leaves the intended thread parked while a sibling runs, corrupting the
// step-over state machine (see #92).
curTID int
bpResume bpResumeAction
bpRetAddr uint64 // bpResumeStepOut only
// Source-line target remembered from the previous step-over. More
// reliable than re-querying locationForPC, which can land on a DWARF
// boundary with line==0. Zeroed on each sourceStepOver and on user-BP hits.
stepOverFile string
stepOverLine int
// manualStopPending records that a Pause request has fired the backend's
// interrupt signal (PauseSignal — SIGSTOP on linux, SIGUSR2 on darwin) at
// the tracee and we are awaiting the resulting signal-delivery stop, which
// should be turned into EventPaused rather than auto-resumed. It needs no
// synchronization: both Pause()'s dispatched closure and handleStop run on
// the single engine loop thread. See AGENTS.md → Pause.
manualStopPending bool
// goLayout caches the DWARF-resolved runtime struct offsets used by the
// goroutine/thread snapshot reader. Resolved lazily per loaded image and
// reset in loadDWARF. nil until first use; see goroutines.go.
goLayout *goLayout
// prevGoids is the set of live goids from the previous snapshot, used to
// compute created/exited lifecycle deltas. Loop-thread-only (like
// manualStopPending); needs no synchronization. See goroutines.go.
prevGoids map[int]struct{}
// log is the single sink for all engine logging. Never call the
// package-level slog functions directly — they bypass the per-session
// logger the hub/server configure, producing duplicate, uncorrelated
// log lines. See AGENTS.md.
log *slog.Logger
}
type engineCmd struct {
fn func() error
err chan error
}
// threadStepper is implemented by backends (currently darwin/arm64) that can
// single-step one specific thread over a disarmed breakpoint while holding
// every other thread, then tear that critical section down. On such backends
// the per-process single-step primitive alone cannot guarantee the breakpoint
// thread (rather than some other thread) is the one that steps, so the engine
// prefers this path. Backends that don't implement it fall back to SingleStep.
type threadStepper interface {
singleStepThread(tid int, addr uint64) error
endThreadStep()
}
// stepThreadOverBP single-steps tid over a just-disarmed breakpoint at addr. On
// darwin it holds all other threads and steps tid specifically; elsewhere it
// falls back to a plain per-process single-step (ptrace stops are per-thread
// there).
func (e *engine) stepThreadOverBP(tid int, addr uint64) error {
if ts, ok := e.backend.(threadStepper); ok {
return ts.singleStepThread(tid, addr)
}
return e.backend.SingleStep(tid)
}
// endThreadStep releases the threads held for an atomic step-over. No-op on
// backends without a threadStepper. Safe to call when no step is in flight.
func (e *engine) endThreadStep() {
if ts, ok := e.backend.(threadStepper); ok {
ts.endThreadStep()
}
}
// activeTID resolves the thread the user is currently stopped on. It prefers
// curTID (set on every user-visible suspend) and falls back to the first task
// thread only before any stop has been recorded. Callers that single-step or
// read registers must use this, not threads[0]: on darwin threads[0] is often
// an idle runtime M, not the goroutine under inspection (see curTID).
func (e *engine) activeTID() (int, error) {
if e.curTID != 0 {
return e.curTID, nil
}
threads, err := e.backend.Threads()
if err != nil || len(threads) == 0 {
return 0, fmt.Errorf("no current thread")
}
return threads[0], nil
}
type stopResult struct {
evt StopEvent
err error
}
func newEngine(b Backend, log *slog.Logger) *engine {
if log == nil {
log = slog.Default()
}
e := &engine{
backend: b,
bps: newBreakpointTable(),
events: make(chan protocol.Event, eventBufSize),
cmdCh: make(chan engineCmd, 8),
stopCh: make(chan stopResult, 1),
done: make(chan struct{}),
state: stateNoProcess,
log: log,
}
go e.loop()
return e
}
func (e *engine) Events() <-chan protocol.Event { return e.events }
func (e *engine) Launch(binaryPath string, args []string, env []string) error {
return e.dispatch(func() error {
if err := e.proc.launch(e.backend, binaryPath, args, env); err != nil {
return err
}
setPID(e.backend, e.proc.pid)
e.loadDWARF(binaryPath)
// startTracedProcess already consumed the initial SIGTRAP. The process
// is stopped — no waitLoop needed.
e.setState(stateSuspended)
e.emitStoppedAtCurrentPC()
return nil
})
}
func (e *engine) Attach(pid int, binaryPath string) error {
return e.dispatch(func() error {
if err := e.proc.attach(e.backend, pid); err != nil {
return err
}
setPID(e.backend, pid)
if binaryPath != "" {
e.loadDWARF(binaryPath)
}
e.setState(stateSuspended)
e.emitStoppedAtCurrentPC()
return nil
})
}
// Kill terminates the tracee. Safe to call multiple times.
func (e *engine) Kill() error {
select {
case <-e.done:
return nil
default:
}
return e.dispatch(func() error {
if e.getState() == stateExited {
return nil
}
// A running tracee has an in-flight waitLoop blocked in Wait4(-1); on
// linux that waitLoop — not killProcess — must reap the SIGKILL death,
// since two concurrent wait4 callers race and wedge Kill (#111). Capture
// whether we're running before endThreadStep/clearAll touch anything.
running := e.getState() == stateRunning
// Release any threads held for an in-flight atomic step-over first, so
// a detach (attached-process Kill) never leaves them Mach-suspended.
e.endThreadStep()
e.bps.clearAll(e.backend)
if killErr := e.proc.kill(e.backend, running); killErr != nil {
return killErr
}
e.setState(stateExited)
// Inject a synthetic StopExited so the loop sees stateExited and exits.
select {
case e.stopCh <- stopResult{evt: StopEvent{Reason: StopExited}}:
default:
}
return nil
})
}
func (e *engine) SetBreakpoint(file string, line int) (protocol.Breakpoint, error) {
var bp protocol.Breakpoint
err := e.dispatch(func() error {
if e.dw == nil {
return fmt.Errorf("SetBreakpoint: no DWARF info — was a binary path provided to Launch/Attach?")
}
addr, err := e.dw.PCForFileLine(file, line)
if err != nil {
return err
}
entry, err := e.bps.set(e.backend, file, line, addr)
if err != nil {
return err
}
bp = entry.toProtocol()
return nil
})
return bp, err
}
func (e *engine) ClearBreakpoint(id int) error {
return e.dispatch(func() error {
return e.bps.clear(e.backend, id)
})
}
func (e *engine) Continue() error {
return e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
if e.lastBP != nil {
if err := e.resumeFromBreakpoint(bpResumeContinue, 0); err != nil {
return err
}
e.emitContinued()
return nil
}
if err := e.backend.ContinueProcess(); err != nil {
return err
}
e.setState(stateRunning)
go e.waitLoop()
e.emitContinued()
return nil
})
}
func (e *engine) StepOver() error {
return e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
return e.stepOver()
})
}
func (e *engine) StepInto() error {
return e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
if e.lastBP != nil {
return e.resumeFromBreakpoint(bpResumeStep, 0)
}
tid, err := e.activeTID()
if err != nil {
return fmt.Errorf("StepInto: %w", err)
}
regs, err := e.backend.GetRegisters(tid)
if err != nil {
return fmt.Errorf("StepInto: get registers: %w", err)
}
// Step exactly one instruction on the user thread. On darwin this holds
// every other thread Mach-suspended and hardware-single-steps tid
// specifically: only the stepped thread runs during the step window, so
// the runtime's sysmon can't observe it and inject a preemption, and any
// Mach breakpoint exception seen mid-step is unambiguously this thread's
// (#92); elsewhere it degrades to a plain per-thread single-step.
if err := e.stepThreadOverBP(tid, regs.PC); err != nil {
return err
}
e.setState(stateRunning)
go e.waitLoop()
return nil
})
}
func (e *engine) StepOut() error {
return e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
return e.stepOut()
})
}
// Pause asynchronously interrupts a running tracee. It is the only resume-side
// operation issued while the process is RUNNING rather than suspended: it fires
// the backend's interrupt signal (PauseSignal) at the tracee via
// StopProcess and records manualStopPending so the resulting signal-delivery
// stop is turned into EventPaused instead of being auto-resumed (see
// handleStop's StopSignal branch). The suspend is reported asynchronously, so
// this returns as soon as the interrupt is armed.
func (e *engine) Pause() error {
return e.dispatch(func() error {
if e.getState() != stateRunning {
return ErrNotRunning
}
e.manualStopPending = true
if err := e.backend.StopProcess(); err != nil {
e.manualStopPending = false
return fmt.Errorf("Pause: %w", err)
}
return nil
})
}
func (e *engine) Locals(frameIndex int) ([]protocol.Variable, error) {
var vars []protocol.Variable
err := e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
framePC, frameBase, err := e.frameLocation(frameIndex)
if err != nil {
return fmt.Errorf("Locals: %w", err)
}
vars, err = e.dw.LocalsForFrame(e.backend, framePC, frameBase)
return err
})
return vars, err
}
// Evaluate resolves a single variable NAME in the given frame (local/parameter
// first, then a package global) and returns its bounded typed tree. It is
// non-suspending and non-resuming — like Locals, it only reads a suspended
// tracee. Expression parsing (dotted paths, indexing, arithmetic) is a later PR.
func (e *engine) Evaluate(frameIndex int, name string) (protocol.Variable, error) {
var result protocol.Variable
err := e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
framePC, frameBase, err := e.frameLocation(frameIndex)
if err != nil {
return fmt.Errorf("Evaluate: %w", err)
}
result, err = e.dw.EvaluateName(e.backend, framePC, frameBase, name)
return err
})
return result, err
}
// frameLocation computes the PC and frame-base address for the given stack frame
// of the currently-stopped thread. Callers must already be on the engine loop
// (inside dispatch) and have verified suspension.
//
// It inspects the thread the user is stopped on (curTID via activeTID), not
// threads[0]: on Darwin threads[0] is frequently an idle runtime M, so a
// breakpoint that fires on another thread would otherwise report an unrelated
// frame. See the activeTID/collectFrames invariant.
func (e *engine) frameLocation(frameIndex int) (framePC, frameBase uint64, err error) {
if e.dw == nil {
return 0, 0, fmt.Errorf("no DWARF info")
}
tid, err := e.activeTID()
if err != nil {
return 0, 0, err
}
regs, err := e.backend.GetRegisters(tid)
if err != nil {
return 0, 0, fmt.Errorf("get registers: %w", err)
}
framePCs := e.walkStack(regs)
if frameIndex < 0 || frameIndex >= len(framePCs) {
return 0, 0, fmt.Errorf("frame index %d out of range (have %d frames)",
frameIndex, len(framePCs))
}
// Resolve the CFA (Go's DW_AT_frame_base) for each frame from 0 up to the
// requested one. Locals are DW_OP_fbreg offsets from the CFA, recovered
// from .debug_frame CFI: the Go rule is SP-relative on arm64 (frame pointer
// + framesize, NOT + 16, because x29 points at the saved FP/LR pair at the
// bottom of the frame with locals above it) and frame-pointer-relative on
// amd64. Frames chain by SP_{i+1} = CFA_i — a callee's CFA is its caller's
// SP at the call, and Go passes arguments in registers so the call itself
// leaves SP unmoved (arm64) / only pushes the return address (amd64, which
// the FP-relative rule already accounts for).
const cfaFallbackFromFP = 16
sp, fp := regs.SP, regs.BP
for i := 0; i < frameIndex; i++ {
cfa, ok := e.dw.cfa(framePCs[i], sp, fp)
if !ok {
cfa = fp + cfaFallbackFromFP
}
var buf [8]byte
if err := e.backend.ReadMemory(fp, buf[:]); err != nil {
return 0, 0, fmt.Errorf("read frame pointer: %w", err)
}
sp = cfa
fp = binary.LittleEndian.Uint64(buf[:])
}
frameBase, ok := e.dw.cfa(framePCs[frameIndex], sp, fp)
if !ok {
frameBase = fp + cfaFallbackFromFP
}
return framePCs[frameIndex], frameBase, nil
}
func (e *engine) StackFrames() ([]protocol.Frame, error) {
var frames []protocol.Frame
err := e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
var err error
// Walk the currently-stopped thread. lastBPTID is only valid immediately
// after a breakpoint hit and is cleared once we single-step off it, so it
// goes stale after a step; curTID always tracks the active stop.
frames, err = e.collectFrames(e.curTID)
return err
})
return frames, err
}
func (e *engine) Goroutines() ([]protocol.Goroutine, error) {
var goroutines []protocol.Goroutine
err := e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
var err error
goroutines, err = e.readGoroutines()
return err
})
return goroutines, err
}
// GoroutineSnapshot returns the full concurrency picture on demand: every
// goroutine (with parent linkage for a spawn tree), every OS thread, the
// current goroutine, and the created/exited lifecycle deltas since the previous
// snapshot. Only valid while suspended (the tracee must be stopped for the
// memory reads to be race-free). Like the auto-streamed snapshots it advances
// the lifecycle-delta baseline.
func (e *engine) GoroutineSnapshot() (protocol.GoroutineSnapshotPayload, error) {
var snap protocol.GoroutineSnapshotPayload
err := e.dispatch(func() error {
if err := e.requireSuspended(); err != nil {
return err
}
snap = e.goroutineSnapshot()
return nil
})
return snap, err
}
func (e *engine) loop() {
// Pin to one OS thread. On Darwin the backend issues ptrace/Mach calls
// directly from these dispatch closures, so they must stay on one thread.
// On Linux the backend owns a dedicated tracer thread (see tracerThread)
// and this lock is merely belt-and-braces.
runtime.LockOSThread()
defer func() {
close(e.done)
close(e.events)
// Release the linux tracer thread now that no more ptrace ops can be
// issued (the loop has exited). No-op on backends without one.
if c, ok := e.backend.(interface{ closeTracer() }); ok {
c.closeTracer()
}
}()
for {
select {
case cmd := <-e.cmdCh:
cmd.err <- cmd.fn()
case result := <-e.stopCh:
if result.err != nil {
if errors.Is(result.err, ErrProcessExited) {
e.emitProcessExited(0)
} else {
e.emitError(protocol.CmdNone, result.err)
}
e.drainCmds()
return
}
// Kill may have already moved us to stateExited while a real
// (non-exit) stop was buffered in stopCh — its synthetic StopExited
// is dropped when the channel is full. Do NOT let that stale stop
// reach handleStop: StopBreakpoint/StopSingleStep/StopSignal call
// setState(stateSuspended) unconditionally, which would resurrect
// the engine out of stateExited and wedge the loop (done/events
// never close, hub never sees the exit). Tear down cleanly instead.
if e.getState() == stateExited {
e.drainCmds()
return
}
e.handleStop(result.evt)
if e.getState() == stateExited {
e.drainCmds()
return
}
}
}
}
func (e *engine) waitLoop() {
// Lock to an OS thread: wait4 has per-thread semantics on some platforms
// and we don't want a thread carrying unrelated ptrace state.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
evt, err := e.backend.Wait()
select {
case e.stopCh <- stopResult{evt: evt, err: err}:
case <-e.done:
}
}
//nolint:gocognit,gocyclo // Stop handling is a single serialized debugger state machine.
func (e *engine) handleStop(stop StopEvent) {
switch stop.Reason {
case StopExited:
if e.getState() == stateExited {
return
}
e.setState(stateExited)
e.emitProcessExited(stop.ExitCode)
case StopKilled:
if e.getState() == stateExited {
return
}
e.setState(stateExited)
e.emitProcessExited(-1)
case StopBreakpoint:
e.setState(stateSuspended)
var err error
stop, err = e.populateBreakpointStop(stop)
if err != nil {
e.emitError(protocol.CmdNone, err)
return
}
bp := e.bps.atAddr(stop.PC)
e.log.Debug("StopBreakpoint", "pc", fmt.Sprintf("0x%x", stop.PC),
"found", bp != nil,
"steppingOverBP", e.steppingOverBP != nil)
if bp == nil {
// Spurious SIGTRAP — a BRK we did not install (Go runtime
// internal trap or libc assertion). On ARM64 PC points AT the
// BRK; ContinueProcess with signal=0 leaves PC unchanged and
// re-executes the trap forever. Advance PC past the 4-byte BRK.
e.log.Warn("spurious SIGTRAP — advancing PC past BRK and resuming",
"pc", fmt.Sprintf("0x%x", stop.PC))
if regs, err := e.backend.GetRegisters(stop.TID); err == nil {
regs.PC = stop.PC + uint64(len(archTrapInstruction()))
_ = e.backend.SetRegisters(stop.TID, regs)
}
_ = e.backend.ContinueProcess()
e.setState(stateRunning)
go e.waitLoop()
return
}
e.log.Debug("StopBreakpoint matched", "file", bp.file, "line", bp.line,
"addr", fmt.Sprintf("0x%x", bp.addr))
e.rewindToBreakpoint(stop)
if bp.file == stepOverNextFile {
_ = e.bps.clear(e.backend, bp.id)
e.lastBP = nil
e.emitStepped(stop)
return
}
if bp.file == stepOutReturnFile {
_ = e.bps.clear(e.backend, bp.id)
e.lastBP = nil
e.emitStepped(stop)
return
}
e.lastBP = bp
e.lastBPTID = stop.TID
e.stepOverFile = ""
e.stepOverLine = 0
e.emitBreakpointHit(bp, stop)
case StopSingleStep:
var err error
stop, err = e.populateStopPC(stop, false)
if err != nil {
// Rearm the in-flight step-over BP and release held threads before
// surfacing the error, so the process is left in a clean state.
if sob := e.steppingOverBP; sob != nil {
e.steppingOverBP = nil
_ = e.bps.reinstall(e.backend, sob)
}
e.endThreadStep()
e.setState(stateSuspended)
e.emitError(protocol.CmdNone, err)
return
}
e.log.Debug("StopSingleStep", "pc", fmt.Sprintf("0x%x", stop.PC),
"steppingOverBP", e.steppingOverBP != nil)
if sob := e.steppingOverBP; sob != nil {
e.steppingOverBP = nil
if rerr := e.bps.reinstall(e.backend, sob); rerr != nil {
// Reinstall failed. Suspend instead of resuming — running
// without the trap would let the process loose.
e.endThreadStep()
e.log.Error("breakpoint reinstall failed — suspending to prevent runaway process",
"addr", fmt.Sprintf("0x%x", sob.addr), "err", rerr)
e.setState(stateSuspended)
e.emitError(protocol.CmdNone, fmt.Errorf("reinstall breakpoint 0x%x: %w", sob.addr, rerr))
return
}
// The trap byte is back in place; only now is it safe to release
// the threads we held for the atomic step-over.
e.endThreadStep()
e.log.Debug("breakpoint reinstalled", "addr", fmt.Sprintf("0x%x", sob.addr))
switch e.bpResume {
case bpResumeContinue:
_ = e.backend.ContinueProcess()
e.setState(stateRunning)
go e.waitLoop()
case bpResumeStep:
e.setState(stateSuspended)
e.emitStepped(stop)
case bpResumeSourceStep:
// Use sob.file/sob.line (the BP's known location) rather than
// a DWARF lookup from stop.PC: stop.PC is one instruction past
// the BP and can land on a DWARF entry with line==0.
if e.dw != nil && sob.file != "" && sob.line > 0 {
if nextPC, nextLine, ok := e.dw.NextLinePC(sob.file, sob.line); ok {
e.log.Debug("sourceStepOver: setting "+stepOverNextFile,
"from", fmt.Sprintf("%s:%d", sob.file, sob.line),
"nextPC", fmt.Sprintf("0x%x", nextPC), "nextLine", nextLine)
entry, setErr := e.bps.set(e.backend, stepOverNextFile, 0, nextPC)
if setErr == nil || errors.Is(setErr, errBreakpointExists) {
e.stepOverFile = sob.file
e.stepOverLine = nextLine
if cerr := e.backend.ContinueProcess(); cerr == nil {
e.setState(stateRunning)
go e.waitLoop()
return
} else if entry != nil {
_ = e.bps.clear(e.backend, entry.id)
e.stepOverFile = ""
e.stepOverLine = 0
}
} else {
e.log.Warn("sourceStepOver: set "+stepOverNextFile+" failed",
"addr", fmt.Sprintf("0x%x", nextPC), "err", setErr)
}
} else {
e.log.Warn("sourceStepOver: NextLinePC found no next line",
"file", sob.file, "line", sob.line)
}
}
e.log.Debug("sourceStepOver fallback: emitting Stepped")
e.setState(stateSuspended)
e.emitStepped(stop)
case bpResumeStepOut:
_, setErr := e.bps.set(e.backend, stepOutReturnFile, 0, e.bpRetAddr)
if setErr != nil && !errors.Is(setErr, errBreakpointExists) {
e.emitError(protocol.CmdStepOut, fmt.Errorf("StepOut: set return breakpoint: %w", setErr))
return
}
_ = e.backend.ContinueProcess()
e.setState(stateRunning)
go e.waitLoop()
}
return
}
e.endThreadStep()
e.setState(stateSuspended)
e.emitStepped(stop)
case StopSignal:
// Reinstall any in-flight step-over BP before resuming or suspending.
if sob := e.steppingOverBP; sob != nil {
e.steppingOverBP = nil
if rerr := e.bps.reinstall(e.backend, sob); rerr != nil {
e.endThreadStep()
e.setState(stateSuspended)
e.emitError(protocol.CmdNone, fmt.Errorf("reinstall breakpoint 0x%x after signal: %w", sob.addr, rerr))
return
}
e.endThreadStep()
}
if stop.Signal == e.backend.PauseSignal() {
if e.manualStopPending {
// A Pause request's interrupt signal has arrived. Suspend and
// report EventPaused instead of auto-resuming — this is the one
// signal stop we deliberately turn into a suspending event.
e.manualStopPending = false
var err error
if stop, err = e.populateStopPC(stop, false); err != nil {
e.setState(stateSuspended)
e.emitError(protocol.CmdNone, err)
return
}
e.setState(stateSuspended)
e.emitPaused(stop)
return
}
// The interrupt signal with no pending Pause is a leftover: a Pause
// raced a self-stop (breakpoint/step won and cleared
// manualStopPending), leaving the signal queued. Suppress it
// silently — surfacing it as output or EventPaused would be bogus.
// Continue discards it (ContinueProcess resumes with signal 0).
_ = e.backend.ContinueProcess()
e.setState(stateRunning)
go e.waitLoop()
return
}
e.emitOutput("stderr", fmt.Sprintf("signal %d", stop.Signal))
_ = e.backend.ContinueProcess()
e.setState(stateRunning)
go e.waitLoop()
}
}
func (e *engine) populateStopPC(stop StopEvent, rewind bool) (StopEvent, error) {
if stop.PC != 0 {
return stop, nil
}
if stop.TID == 0 {
threads, err := e.backend.Threads()
if err != nil {
return stop, fmt.Errorf("get stop thread: %w", err)
}
if len(threads) == 0 {
return stop, fmt.Errorf("get stop thread: no threads")
}
stop.TID = threads[0]
}
regs, err := e.backend.GetRegisters(stop.TID)
if err != nil {
return stop, fmt.Errorf("get stop PC for tid %d: %w", stop.TID, err)
}
if rewind {
stop.PC = archRewindPC(regs.PC)
} else {
stop.PC = regs.PC
}
return stop, nil
}
// rewindToBreakpoint writes the breakpoint address back into the tracee's live
// PC register. On amd64 the CPU advances RIP past the INT3 before delivering
// the trap, so after a software-breakpoint stop the register points one byte
// past the patched instruction even though stop.PC has already been rewound
// for table lookup. Every resume path (plain continue after a sentinel step
// breakpoint, or the restore→single-step→reinstall step-over dance) would then
// execute starting one byte into the original instruction, corrupting the
// tracee and letting it run away — which manifests as a hung Continue/StepOver.
// Writing the rewound PC back makes every resume start at the real
// instruction. It is a no-op where the register already matches (e.g. arm64,
// whose BRK leaves PC in place, and Darwin).
func (e *engine) rewindToBreakpoint(stop StopEvent) {
if stop.TID == 0 {
return
}
regs, err := e.backend.GetRegisters(stop.TID)
if err != nil {
e.log.Warn("rewindToBreakpoint: get registers failed",
"tid", stop.TID, "err", err)
return
}
if regs.PC == stop.PC {
return
}
regs.PC = stop.PC
if err := e.backend.SetRegisters(stop.TID, regs); err != nil {
e.log.Warn("rewindToBreakpoint: set registers failed",
"tid", stop.TID, "pc", fmt.Sprintf("0x%x", stop.PC), "err", err)
}
}
func (e *engine) populateBreakpointStop(stop StopEvent) (StopEvent, error) {
if stop.PC != 0 {
return stop, nil
}
if stop.TID != 0 {
return e.populateStopPC(stop, true)
}
threads, err := e.backend.Threads()
if err != nil {
return stop, fmt.Errorf("find breakpoint thread: %w", err)
}
if len(threads) == 0 {
return stop, fmt.Errorf("find breakpoint thread: no threads")
}
trap := archTrapInstruction()
var firstTrap *StopEvent
var fallback *StopEvent
var firstErr error
for _, tid := range threads {
regs, err := e.backend.GetRegisters(tid)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
candidate := StopEvent{
Reason: stop.Reason,
TID: tid,
PC: archRewindPC(regs.PC),
}
if fallback == nil {
cp := candidate
fallback = &cp
}
if !e.instructionAt(candidate.PC, trap) {
continue
}
cp := candidate
if e.bps.atAddr(candidate.PC) != nil {
return cp, nil
}
if firstTrap == nil {
firstTrap = &cp
}
}
if firstTrap != nil {
return *firstTrap, nil
}
if fallback != nil {
return *fallback, nil
}
return stop, fmt.Errorf("find breakpoint thread: read registers: %w", firstErr)
}
func (e *engine) instructionAt(addr uint64, want []byte) bool {
buf := make([]byte, len(want))
if err := e.backend.ReadMemory(addr, buf); err != nil {
return false
}
for i := range want {
if buf[i] != want[i] {
return false
}
}
return true
}
// drainCmds answers queued commands with ErrProcessExited so blocked dispatchers
// unblock immediately.
func (e *engine) drainCmds() {
for {
select {
case cmd := <-e.cmdCh:
cmd.err <- ErrProcessExited
default:
return
}
}
}
func (e *engine) stepOver() error {
if e.lastBP != nil {
return e.resumeFromBreakpoint(bpResumeSourceStep, 0)
}
return e.sourceStepOver()
}
// sourceStepOver sets a temp BP at the next source line and resumes. Falls
// back to a single machine-instruction step when DWARF can't resolve a target.
//
//nolint:gocognit // Source stepping fallback logic stays together to preserve state transitions.
func (e *engine) sourceStepOver() error {
if e.dw != nil {
// Prefer the remembered destination from the previous step-over over
// re-querying locationForPC, which can land on a DWARF boundary.
file := e.stepOverFile
line := e.stepOverLine
e.stepOverFile = ""
e.stepOverLine = 0
if file == "" || line == 0 {
if tid, err := e.activeTID(); err == nil {
if regs, err := e.backend.GetRegisters(tid); err == nil {
loc := e.dw.locationForPC(regs.PC)
file = loc.File
line = loc.Line
}
}
}
if file != "" && line > 0 {
if nextPC, nextLine, ok := e.dw.NextLinePC(file, line); ok {
entry, setErr := e.bps.set(e.backend, stepOverNextFile, 0, nextPC)
if setErr == nil || errors.Is(setErr, errBreakpointExists) {
e.stepOverFile = file
e.stepOverLine = nextLine
if cerr := e.backend.ContinueProcess(); cerr != nil {
if entry != nil {
_ = e.bps.clear(e.backend, entry.id)
}
e.stepOverFile = ""
e.stepOverLine = 0
return cerr
}
e.setState(stateRunning)
go e.waitLoop()
return nil
}
}
}
}
tid, err := e.activeTID()
if err != nil {
return fmt.Errorf("StepOver: %w", err)
}
regs, err := e.backend.GetRegisters(tid)
if err != nil {
return fmt.Errorf("StepOver: get registers: %w", err)
}
// No DWARF next-line target (e.g. stopped outside known source): fall back
// to a single machine-instruction step of the user thread via the atomic
// path, same rationale as StepInto (#92).
if err := e.stepThreadOverBP(tid, regs.PC); err != nil {
return err
}
e.setState(stateRunning)
go e.waitLoop()
return nil
}
func (e *engine) stepOut() error {
tid, err := e.activeTID()
if err != nil {
return fmt.Errorf("StepOut: %w", err)
}
regs, err := e.backend.GetRegisters(tid)
if err != nil {
return fmt.Errorf("StepOut: get registers: %w", err)
}
// The return address lives at BP+8 — just above the caller's saved frame
// pointer at BP — the same frame-pointer chain walkStack follows. Reading
// *(SP) only yields the return address at a function's first instruction,
// before the prologue moves SP below the pushed return address; StepOut is
// normally invoked at a mid-function breakpoint, where *(SP) is a local slot.