proc: when stepping set condition on thread ID if there is no curg (#3475)

If there is no current goroutine when 'next', 'step' or 'stepout' are
used set a condition that the thread ID should stay the same instead.
This makes stepping work for multithreaded C programs or Go programs
that have threads started by cgo code.

Fixes #3262
This commit is contained in:
Alessandro Arzilli 2023-08-21 21:30:56 +02:00 committed by GitHub
parent f0b534ddcc
commit 6a0423a1e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 68 additions and 39 deletions

@ -30,11 +30,17 @@ func Int(n int64) *ast.BasicLit {
} }
// And returns an expression evaluating 'x && y'. // And returns an expression evaluating 'x && y'.
func And(x, y ast.Expr) *ast.BinaryExpr { func And(x, y ast.Expr) ast.Expr {
if x == nil || y == nil {
return nil
}
return &ast.BinaryExpr{Op: token.LAND, X: x, Y: y} return &ast.BinaryExpr{Op: token.LAND, X: x, Y: y}
} }
// Or returns an expression evaluating 'x || y'. // Or returns an expression evaluating 'x || y'.
func Or(x, y ast.Expr) *ast.BinaryExpr { func Or(x, y ast.Expr) ast.Expr {
if x == nil || y == nil {
return nil
}
return &ast.BinaryExpr{Op: token.LOR, X: x, Y: y} return &ast.BinaryExpr{Op: token.LOR, X: x, Y: y}
} }

@ -299,7 +299,7 @@ func TestCore(t *testing.T) {
if mainFrame == nil { if mainFrame == nil {
t.Fatalf("Couldn't find main in stack %v", panickingStack) t.Fatalf("Couldn't find main in stack %v", panickingStack)
} }
msg, err := proc.FrameToScope(p, p.Memory(), nil, *mainFrame).EvalExpression("msg", proc.LoadConfig{MaxStringLen: 64}) msg, err := proc.FrameToScope(p, p.Memory(), nil, p.CurrentThread().ThreadID(), *mainFrame).EvalExpression("msg", proc.LoadConfig{MaxStringLen: 64})
if err != nil { if err != nil {
t.Fatalf("Couldn't EvalVariable(msg, ...): %v", err) t.Fatalf("Couldn't EvalVariable(msg, ...): %v", err)
} }
@ -434,7 +434,7 @@ mainSearch:
t.Fatal("could not find main.main frame") t.Fatal("could not find main.main frame")
} }
scope := proc.FrameToScope(p, p.Memory(), nil, *mainFrame) scope := proc.FrameToScope(p, p.Memory(), nil, p.CurrentThread().ThreadID(), *mainFrame)
loadConfig := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1} loadConfig := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
v1, err := scope.EvalExpression("t", loadConfig) v1, err := scope.EvalExpression("t", loadConfig)
assertNoError(err, t, "EvalVariable(t)") assertNoError(err, t, "EvalVariable(t)")

