ci: add Gitea CI workflows and release pipeline
Test / vet (push) Successful in 44s
Release / build (amd64, linux) (push) Successful in 39s
Release / build (arm64, linux) (push) Successful in 38s
Release / build (loong64, linux) (push) Successful in 39s
Release / build (riscv64, linux) (push) Successful in 39s
Test / test (push) Successful in 1m58s
Release / release (push) Successful in 18s
Test / build (push) Successful in 37s
Test / vet (push) Successful in 44s
Release / build (amd64, linux) (push) Successful in 39s
Release / build (arm64, linux) (push) Successful in 38s
Release / build (loong64, linux) (push) Successful in 39s
Release / build (riscv64, linux) (push) Successful in 39s
Test / test (push) Successful in 1m58s
Release / release (push) Successful in 18s
Test / build (push) Successful in 37s
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
+16
-17
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
@@ -85,14 +83,15 @@ func (c *Condition) Eval(regs *Regs) bool {
|
||||
}
|
||||
|
||||
// Breakpoints manages the set of breakpoints for a Session.
|
||||
// Breakpoints manages software breakpoints for a debuggee.
|
||||
type Breakpoints struct {
|
||||
s *Session
|
||||
t tracer
|
||||
bps map[uint64]*Breakpoint
|
||||
}
|
||||
|
||||
// NewBreakpoints creates a breakpoint manager for the session.
|
||||
func NewBreakpoints(s *Session) *Breakpoints {
|
||||
return &Breakpoints{s: s, bps: make(map[uint64]*Breakpoint)}
|
||||
// NewBreakpoints creates a new breakpoint manager.
|
||||
func NewBreakpoints(t tracer) *Breakpoints {
|
||||
return &Breakpoints{t: t, bps: make(map[uint64]*Breakpoint)}
|
||||
}
|
||||
|
||||
// Set installs a breakpoint at addr (replaces any existing one).
|
||||
@@ -108,14 +107,14 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
|
||||
return bp, nil
|
||||
}
|
||||
// Read the original byte.
|
||||
word, err := bm.s.Peek(addr)
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orig := byte(word)
|
||||
// Patch with INT3 (0xCC), preserving the rest of the word.
|
||||
patched := (word &^ 0xFF) | 0xCC
|
||||
if err := bm.s.Poke(addr, patched); err != nil {
|
||||
if err := bm.t.Poke(addr, patched); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true, Cond: cond}
|
||||
@@ -160,12 +159,12 @@ func (bm *Breakpoints) Clear(addr uint64) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("debug: no breakpoint at %#x", addr)
|
||||
}
|
||||
word, err := bm.s.Peek(addr)
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
if err := bm.s.Poke(addr, restored); err != nil {
|
||||
if err := bm.t.Poke(addr, restored); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(bm.bps, addr)
|
||||
@@ -211,24 +210,24 @@ func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
||||
// Condition not met — restore the byte but do NOT rewind RIP.
|
||||
// The process continues from the next instruction (past the INT3).
|
||||
word, err := bm.s.Peek(trapAddr)
|
||||
word, err := bm.t.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.s.Poke(trapAddr, restored)
|
||||
bm.t.Poke(trapAddr, restored)
|
||||
}
|
||||
// RIP is already past the INT3 (trapAddr + 1). Don't rewind.
|
||||
return nil
|
||||
}
|
||||
bp.hits++
|
||||
// Restore the original byte.
|
||||
word, err := bm.s.Peek(trapAddr)
|
||||
word, err := bm.t.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.s.Poke(trapAddr, restored)
|
||||
bm.t.Poke(trapAddr, restored)
|
||||
}
|
||||
// Rewind RIP to re-execute the original instruction.
|
||||
regs.RIP = trapAddr
|
||||
bm.s.SetRegs(regs)
|
||||
bm.t.SetRegs(regs)
|
||||
return bp
|
||||
}
|
||||
|
||||
@@ -240,10 +239,10 @@ func (bm *Breakpoints) Reinsert(addr uint64) error {
|
||||
if !ok || !bp.Enabled {
|
||||
return nil
|
||||
}
|
||||
word, err := bm.s.Peek(addr)
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patched := (word &^ 0xFF) | 0xCC
|
||||
return bm.s.Poke(addr, patched)
|
||||
return bm.t.Poke(addr, patched)
|
||||
}
|
||||
|
||||
+110
-3
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConditionEval(t *testing.T) {
|
||||
regs := &Regs{
|
||||
@@ -164,3 +165,109 @@ func TestNearestLabel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsSetAndClear(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
// Set a breakpoint at address 0x1000.
|
||||
bp, err := bm.Set(0x1000, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
if !bp.Enabled {
|
||||
t.Error("breakpoint not enabled")
|
||||
}
|
||||
if bp.Label != "test" {
|
||||
t.Errorf("label = %q, want test", bp.Label)
|
||||
}
|
||||
|
||||
// Verify Peek was called.
|
||||
if len(tr.peeks) != 1 || tr.peeks[0] != 0x1000 {
|
||||
t.Errorf("peeks = %v, want [0x1000]", tr.peeks)
|
||||
}
|
||||
|
||||
// Verify Poke wrote INT3.
|
||||
if len(tr.pokes) != 1 || tr.pokes[0].addr != 0x1000 {
|
||||
t.Errorf("pokes = %v", tr.pokes)
|
||||
}
|
||||
|
||||
// At should find it.
|
||||
if bm.At(0x1000) == nil {
|
||||
t.Error("At(0x1000) returned nil")
|
||||
}
|
||||
|
||||
// All should return it.
|
||||
all := bm.All()
|
||||
if len(all) != 1 {
|
||||
t.Errorf("All() = %d breakpoints, want 1", len(all))
|
||||
}
|
||||
|
||||
// Clear it.
|
||||
if err := bm.Clear(0x1000); err != nil {
|
||||
t.Fatalf("Clear: %v", err)
|
||||
}
|
||||
if bm.At(0x1000) != nil {
|
||||
t.Error("At(0x1000) after Clear should be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsSetWithCond(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
cond := &Condition{Reg: "rax", Op: "==", Value: 42}
|
||||
bp, err := bm.SetWithCond(0x2000, "cond_test", cond)
|
||||
if err != nil {
|
||||
t.Fatalf("SetWithCond: %v", err)
|
||||
}
|
||||
if bp.Cond == nil || bp.Cond.Value != 42 {
|
||||
t.Error("condition not set")
|
||||
}
|
||||
|
||||
// Re-setting the same address should update the condition.
|
||||
cond2 := &Condition{Reg: "rbx", Op: "<", Value: 100}
|
||||
bp2, err := bm.SetWithCond(0x2000, "cond_test2", cond2)
|
||||
if err != nil {
|
||||
t.Fatalf("SetWithCond (update): %v", err)
|
||||
}
|
||||
if bp2.Cond.Value != 100 {
|
||||
t.Error("condition not updated")
|
||||
}
|
||||
// Should have only 1 Peek (first Set), second is update (no Peek needed).
|
||||
if len(tr.peeks) != 1 {
|
||||
t.Errorf("expected 1 Peek, got %d", len(tr.peeks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointsClearAll(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
|
||||
bm.Set(0x1000, "a")
|
||||
bm.Set(0x2000, "b")
|
||||
bm.Set(0x3000, "c")
|
||||
|
||||
if len(bm.All()) != 3 {
|
||||
t.Fatalf("expected 3 breakpoints, got %d", len(bm.All()))
|
||||
}
|
||||
|
||||
bm.ClearAll()
|
||||
if len(bm.All()) != 0 {
|
||||
t.Errorf("ClearAll: expected 0 breakpoints, got %d", len(bm.All()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakpointInfo(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
bm.Set(0x4000, "info_test")
|
||||
|
||||
info := bm.Info()
|
||||
if info == "" {
|
||||
t.Error("Info returned empty string")
|
||||
}
|
||||
if !strings.Contains(info, "info_test") {
|
||||
t.Errorf("Info %q does not contain label", info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,38 +19,6 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the Linux amd64 user_regs_struct layout).
|
||||
type Regs struct {
|
||||
R15 uint64
|
||||
R14 uint64
|
||||
R13 uint64
|
||||
R12 uint64
|
||||
RBP uint64
|
||||
RBX uint64
|
||||
R11 uint64
|
||||
R10 uint64
|
||||
R9 uint64
|
||||
R8 uint64
|
||||
RAX uint64
|
||||
RCX uint64
|
||||
RDX uint64
|
||||
RSI uint64
|
||||
RDI uint64
|
||||
OrigRAX uint64
|
||||
RIP uint64
|
||||
CS uint64
|
||||
RFLAGS uint64
|
||||
RSP uint64
|
||||
SS uint64
|
||||
FSBase uint64
|
||||
GSBase uint64
|
||||
DS uint64
|
||||
ES uint64
|
||||
FS uint64
|
||||
GS uint64
|
||||
}
|
||||
|
||||
// Session is a ptrace debugging session controlling one debuggee process.
|
||||
type Session struct {
|
||||
pid int
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the Linux amd64 user_regs_struct layout).
|
||||
type Regs struct {
|
||||
R15 uint64
|
||||
R14 uint64
|
||||
R13 uint64
|
||||
R12 uint64
|
||||
RBP uint64
|
||||
RBX uint64
|
||||
R11 uint64
|
||||
R10 uint64
|
||||
R9 uint64
|
||||
R8 uint64
|
||||
RAX uint64
|
||||
RCX uint64
|
||||
RDX uint64
|
||||
RSI uint64
|
||||
RDI uint64
|
||||
OrigRAX uint64
|
||||
RIP uint64
|
||||
CS uint64
|
||||
RFLAGS uint64
|
||||
RSP uint64
|
||||
SS uint64
|
||||
FSBase uint64
|
||||
GSBase uint64
|
||||
DS uint64
|
||||
ES uint64
|
||||
FS uint64
|
||||
GS uint64
|
||||
}
|
||||
|
||||
// tracer abstracts the minimal ptrace operations needed by the breakpoint
|
||||
// manager and the stop-information helpers. The live implementation is
|
||||
// *Session (ptrace_linux_amd64.go); tests supply a mock.
|
||||
type tracer interface {
|
||||
Peek(addr uint64) (uint64, error)
|
||||
Poke(addr uint64, val uint64) error
|
||||
SetRegs(regs *Regs) error
|
||||
Pid() int
|
||||
}
|
||||
|
||||
// mockTracer records Peek/Poke calls and provides fake register state.
|
||||
type mockTracer struct {
|
||||
mem map[uint64]byte
|
||||
peeks []uint64
|
||||
pokes []struct {
|
||||
addr uint64
|
||||
val uint64
|
||||
}
|
||||
regs *Regs
|
||||
}
|
||||
|
||||
func newMockTracer() *mockTracer {
|
||||
return &mockTracer{
|
||||
mem: make(map[uint64]byte),
|
||||
regs: &Regs{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockTracer) Peek(addr uint64) (uint64, error) {
|
||||
m.peeks = append(m.peeks, addr)
|
||||
var val uint64
|
||||
for i := uint64(0); i < 8; i++ {
|
||||
val |= uint64(m.mem[addr+i]) << (i * 8)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (m *mockTracer) Poke(addr uint64, val uint64) error {
|
||||
m.pokes = append(m.pokes, struct {
|
||||
addr uint64
|
||||
val uint64
|
||||
}{addr, val})
|
||||
for i := uint64(0); i < 8; i++ {
|
||||
m.mem[addr+i] = byte(val >> (i * 8))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTracer) SetRegs(regs *Regs) error {
|
||||
m.regs = regs
|
||||
return nil
|
||||
}
|
||||
func (m *mockTracer) Pid() int { return 42 }
|
||||
Reference in New Issue
Block a user