176 lines
5.2 KiB
Go
176 lines
5.2 KiB
Go
// 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"
|
|
)
|
|
|
|
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, nil)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConditionEvalMem covers the register-memory form: the value is read
|
|
// through the supplied reader, and a missing or failing reader must not
|
|
// block the breakpoint.
|
|
func TestConditionEvalMem(t *testing.T) {
|
|
regs := &Regs{RAX: 7}
|
|
mem := func(addr uint64) (uint64, bool) {
|
|
if addr == 0x5000 {
|
|
return 7, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
eq := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000}
|
|
if !eq.Eval(regs, mem) {
|
|
t.Error("register-memory comparison with matching word should hold")
|
|
}
|
|
ne := Condition{Reg: "rax", Op: "!=", MemAddr: 0x5000}
|
|
if ne.Eval(regs, mem) {
|
|
t.Error("register-memory comparison with mismatching word should not hold")
|
|
}
|
|
bad := Condition{Reg: "rax", Op: "==", MemAddr: 0x6000}
|
|
if !bad.Eval(regs, mem) {
|
|
t.Error("unreadable memory must not block the breakpoint")
|
|
}
|
|
noReader := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000}
|
|
if !noReader.Eval(regs, nil) {
|
|
t.Error("missing memory reader must not block the breakpoint")
|
|
}
|
|
}
|
|
|
|
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 TestWatchpointSlotTracking(t *testing.T) {
|
|
s := &Session{} // per-session slots start free
|
|
|
|
// All four slots are free initially.
|
|
for i := range 4 {
|
|
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).
|
|
s.wpSlots[0] = true
|
|
s.wpSlots[2] = true
|
|
|
|
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 := range 4 {
|
|
s.wpSlots[i] = true
|
|
}
|
|
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
|
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
|
}
|
|
}
|
|
|
|
// TestUnwatchSlotBound checks the bound the REPL parses against: it must
|
|
// cover the architecture's whole slot range, not a hardcoded 0-3.
|
|
func TestUnwatchSlotBound(t *testing.T) {
|
|
max := maxWatchpoints()
|
|
if max < 4 {
|
|
t.Fatalf("maxWatchpoints() = %d, want at least 4", max)
|
|
}
|
|
s := &Session{}
|
|
if s.IsWatchpointSlotUsed(max - 1) {
|
|
t.Errorf("slot %d should be free initially", max-1)
|
|
}
|
|
if s.IsWatchpointSlotUsed(max) {
|
|
t.Errorf("slot %d must be out of range", max)
|
|
}
|
|
}
|