@ -34,6 +34,7 @@ type EvalScope struct {
Regs op.DwarfRegisters Regs op.DwarfRegisters
Mem MemoryReadWriter // Target's memory Mem MemoryReadWriter // Target's memory
g *G g *G
threadID int
BinInfo *BinaryInfo BinInfo *BinaryInfo
target *Target target *Target
loadCfg *LoadConfig loadCfg *LoadConfig
@ -78,6 +79,7 @@ func ConvertEvalScope(dbp *Target, gid int64, frame, deferCall int) (*EvalScope,
return nil, err return nil, err
} }
ct := dbp.CurrentThread() ct := dbp.CurrentThread()
threadID := ct.ThreadID()
g, err := FindGoroutine(dbp, gid) g, err := FindGoroutine(dbp, gid)
if err != nil { if err != nil {
return nil, err return nil, err
@ -90,6 +92,9 @@ func ConvertEvalScope(dbp *Target, gid int64, frame, deferCall int) (*EvalScope,
var locs []Stackframe var locs []Stackframe
if g != nil { if g != nil {
if g.Thread != nil {
threadID = g.Thread.ThreadID()
}
locs, err = GoroutineStacktrace(dbp, g, frame+1, opts) locs, err = GoroutineStacktrace(dbp, g, frame+1, opts)
} else { } else {
locs, err = ThreadStacktrace(dbp, ct, frame+1) locs, err = ThreadStacktrace(dbp, ct, frame+1)
@ -115,7 +120,7 @@ func ConvertEvalScope(dbp *Target, gid int64, frame, deferCall int) (*EvalScope,
return d.EvalScope(dbp, ct) return d.EvalScope(dbp, ct)
} }
return FrameToScope(dbp, dbp.Memory(), g, locs[frame:]...), nil return FrameToScope(dbp, dbp.Memory(), g, threadID, locs[frame:]...), nil
} }
// FrameToScope returns a new EvalScope for frames[0]. // FrameToScope returns a new EvalScope for frames[0].
@ -123,7 +128,7 @@ func ConvertEvalScope(dbp *Target, gid int64, frame, deferCall int) (*EvalScope,
// frames[0].Regs.SP() and frames[1].Regs.CFA will be cached. // frames[0].Regs.SP() and frames[1].Regs.CFA will be cached.
// Otherwise all memory between frames[0].Regs.SP() and frames[0].Regs.CFA // Otherwise all memory between frames[0].Regs.SP() and frames[0].Regs.CFA
// will be cached. // will be cached.
func FrameToScope(t *Target, thread MemoryReadWriter, g *G, frames ...Stackframe) *EvalScope { func FrameToScope(t *Target, thread MemoryReadWriter, g *G, threadID int, frames ...Stackframe) *EvalScope {
// Creates a cacheMem that will preload the entire stack frame the first // Creates a cacheMem that will preload the entire stack frame the first
// time any local variable is read. // time any local variable is read.
// Remember that the stack grows downward in memory. // Remember that the stack grows downward in memory.
@ -138,7 +143,7 @@ func FrameToScope(t *Target, thread MemoryReadWriter, g *G, frames ...Stackframe
thread = cacheMemory(thread, minaddr, int(maxaddr-minaddr)) thread = cacheMemory(thread, minaddr, int(maxaddr-minaddr))
} }
s := &EvalScope{Location: frames[0].Call, Regs: frames[0].Regs, Mem: thread, g: g, BinInfo: t.BinInfo(), target: t, frameOffset: frames[0].FrameOffset()} s := &EvalScope{Location: frames[0].Call, Regs: frames[0].Regs, Mem: thread, g: g, BinInfo: t.BinInfo(), target: t, frameOffset: frames[0].FrameOffset(), threadID: threadID}
s.PC = frames[0].lastpc s.PC = frames[0].lastpc
return s return s
} }
@ -152,7 +157,7 @@ func ThreadScope(t *Target, thread Thread) (*EvalScope, error) {
if len(locations) < 1 { if len(locations) < 1 {
return nil, errors.New("could not decode first frame") return nil, errors.New("could not decode first frame")
} }
return FrameToScope(t, thread.ProcessMemory(), nil, locations...), nil return FrameToScope(t, thread.ProcessMemory(), nil, thread.ThreadID(), locations...), nil
} }
// GoroutineScope returns an EvalScope for the goroutine running on the given thread. // GoroutineScope returns an EvalScope for the goroutine running on the given thread.
@ -168,7 +173,11 @@ func GoroutineScope(t *Target, thread Thread) (*EvalScope, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return FrameToScope(t, thread.ProcessMemory(), g, locations...), nil threadID := 0
if g.Thread != nil {
threadID = g.Thread.ThreadID()
}
return FrameToScope(t, thread.ProcessMemory(), g, threadID, locations...), nil
} }
// EvalExpression returns the value of the given expression. // EvalExpression returns the value of the given expression.
@ -634,7 +643,7 @@ func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
if scope.g == nil { if scope.g == nil {
typ, err := scope.BinInfo.findType("runtime.g") typ, err := scope.BinInfo.findType("runtime.g")
if err != nil { if err != nil {
return nil, fmt.Errorf("blah: %v", err) return nil, fmt.Errorf("could not find runtime.g: %v", err)
} }
gvar := newVariable("curg", fakeAddressUnresolv, typ, scope.BinInfo, scope.Mem) gvar := newVariable("curg", fakeAddressUnresolv, typ, scope.BinInfo, scope.Mem)
gvar.loaded = true gvar.loaded = true
@ -646,6 +655,8 @@ func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
return scope.g.variable.clone(), nil return scope.g.variable.clone(), nil
} else if maybePkg.Name == "runtime" && node.Sel.Name == "frameoff" { } else if maybePkg.Name == "runtime" && node.Sel.Name == "frameoff" {
return newConstant(constant.MakeInt64(scope.frameOffset), scope.Mem), nil return newConstant(constant.MakeInt64(scope.frameOffset), scope.Mem), nil
} else if maybePkg.Name == "runtime" && node.Sel.Name == "threadid" {
return newConstant(constant.MakeInt64(int64(scope.threadID)), scope.Mem), nil
} else if v, err := scope.findGlobal(maybePkg.Name, node.Sel.Name); err == nil { } else if v, err := scope.findGlobal(maybePkg.Name, node.Sel.Name); err == nil {
return v, nil return v, nil
} }

