2026-08-01 14:33:00 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
2026-08-20 23:57:10 +02:00
|
|
|
//go:build linux && amd64
|
|
|
|
|
|
2026-08-01 14:33:00 +02:00
|
|
|
package debug
|
|
|
|
|
|
2026-08-03 19:42:05 +02:00
|
|
|
import (
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
2026-08-01 14:33:00 +02:00
|
|
|
|
|
|
|
|
func TestConditionEval(t *testing.T) {
|
|
|
|
|
regs := &Regs{
|
|
|
|
|
RAX: 42,
|
|
|
|
|
RBX: 0,
|
|
|
|
|
RCX: 100,
|
|
|
|
|
RIP: 0x1000,
|
|
|
|
|
RSP: 0x2000,
|
|
|
|
|
R8: 8,
|
|
|
|
|
R15: 15,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
cond Condition
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{Condition{Reg: "rax", Op: "==", Value: 42}, true},
|
|
|
|
|
{Condition{Reg: "rax", Op: "==", Value: 43}, false},
|
|
|
|
|
{Condition{Reg: "rax", Op: "!=", Value: 43}, true},
|
|
|
|
|
{Condition{Reg: "rax", Op: "!=", Value: 42}, false},
|
|
|
|
|
{Condition{Reg: "rax", Op: "<", Value: 50}, true},
|
|
|
|
|
{Condition{Reg: "rax", Op: "<", Value: 40}, false},
|
|
|
|
|
{Condition{Reg: "rax", Op: ">", Value: 40}, true},
|
|
|
|
|
{Condition{Reg: "rax", Op: ">", Value: 50}, false},
|
|
|
|
|
{Condition{Reg: "rax", Op: "<=", Value: 42}, true},
|
|
|
|
|
{Condition{Reg: "rax", Op: ">=", Value: 42}, true},
|
|
|
|
|
{Condition{Reg: "rbx", Op: "==", Value: 0}, true},
|
|
|
|
|
{Condition{Reg: "rcx", Op: ">", Value: 50}, true},
|
|
|
|
|
{Condition{Reg: "rip", Op: "==", Value: 0x1000}, true},
|
|
|
|
|
{Condition{Reg: "rsp", Op: ">", Value: 0x1000}, true},
|
|
|
|
|
{Condition{Reg: "r8", Op: "==", Value: 8}, true},
|
|
|
|
|
{Condition{Reg: "r15", Op: "==", Value: 15}, true},
|
|
|
|
|
{Condition{Reg: "eax", Op: "==", Value: 42}, true}, // 32-bit alias
|
|
|
|
|
{Condition{Reg: "ax", Op: "==", Value: 42}, true}, // 16-bit alias
|
|
|
|
|
{Condition{Reg: "unknown", Op: "==", Value: 0}, true}, // unknown reg → don't block
|
|
|
|
|
{Condition{Reg: "rax", Op: "??", Value: 0}, true}, // unknown op → don't block
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
got := tt.cond.Eval(regs)
|
|
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("Condition{%q %q %d}.Eval() = %v, want %v",
|
|
|
|
|
tt.cond.Reg, tt.cond.Op, tt.cond.Value, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLineAt(t *testing.T) {
|
|
|
|
|
lines := []SourceLine{
|
|
|
|
|
{Offset: 0, Line: 5},
|
|
|
|
|
{Offset: 5, Line: 6},
|
|
|
|
|
{Offset: 10, Line: 7},
|
|
|
|
|
{Offset: 15, Line: 8},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
offset int
|
|
|
|
|
want int
|
|
|
|
|
}{
|
|
|
|
|
{0, 5},
|
|
|
|
|
{1, 5},
|
|
|
|
|
{4, 5},
|
|
|
|
|
{5, 6},
|
|
|
|
|
{7, 6},
|
|
|
|
|
{10, 7},
|
|
|
|
|
{12, 7},
|
|
|
|
|
{15, 8},
|
|
|
|
|
{20, 8},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
got := lineAt(lines, tt.offset)
|
|
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("lineAt(lines, %d) = %d, want %d", tt.offset, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Empty table.
|
|
|
|
|
if lineAt(nil, 5) != 0 {
|
|
|
|
|
t.Error("lineAt(nil, 5) should return 0")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOffsetForLine(t *testing.T) {
|
|
|
|
|
lines := []SourceLine{
|
|
|
|
|
{Offset: 0, Line: 5},
|
|
|
|
|
{Offset: 5, Line: 6},
|
|
|
|
|
{Offset: 10, Line: 7},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
line int
|
|
|
|
|
want int
|
|
|
|
|
}{
|
|
|
|
|
{5, 0},
|
|
|
|
|
{6, 5},
|
|
|
|
|
{7, 10},
|
|
|
|
|
{99, -1}, // not found
|
|
|
|
|
{0, -1}, // not found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
got := offsetForLine(lines, tt.line)
|
|
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("offsetForLine(lines, %d) = %d, want %d", tt.line, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDecodeRflags(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
flags uint64
|
|
|
|
|
want string
|
|
|
|
|
}{
|
|
|
|
|
{0x202, "IF"}, // only IF set (bit 9)
|
|
|
|
|
{0x246, "PF ZF IF"}, // PF(2) + ZF(6) + IF(9)
|
|
|
|
|
{0x001, "CF"}, // carry flag
|
|
|
|
|
{0x080, "SF"}, // sign flag
|
|
|
|
|
{0x800, "OF"}, // overflow flag
|
|
|
|
|
{0x000, "none"}, // no flags
|
|
|
|
|
{0x202 | 0x001, "CF IF"}, // CF + IF
|
|
|
|
|
{0x3F7, "CF PF AF ZF SF TF IF"}, // all arithmetic flags
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
got := decodeRflags(tt.flags)
|
|
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("decodeRflags(%#x) = %q, want %q", tt.flags, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNearestLabel(t *testing.T) {
|
|
|
|
|
labels := []Label{
|
|
|
|
|
{Name: "start", Offset: 0},
|
|
|
|
|
{Name: "loop", Offset: 10},
|
|
|
|
|
{Name: "done", Offset: 20},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
offset int
|
|
|
|
|
want string
|
|
|
|
|
}{
|
|
|
|
|
{0, "start"},
|
|
|
|
|
{5, "start"},
|
|
|
|
|
{10, "loop"},
|
|
|
|
|
{15, "loop"},
|
|
|
|
|
{20, "done"},
|
|
|
|
|
{25, "done"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
got := nearestLabel(labels, tt.offset)
|
|
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("nearestLabel(labels, %d) = %q, want %q", tt.offset, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-03 19:42:05 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-07 22:27:49 +02:00
|
|
|
|
|
|
|
|
func TestWatchpointSlotTracking(t *testing.T) {
|
2026-08-20 23:57:10 +02:00
|
|
|
wpSlots = [4]bool{} // reset
|
2026-08-07 22:27:49 +02:00
|
|
|
s := &Session{}
|
|
|
|
|
|
|
|
|
|
// All four slots are free initially.
|
|
|
|
|
for i := 0; i < 4; i++ {
|
|
|
|
|
if s.IsWatchpointSlotUsed(i) {
|
|
|
|
|
t.Errorf("slot %d should be free initially", i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if got := s.FindFreeWatchpointSlot(); got != 0 {
|
|
|
|
|
t.Errorf("FindFreeWatchpointSlot() = %d, want 0", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint).
|
2026-08-20 23:57:10 +02:00
|
|
|
wpSlots[0] = true
|
|
|
|
|
wpSlots[2] = true
|
2026-08-07 22:27:49 +02:00
|
|
|
|
|
|
|
|
if !s.IsWatchpointSlotUsed(0) {
|
|
|
|
|
t.Error("slot 0 should be in use")
|
|
|
|
|
}
|
|
|
|
|
if s.IsWatchpointSlotUsed(1) {
|
|
|
|
|
t.Error("slot 1 should be free")
|
|
|
|
|
}
|
|
|
|
|
if !s.IsWatchpointSlotUsed(2) {
|
|
|
|
|
t.Error("slot 2 should be in use")
|
|
|
|
|
}
|
|
|
|
|
if s.IsWatchpointSlotUsed(3) {
|
|
|
|
|
t.Error("slot 3 should be free")
|
|
|
|
|
}
|
|
|
|
|
if got := s.FindFreeWatchpointSlot(); got != 1 {
|
|
|
|
|
t.Errorf("FindFreeWatchpointSlot() = %d, want 1", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Out-of-range slot queries return false.
|
|
|
|
|
if s.IsWatchpointSlotUsed(-1) {
|
|
|
|
|
t.Error("slot -1 should be reported as free (out of range)")
|
|
|
|
|
}
|
|
|
|
|
if s.IsWatchpointSlotUsed(4) {
|
|
|
|
|
t.Error("slot 4 should be reported as free (out of range)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
|
|
|
|
for i := 0; i < 4; i++ {
|
2026-08-20 23:57:10 +02:00
|
|
|
wpSlots[i] = true
|
2026-08-07 22:27:49 +02:00
|
|
|
}
|
|
|
|
|
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
|
|
|
|
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
|
|
|
|
}
|
|
|
|
|
}
|