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

Assisted-by: DeepSeek V4 Pro
This commit is contained in:
2026-08-03 19:42:05 +02:00
parent f41a86b660
commit 2ab6b9eb84
10 changed files with 573 additions and 82 deletions
+16 -17
View File
@@ -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
View File
@@ -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)
}
}
-32
View File
@@ -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
+90
View File
@@ -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 }