@ -1199,7 +1199,7 @@ func evalVariableOrError(p *proc.Target, symbol string) (*proc.Variable, error)
var frame proc.Stackframe var frame proc.Stackframe
frame, err = findFirstNonRuntimeFrame(p) frame, err = findFirstNonRuntimeFrame(p)
if err == nil { if err == nil {
scope = proc.FrameToScope(p, p.Memory(), nil, frame) scope = proc.FrameToScope(p, p.Memory(), nil, 0, frame)
} }
} else { } else {
scope, err = proc.GoroutineScope(p, p.CurrentThread()) scope, err = proc.GoroutineScope(p, p.CurrentThread())
@ -3101,7 +3101,7 @@ func TestIssue871(t *testing.T) {
var frame proc.Stackframe var frame proc.Stackframe
frame, err = findFirstNonRuntimeFrame(p) frame, err = findFirstNonRuntimeFrame(p)
if err == nil { if err == nil {
scope = proc.FrameToScope(p, p.Memory(), nil, frame) scope = proc.FrameToScope(p, p.Memory(), nil, 0, frame)
} }
} else { } else {
scope, err = proc.GoroutineScope(p, p.CurrentThread()) scope, err = proc.GoroutineScope(p, p.CurrentThread())
@ -3514,7 +3514,7 @@ func TestIssue1034(t *testing.T) {
assertNoError(grp.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
frames, err := proc.GoroutineStacktrace(p, p.SelectedGoroutine(), 10, 0) frames, err := proc.GoroutineStacktrace(p, p.SelectedGoroutine(), 10, 0)
assertNoError(err, t, "Stacktrace") assertNoError(err, t, "Stacktrace")
scope := proc.FrameToScope(p, p.Memory(), nil, frames[2:]...) scope := proc.FrameToScope(p, p.Memory(), nil, 0, frames[2:]...)
args, _ := scope.FunctionArguments(normalLoadConfig) args, _ := scope.FunctionArguments(normalLoadConfig)
assertNoError(err, t, "FunctionArguments()") assertNoError(err, t, "FunctionArguments()")
if len(args) > 0 { if len(args) > 0 {
@ -5277,8 +5277,8 @@ func TestDump(t *testing.T) {
t.Errorf("Frame mismatch %d.%d\nlive:\t%s\ncore:\t%s", gos[i].ID, j, convertFrame(p.BinInfo().Arch, &frames[j]), convertFrame(p.BinInfo().Arch, &cframes[j])) t.Errorf("Frame mismatch %d.%d\nlive:\t%s\ncore:\t%s", gos[i].ID, j, convertFrame(p.BinInfo().Arch, &frames[j]), convertFrame(p.BinInfo().Arch, &cframes[j]))
} }
if frames[j].Call.Fn != nil && frames[j].Call.Fn.Name == "main.main" { if frames[j].Call.Fn != nil && frames[j].Call.Fn.Name == "main.main" {
scope = proc.FrameToScope(p, p.Memory(), gos[i], frames[j:]...) scope = proc.FrameToScope(p, p.Memory(), gos[i], 0, frames[j:]...)
cscope = proc.FrameToScope(c, c.Memory(), cgos[i], cframes[j:]...) cscope = proc.FrameToScope(c, c.Memory(), cgos[i], 0, cframes[j:]...)
} }
} }
} }

@ -14,7 +14,7 @@ import (
// readSigtrampgoContext reads runtime.sigtrampgo context at the specified address // readSigtrampgoContext reads runtime.sigtrampgo context at the specified address
func (it *stackIterator) readSigtrampgoContext() (*op.DwarfRegisters, error) { func (it *stackIterator) readSigtrampgoContext() (*op.DwarfRegisters, error) {
logger := logflags.DebuggerLogger() logger := logflags.DebuggerLogger()
scope := FrameToScope(it.target, it.mem, it.g, it.frame) scope := FrameToScope(it.target, it.mem, it.g, 0, it.frame)
bi := it.bi bi := it.bi
findvar := func(name string) *Variable { findvar := func(name string) *Variable {

@ -38,7 +38,7 @@ func (t *Target) setStackWatchBreakpoints(scope *EvalScope, watchpoint *Breakpoi
return err return err
} }
sameGCond := sameGoroutineCondition(scope.g) sameGCond := sameGoroutineCondition(scope.BinInfo, scope.g, 0)
retFrameCond := astutil.And(sameGCond, frameoffCondition(&retframe)) retFrameCond := astutil.And(sameGCond, frameoffCondition(&retframe))
var deferpc uint64 var deferpc uint64

@ -444,10 +444,22 @@ func (grp *TargetGroup) Step() (err error) {
// sameGoroutineCondition returns an expression that evaluates to true when // sameGoroutineCondition returns an expression that evaluates to true when
// the current goroutine is g. // the current goroutine is g.
func sameGoroutineCondition(g *G) ast.Expr { func sameGoroutineCondition(bi *BinaryInfo, g *G, threadID int) ast.Expr {
if g == nil { if g == nil {
if len(bi.Images[0].compileUnits) == 0 {
// It's unclear what the right behavior is here. We are probably
// debugging a process without debug info, this means we can't properly
// create a same goroutine condition (we don't have a description for the
// runtime.g type). If we don't set the condition then 'next' (and step,
// stepout) will work for single-threaded programs (in limited
// circumstances) but fail in presence of any concurrency.
// If we set a thread ID condition even single threaded programs can fail
// due to goroutine migration, but sometimes it will work even with
// concurrency.
return nil return nil
} }
return astutil.Eql(astutil.PkgVar("runtime", "threadid"), astutil.Int(int64(threadID)))
}
return astutil.Eql(astutil.Sel(astutil.PkgVar("runtime", "curg"), "goid"), astutil.Int(int64(g.ID))) return astutil.Eql(astutil.Sel(astutil.PkgVar("runtime", "curg"), "goid"), astutil.Int(int64(g.ID)))
} }
@ -492,7 +504,7 @@ func (grp *TargetGroup) StepOut() error {
return grp.Continue() return grp.Continue()
} }
sameGCond := sameGoroutineCondition(selg) sameGCond := sameGoroutineCondition(dbp.BinInfo(), selg, curthread.ThreadID())
if backward { if backward {
if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil { if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil {
@ -544,7 +556,7 @@ func (grp *TargetGroup) StepInstruction() (err error) {
if g.Thread == nil { if g.Thread == nil {
// Step called on parked goroutine // Step called on parked goroutine
if _, err := dbp.SetBreakpoint(0, g.PC, NextBreakpoint, if _, err := dbp.SetBreakpoint(0, g.PC, NextBreakpoint,
sameGoroutineCondition(dbp.SelectedGoroutine())); err != nil { sameGoroutineCondition(dbp.BinInfo(), dbp.SelectedGoroutine(), thread.ThreadID())); err != nil {
return err return err
} }
return grp.Continue() return grp.Continue()
@ -637,7 +649,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
} }
} }
sameGCond := sameGoroutineCondition(selg) sameGCond := sameGoroutineCondition(dbp.BinInfo(), selg, curthread.ThreadID())
var firstPCAfterPrologue uint64 var firstPCAfterPrologue uint64
@ -667,10 +679,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
return err return err
} }
var sameFrameCond ast.Expr sameFrameCond := astutil.And(sameGCond, frameoffCondition(&topframe))
if sameGCond != nil {
sameFrameCond = astutil.And(sameGCond, frameoffCondition(&topframe))
}
if stepInto && !backward { if stepInto && !backward {
err := setStepIntoBreakpoints(dbp, topframe.Current.Fn, text, topframe, sameGCond) err := setStepIntoBreakpoints(dbp, topframe.Current.Fn, text, topframe, sameGCond)
@ -818,7 +827,7 @@ func stepIntoCallback(curthread Thread, p *Target) (bool, error) {
// here we either set a breakpoint into the destination of the CALL // here we either set a breakpoint into the destination of the CALL
// instruction or we determined that the called function is hidden, // instruction or we determined that the called function is hidden,
// either way we need to resume execution // either way we need to resume execution
if err = setStepIntoBreakpoint(p, fn, text, sameGoroutineCondition(g)); err != nil { if err = setStepIntoBreakpoint(p, fn, text, sameGoroutineCondition(p.BinInfo(), g, curthread.ThreadID())); err != nil {
return false, err return false, err
} }
@ -1241,10 +1250,13 @@ type onNextGoroutineWalker struct {
} }
func (w *onNextGoroutineWalker) Visit(n ast.Node) ast.Visitor { func (w *onNextGoroutineWalker) Visit(n ast.Node) ast.Visitor {
if binx, isbin := n.(*ast.BinaryExpr); isbin && binx.Op == token.EQL && exprToString(binx.X) == "runtime.curg.goid" { if binx, isbin := n.(*ast.BinaryExpr); isbin && binx.Op == token.EQL {
x := exprToString(binx.X)
if x == "runtime.curg.goid" || x == "runtime.threadid" {
w.ret, w.err = evalBreakpointCondition(w.tgt, w.thread, n.(ast.Expr)) w.ret, w.err = evalBreakpointCondition(w.tgt, w.thread, n.(ast.Expr))
return nil return nil
} }
}
return w return w
} }

@ -82,7 +82,7 @@ func evalScope(p *proc.Target) (*proc.EvalScope, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return proc.FrameToScope(p, p.Memory(), nil, frame), nil return proc.FrameToScope(p, p.Memory(), nil, p.CurrentThread().ThreadID(), frame), nil
} }
func evalVariableWithCfg(p *proc.Target, symbol string, cfg proc.LoadConfig) (*proc.Variable, error) { func evalVariableWithCfg(p *proc.Target, symbol string, cfg proc.LoadConfig) (*proc.Variable, error) {
@ -417,7 +417,7 @@ func TestLocalVariables(t *testing.T) {
var frame proc.Stackframe var frame proc.Stackframe
frame, err = findFirstNonRuntimeFrame(p) frame, err = findFirstNonRuntimeFrame(p)
if err == nil { if err == nil {
scope = proc.FrameToScope(p, p.Memory(), nil, frame) scope = proc.FrameToScope(p, p.Memory(), nil, p.CurrentThread().ThreadID(), frame)
} }
} else { } else {
scope, err = proc.GoroutineScope(p, p.CurrentThread()) scope, err = proc.GoroutineScope(p, p.CurrentThread())

@ -1863,7 +1863,7 @@ func (d *Debugger) convertStacktrace(rawlocs []proc.Stackframe, cfg *proc.LoadCo
} }
if cfg != nil && rawlocs[i].Current.Fn != nil { if cfg != nil && rawlocs[i].Current.Fn != nil {
var err error var err error
scope := proc.FrameToScope(d.target.Selected, d.target.Selected.Memory(), nil, rawlocs[i:]...) scope := proc.FrameToScope(d.target.Selected, d.target.Selected.Memory(), nil, 0, rawlocs[i:]...)
locals, err := scope.LocalVariables(*cfg) locals, err := scope.LocalVariables(*cfg)
if err != nil { if err != nil {
return nil, err return nil, err