diff --git a/_fixtures/decllinetest.go b/_fixtures/decllinetest.go index 1455999d99cf7bc8859b7461eefe76865669b07e..60fc110ffaa5158e653be75e74034bec10be0a63 100644 --- a/_fixtures/decllinetest.go +++ b/_fixtures/decllinetest.go @@ -2,14 +2,15 @@ package main import ( "fmt" - "runtime" ) func main() { a := 0 - runtime.Breakpoint() a++ b := 0 - runtime.Breakpoint() - fmt.Println(a, b) + f1(a, b) +} + +func f1(a, b int) { + fmt.Printf("%d %d\n", a, b) } diff --git a/_fixtures/testvariables.go b/_fixtures/testvariables.go index 37ae096f872422b856bc15a856552d85b835a375..c086e981ac63c4ef8e19a9b39ff5b8442fed34e1 100644 --- a/_fixtures/testvariables.go +++ b/_fixtures/testvariables.go @@ -20,8 +20,8 @@ type Nest struct { } func barfoo() { - runtime.Breakpoint() a1 := "bur" + runtime.Breakpoint() fmt.Println(a1) } diff --git a/_scripts/make.go b/_scripts/make.go index 96872f99da642b6c3b70fe92e0ca96521bbed7a3..dca12f8ffda83ffa027a98d2957d97f429a2b12a 100644 --- a/_scripts/make.go +++ b/_scripts/make.go @@ -70,15 +70,7 @@ func NewMakeCommands() *cobra.Command { Short: "Tests delve", Long: `Tests delve. -Use the flags -s, -r and -b to specify which tests to run. Specifying nothing is equivalent to: - - go run _scripts/make.go test -s all -b default - go run _scripts/make.go test -s basic -b lldb # if lldb-server is installed and Go < 1.14 - go run _scripts/make.go test -s basic -b rr # if rr is installed - - go run _scripts/make.go test -s basic -m pie # only on linux - go run _scripts/make.go test -s core -m pie # only on linux - go run _scripts/make.go test -s +Use the flags -s, -r and -b to specify which tests to run. Specifying nothing will run all tests relevant for the current environment (see testStandard). `, Run: testCmd, } @@ -310,25 +302,7 @@ func testCmd(cmd *cobra.Command, args []string) { os.Exit(1) } - fmt.Println("Testing default backend") - testCmdIntl("all", "", "default", "normal") - if inpath("lldb-server") && !goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) { - fmt.Println("\nTesting LLDB backend") - testCmdIntl("basic", "", "lldb", "normal") - } - if inpath("rr") { - fmt.Println("\nTesting RR backend") - testCmdIntl("basic", "", "rr", "normal") - } - if runtime.GOOS == "linux" { - fmt.Println("\nTesting PIE buildmode, default backend") - testCmdIntl("basic", "", "default", "pie") - testCmdIntl("core", "", "default", "pie") - } - if runtime.GOOS == "linux" && inpath("rr") { - fmt.Println("\nTesting PIE buildmode, RR backend") - testCmdIntl("basic", "", "rr", "pie") - } + testStandard() return } @@ -347,6 +321,28 @@ func testCmd(cmd *cobra.Command, args []string) { testCmdIntl(TestSet, TestRegex, TestBackend, TestBuildMode) } +func testStandard() { + fmt.Println("Testing default backend") + testCmdIntl("all", "", "default", "normal") + if inpath("lldb-server") && !goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) { + fmt.Println("\nTesting LLDB backend") + testCmdIntl("basic", "", "lldb", "normal") + } + if inpath("rr") { + fmt.Println("\nTesting RR backend") + testCmdIntl("basic", "", "rr", "normal") + } + if runtime.GOOS == "linux" || (runtime.GOOS == "windows" && goversion.VersionAfterOrEqual(runtime.Version(), 1, 15)) { + fmt.Println("\nTesting PIE buildmode, default backend") + testCmdIntl("basic", "", "default", "pie") + testCmdIntl("core", "", "default", "pie") + } + if runtime.GOOS == "linux" && inpath("rr") { + fmt.Println("\nTesting PIE buildmode, RR backend") + testCmdIntl("basic", "", "rr", "pie") + } +} + func testCmdIntl(testSet, testRegex, testBackend, testBuildMode string) { testPackages := testSetToPackages(testSet) if len(testPackages) == 0 { diff --git a/pkg/dwarf/reader/variables.go b/pkg/dwarf/reader/variables.go index 1035a3329a69aa1cd31fc7afd40946d1a5ac836d..13640e65853a8501ef3ff8edf449d024d4dbfffc 100644 --- a/pkg/dwarf/reader/variables.go +++ b/pkg/dwarf/reader/variables.go @@ -11,29 +11,45 @@ type Variable struct { Depth int } +// VariablesFlags specifies some configuration flags for the Variables function. +type VariablesFlags uint8 + +const ( + VariablesOnlyVisible VariablesFlags = 1 << iota + VariablesSkipInlinedSubroutines + VariablesTrustDeclLine +) + // Variables returns a list of variables contained inside 'root'. // If onlyVisible is true only variables visible at pc will be returned. // If skipInlinedSubroutines is true inlined subroutines will be skipped -func Variables(root *godwarf.Tree, pc uint64, line int, onlyVisible, skipInlinedSubroutines bool) []Variable { - return variablesInternal(nil, root, 0, pc, line, onlyVisible, skipInlinedSubroutines) +func Variables(root *godwarf.Tree, pc uint64, line int, flags VariablesFlags) []Variable { + return variablesInternal(nil, root, 0, pc, line, flags) } -func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, line int, onlyVisible, skipInlinedSubroutines bool) []Variable { +func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, line int, flags VariablesFlags) []Variable { switch root.Tag { case dwarf.TagInlinedSubroutine: - if skipInlinedSubroutines { + if flags&VariablesSkipInlinedSubroutines != 0 { return v } fallthrough case dwarf.TagLexDwarfBlock, dwarf.TagSubprogram: - if !onlyVisible || root.ContainsPC(pc) { + if (flags&VariablesOnlyVisible == 0) || root.ContainsPC(pc) { for _, child := range root.Children { - v = variablesInternal(v, child, depth+1, pc, line, onlyVisible, skipInlinedSubroutines) + v = variablesInternal(v, child, depth+1, pc, line, flags) } } return v default: - if declLine, ok := root.Val(dwarf.AttrDeclLine).(int64); !ok || line >= int(declLine) { + o := 0 + if root.Tag != dwarf.TagFormalParameter && (flags&VariablesTrustDeclLine != 0) { + // visibility for variables starts the line after declaration line, + // except for formal parameters, which are visible on the same line they + // are defined. + o = 1 + } + if declLine, ok := root.Val(dwarf.AttrDeclLine).(int64); !ok || line >= int(declLine)+o { return append(v, Variable{root, depth}) } return v diff --git a/pkg/goversion/compat.go b/pkg/goversion/compat.go index 63bf6f0b6587b148936b09a7d6b895af73d745c2..24f2f6029fbd6116a129ffdb07f3753e78361b75 100644 --- a/pkg/goversion/compat.go +++ b/pkg/goversion/compat.go @@ -8,7 +8,7 @@ var ( MinSupportedVersionOfGoMajor = 1 MinSupportedVersionOfGoMinor = 12 MaxSupportedVersionOfGoMajor = 1 - MaxSupportedVersionOfGoMinor = 14 + MaxSupportedVersionOfGoMinor = 15 goTooOldErr = fmt.Errorf("Version of Go is too old for this version of Delve (minimum supported version %d.%d, suppress this error with --check-go-version=false)", MinSupportedVersionOfGoMajor, MinSupportedVersionOfGoMinor) dlvTooOldErr = fmt.Errorf("Version of Delve is too old for this version of Go (maximum supported version %d.%d, suppress this error with --check-go-version=false)", MaxSupportedVersionOfGoMajor, MaxSupportedVersionOfGoMinor) ) diff --git a/pkg/proc/arm64_disasm.go b/pkg/proc/arm64_disasm.go index a3c9f4264be85486b62b9ed99fa4fec5d57823f3..8a137a1ba0b533208fdd8508c92c2159d9419ecd 100644 --- a/pkg/proc/arm64_disasm.go +++ b/pkg/proc/arm64_disasm.go @@ -28,6 +28,8 @@ func arm64AsmDecode(asmInst *AsmInstruction, mem []byte, regs Registers, memrw M asmInst.Kind = RetInstruction case arm64asm.B, arm64asm.BR: asmInst.Kind = JmpInstruction + case arm64asm.BRK: + asmInst.Kind = HardBreakInstruction } asmInst.DestLoc = resolveCallArgARM64(&inst, asmInst.Loc.PC, asmInst.AtPC, regs, memrw, bi) diff --git a/pkg/proc/core/windows_amd64_minidump.go b/pkg/proc/core/windows_amd64_minidump.go index 4a67ef4428290292e9c7bd8f74f5d1dc91b8525f..31879e49a110fd9c0eeb2800686873ff9678214b 100644 --- a/pkg/proc/core/windows_amd64_minidump.go +++ b/pkg/proc/core/windows_amd64_minidump.go @@ -28,10 +28,16 @@ func readAMD64Minidump(minidumpPath, exePath string) (*process, error) { memory.Add(m, uintptr(m.Addr), uintptr(len(m.Data))) } + entryPoint := uint64(0) + if len(mdmp.Modules) > 0 { + entryPoint = mdmp.Modules[0].BaseOfImage + } + p := &process{ mem: memory, Threads: map[int]*thread{}, bi: proc.NewBinaryInfo("windows", "amd64"), + entryPoint: entryPoint, breakpoints: proc.NewBreakpointMap(), pid: int(mdmp.Pid), } diff --git a/pkg/proc/disasm.go b/pkg/proc/disasm.go index cd7c4900a9be6db1f70ea29914f1eb333d7c83ec..06f4b1ae94abec28a4d9d01547e2e6edf867fc39 100644 --- a/pkg/proc/disasm.go +++ b/pkg/proc/disasm.go @@ -23,6 +23,7 @@ const ( CallInstruction RetInstruction JmpInstruction + HardBreakInstruction ) // IsCall is true if instr is a call instruction. @@ -40,6 +41,11 @@ func (instr *AsmInstruction) IsJmp() bool { return instr.Kind == JmpInstruction } +// IsHardBreak is true if instr is a hardcoded breakpoint instruction. +func (instr *AsmInstruction) IsHardBreak() bool { + return instr.Kind == HardBreakInstruction +} + type archInst interface { Text(flavour AssemblyFlavour, pc uint64, symLookup func(uint64) (string, uint64)) string OpcodeEquals(op uint64) bool diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index 62886188bd6bd56cb32ff72b5f46f1cac031636e..53826f484af3d7309b382b129ab47d5235819a2a 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -215,7 +215,12 @@ func (scope *EvalScope) Locals() ([]*Variable, error) { return nil, err } - varEntries := reader.Variables(dwarfTree, scope.PC, scope.Line, true, false) + variablesFlags := reader.VariablesOnlyVisible + if scope.BinInfo.Producer() != "" && goversion.ProducerAfterOrEqual(scope.BinInfo.Producer(), 1, 15) { + variablesFlags |= reader.VariablesTrustDeclLine + } + + varEntries := reader.Variables(dwarfTree, scope.PC, scope.Line, variablesFlags) vars := make([]*Variable, 0, len(varEntries)) depths := make([]int, 0, len(varEntries)) for _, entry := range varEntries { @@ -224,7 +229,7 @@ func (scope *EvalScope) Locals() ([]*Variable, error) { // skip variables that we can't parse yet continue } - if trustArgOrder && val.Unreadable != nil && val.Addr == 0 && entry.Tag == dwarf.TagFormalParameter { + if trustArgOrder && ((val.Unreadable != nil && val.Addr == 0) || val.Flags&VariableFakeAddress != 0) && entry.Tag == dwarf.TagFormalParameter { addr := afterLastArgAddr(vars) if addr == 0 { addr = uintptr(scope.Regs.CFA) @@ -653,7 +658,7 @@ func (scope *EvalScope) evalToplevelTypeCast(t ast.Expr, cfg LoadConfig) (*Varia return v, nil case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr: b, _ := constant.Int64Val(argv.Value) - s := string(b) + s := string(rune(b)) v.Value = constant.MakeString(s) v.Len = int64(len(s)) return v, nil diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index ffeb9d27fc203e753a0f010d591c8cd4641a09fc..374549d1e3a02dd1cb5eb1235de659ebad675101 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -107,6 +107,14 @@ type callContext struct { // continueRequest having cont == false and the return values in ret. continueRequest chan<- continueRequest continueCompleted <-chan *G + + // injectionThread is the thread to use for nested call injections if the + // original injection goroutine isn't running (because we are in Go 1.15) + injectionThread Thread + + // stacks is a slice of known goroutine stacks used to check for + // inappropriate escapes + stacks []stack } type continueRequest struct { @@ -121,6 +129,7 @@ type callInjection struct { // pkg/proc/fncall.go for a description of how this works. continueCompleted chan<- *G continueRequest <-chan continueRequest + startThreadID int } func (callCtx *callContext) doContinue() *G { @@ -178,8 +187,9 @@ func EvalExpressionWithCalls(t *Target, g *G, expr string, retLoadCfg LoadConfig } t.fncallForG[g.ID] = &callInjection{ - continueCompleted, - continueRequest, + continueCompleted: continueCompleted, + continueRequest: continueRequest, + startThreadID: 0, } go scope.EvalExpression(expr, retLoadCfg) @@ -193,7 +203,7 @@ func EvalExpressionWithCalls(t *Target, g *G, expr string, retLoadCfg LoadConfig } func finishEvalExpressionWithCalls(t *Target, g *G, contReq continueRequest, ok bool) error { - fncallLog("stashing return values for %d in thread=%d\n", g.ID, g.Thread.ThreadID()) + fncallLog("stashing return values for %d in thread=%d", g.ID, g.Thread.ThreadID()) var err error if !ok { err = errors.New("internal error EvalExpressionWithCalls didn't return anything") @@ -239,6 +249,23 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { if scope.callCtx == nil { return nil, errFuncCallNotAllowed } + thread := scope.g.Thread + stacklo := scope.g.stack.lo + if thread == nil { + // We are doing a nested function call and using Go 1.15, the original + // injection goroutine was suspended and now we are using a different + // goroutine, evaluation still happend on the original goroutine but we + // need to use a different thread to do the nested call injection. + thread = scope.callCtx.injectionThread + g2, err := GetG(thread) + if err != nil { + return nil, err + } + stacklo = g2.stack.lo + } + if thread == nil { + return nil, errGoroutineNotRunning + } p := scope.callCtx.p bi := scope.BinInfo @@ -252,7 +279,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { } // check that there are at least 256 bytes free on the stack - regs, err := scope.g.Thread.Registers() + regs, err := thread.Registers() if err != nil { return nil, err } @@ -260,7 +287,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { if err != nil { return nil, err } - if regs.SP()-256 <= scope.g.stack.lo { + if regs.SP()-256 <= stacklo { return nil, errNotEnoughStack } _, err = regs.Get(int(x86asm.RAX)) @@ -278,22 +305,37 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { return nil, err } - if err := callOP(bi, scope.g.Thread, regs, dbgcallfn.Entry); err != nil { + if err := callOP(bi, thread, regs, dbgcallfn.Entry); err != nil { return nil, err } // write the desired argument frame size at SP-(2*pointer_size) (the extra pointer is the saved PC) - if err := writePointer(bi, scope.g.Thread, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(fncall.argFrameSize)); err != nil { + if err := writePointer(bi, thread, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(fncall.argFrameSize)); err != nil { return nil, err } - fncallLog("function call initiated %v frame size %d", fncall.fn, fncall.argFrameSize) + fncallLog("function call initiated %v frame size %d goroutine %d (thread %d)", fncall.fn, fncall.argFrameSize, scope.g.ID, thread.ThreadID()) + + p.fncallForG[scope.g.ID].startThreadID = thread.ThreadID() spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(scope.g.stack.hi) bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(scope.g.stack.hi) fboff := scope.Regs.FrameBase - int64(scope.g.stack.hi) for { - scope.g = scope.callCtx.doContinue() + scope.callCtx.injectionThread = nil + g := scope.callCtx.doContinue() + // Go 1.15 will move call injection execution to a different goroutine, + // but we want to keep evaluation on the original goroutine. + if g.ID == scope.g.ID { + scope.g = g + } else { + // We are in Go 1.15 and we switched to a new goroutine, the original + // goroutine is now parked and therefore does not have a thread + // associated. + scope.g.Thread = nil + scope.g.Status = Gwaiting + scope.callCtx.injectionThread = g.Thread + } // adjust the value of registers inside scope pcreg, bpreg, spreg := scope.Regs.Reg(scope.Regs.PCRegNum), scope.Regs.Reg(scope.Regs.BPRegNum), scope.Regs.Reg(scope.Regs.SPRegNum) @@ -306,7 +348,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { scope.Regs.FrameBase = fboff + int64(scope.g.stack.hi) scope.Regs.CFA = scope.frameOffset + int64(scope.g.stack.hi) - finished := funcCallStep(scope, &fncall) + finished := funcCallStep(scope, &fncall, g.Thread) if finished { break } @@ -500,9 +542,14 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg *Variable, formalArg *funcCallArg, argFrameAddr uint64) error { if scope.callCtx.checkEscape { //TODO(aarzilli): only apply the escapeCheck to leaking parameters. - if err := escapeCheck(actualArg, formalArg.name, scope.g); err != nil { + if err := escapeCheck(actualArg, formalArg.name, scope.g.stack); err != nil { return fmt.Errorf("cannot use %s as argument %s in function %s: %v", actualArg.Name, formalArg.name, fncall.fn.Name, err) } + for _, stack := range scope.callCtx.stacks { + if err := escapeCheck(actualArg, formalArg.name, stack); err != nil { + return fmt.Errorf("cannot use %s as argument %s in function %s: %v", actualArg.Name, formalArg.name, fncall.fn.Name, err) + } + } } //TODO(aarzilli): autmoatic wrapping in interfaces for cases not handled @@ -524,7 +571,7 @@ func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize i return 0, nil, fmt.Errorf("DWARF read error: %v", err) } - varEntries := reader.Variables(dwarfTree, fn.Entry, int(^uint(0)>>1), false, true) + varEntries := reader.Variables(dwarfTree, fn.Entry, int(^uint(0)>>1), reader.VariablesSkipInlinedSubroutines) trustArgOrder := bi.Producer() != "" && goversion.ProducerAfterOrEqual(bi.Producer(), 1, 12) @@ -590,7 +637,7 @@ func alignAddr(addr, align int64) int64 { return (addr + int64(align-1)) &^ int64(align-1) } -func escapeCheck(v *Variable, name string, g *G) error { +func escapeCheck(v *Variable, name string, stack stack) error { switch v.Kind { case reflect.Ptr: var w *Variable @@ -600,31 +647,31 @@ func escapeCheck(v *Variable, name string, g *G) error { } else { w = v.maybeDereference() } - return escapeCheckPointer(w.Addr, name, g) + return escapeCheckPointer(w.Addr, name, stack) case reflect.Chan, reflect.String, reflect.Slice: - return escapeCheckPointer(v.Base, name, g) + return escapeCheckPointer(v.Base, name, stack) case reflect.Map: sv := v.clone() sv.RealType = resolveTypedef(&(v.RealType.(*godwarf.MapType).TypedefType)) sv = sv.maybeDereference() - return escapeCheckPointer(sv.Addr, name, g) + return escapeCheckPointer(sv.Addr, name, stack) case reflect.Struct: t := v.RealType.(*godwarf.StructType) for _, field := range t.Field { fv, _ := v.toField(field) - if err := escapeCheck(fv, fmt.Sprintf("%s.%s", name, field.Name), g); err != nil { + if err := escapeCheck(fv, fmt.Sprintf("%s.%s", name, field.Name), stack); err != nil { return err } } case reflect.Array: for i := int64(0); i < v.Len; i++ { sv, _ := v.sliceAccess(int(i)) - if err := escapeCheck(sv, fmt.Sprintf("%s[%d]", name, i), g); err != nil { + if err := escapeCheck(sv, fmt.Sprintf("%s[%d]", name, i), stack); err != nil { return err } } case reflect.Func: - if err := escapeCheckPointer(uintptr(v.funcvalAddr()), name, g); err != nil { + if err := escapeCheckPointer(uintptr(v.funcvalAddr()), name, stack); err != nil { return err } } @@ -632,8 +679,8 @@ func escapeCheck(v *Variable, name string, g *G) error { return nil } -func escapeCheckPointer(addr uintptr, name string, g *G) error { - if uint64(addr) >= g.stack.lo && uint64(addr) < g.stack.hi { +func escapeCheckPointer(addr uintptr, name string, stack stack) error { + if uint64(addr) >= stack.lo && uint64(addr) < stack.hi { return fmt.Errorf("stack object passed to escaping pointer: %s", name) } return nil @@ -648,21 +695,15 @@ const ( ) // funcCallStep executes one step of the function call injection protocol. -func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool { +func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread) bool { p := callScope.callCtx.p bi := p.BinInfo() - thread := callScope.g.Thread regs, err := thread.Registers() if err != nil { fncall.err = err return true } - regs, err = regs.Copy() - if err != nil { - fncall.err = err - return true - } rax, _ := regs.Get(int(x86asm.RAX)) @@ -676,7 +717,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool { fnname = loc.Fn.Name } } - fncallLog("function call interrupt gid=%d thread=%d rax=%#x (PC=%#x in %s)", callScope.g.ID, thread.ThreadID(), rax, pc, fnname) + fncallLog("function call interrupt gid=%d (original) thread=%d rax=%#x (PC=%#x in %s)", callScope.g.ID, thread.ThreadID(), rax, pc, fnname) } switch rax { @@ -691,6 +732,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool { fncall.err = fmt.Errorf("%v", constant.StringVal(errvar.Value)) case debugCallAXCompleteCall: + p.fncallForG[callScope.g.ID].startThreadID = 0 // evaluate arguments of the target function, copy them into its argument frame and call the function if fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0 { // if we couldn't figure out which function we are calling before @@ -719,13 +761,15 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool { // address of the function pointer itself. thread.SetDX(fncall.closureAddr) } + cfa := regs.SP() + oldpc := regs.PC() callOP(bi, thread, regs, fncall.fn.Entry) - err := funcCallEvalArgs(callScope, fncall, regs.SP()) + err := funcCallEvalArgs(callScope, fncall, cfa) if err != nil { // rolling back the call, note: this works because we called regs.Copy() above - thread.SetSP(regs.SP()) - thread.SetPC(regs.PC()) + thread.SetSP(cfa) + thread.SetPC(oldpc) fncall.err = err fncall.lateCallFailure = true break @@ -777,6 +821,13 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool { v.Flags |= VariableFakeAddress } + // Store the stack span of the currently running goroutine (which in Go >= + // 1.15 might be different from the original injection goroutine) so that + // later on we can use it to perform the escapeCheck + if threadg, _ := GetG(thread); threadg != nil { + callScope.callCtx.stacks = append(callScope.callCtx.stacks, threadg.stack) + } + case debugCallAXReadPanic: // read panic value from stack fncall.panicvar, err = readTopstackVariable(thread, regs, "interface {}", callScope.callCtx.retLoadCfg) @@ -886,11 +937,18 @@ func allocString(scope *EvalScope, v *Variable) error { return err } -func isCallInjectionStop(loc *Location) bool { +func isCallInjectionStop(t *Target, thread Thread, loc *Location) bool { if loc.Fn == nil { return false } - return strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) || strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2) + if !strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) && !strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2) { + return false + } + text, err := disassembleCurrentInstruction(t, thread, -1) + if err != nil || len(text) <= 0 { + return false + } + return text[0].IsHardBreak() } // callInjectionProtocol is the function called from Continue to progress @@ -906,19 +964,16 @@ func callInjectionProtocol(t *Target, threads []Thread) (done bool, err error) { if err != nil { continue } - if !isCallInjectionStop(loc) { + if !isCallInjectionStop(t, thread, loc) { continue } - g, err := GetG(thread) + g, callinj, err := findCallInjectionStateForThread(t, thread) if err != nil { - return done, fmt.Errorf("could not determine running goroutine for thread %#x currently executing the function call injection protocol: %v", thread.ThreadID(), err) + return false, err } - callinj := t.fncallForG[g.ID] - if callinj == nil || callinj.continueCompleted == nil { - return false, fmt.Errorf("could not recover call injection state for goroutine %d", g.ID) - } - fncallLog("step for injection on goroutine %d thread=%d (location %s)", g.ID, thread.ThreadID(), loc.Fn.Name) + + fncallLog("step for injection on goroutine %d (current) thread=%d (location %s)", g.ID, thread.ThreadID(), loc.Fn.Name) callinj.continueCompleted <- g contReq, ok := <-callinj.continueRequest if !contReq.cont { @@ -931,3 +986,36 @@ func callInjectionProtocol(t *Target, threads []Thread) (done bool, err error) { } return done, nil } + +func findCallInjectionStateForThread(t *Target, thread Thread) (*G, *callInjection, error) { + g, err := GetG(thread) + if err != nil { + return nil, nil, fmt.Errorf("could not determine running goroutine for thread %#x currently executing the function call injection protocol: %v", thread.ThreadID(), err) + } + fncallLog("findCallInjectionStateForThread thread=%d goroutine=%d", thread.ThreadID(), g.ID) + notfound := func() error { + return fmt.Errorf("could not recover call injection state for goroutine %d (thread %d)", g.ID, thread.ThreadID()) + } + callinj := t.fncallForG[g.ID] + if callinj != nil { + if callinj.continueCompleted == nil { + return nil, nil, notfound() + } + return g, callinj, nil + } + + // In Go 1.15 and later the call injection protocol will switch to a + // different goroutine. + // Here we try to recover the injection goroutine by checking the injection + // thread. + + for goid, callinj := range t.fncallForG { + if callinj != nil && callinj.continueCompleted != nil && callinj.startThreadID != 0 && callinj.startThreadID == thread.ThreadID() { + t.fncallForG[g.ID] = callinj + fncallLog("goroutine %d is the goroutine executing the call injection started in goroutine %d", g.ID, goid) + return g, callinj, nil + } + } + + return nil, nil, notfound() +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 82d0dcf063f3ebd430bbc3756fa8155f35c295e5..c69c52cbe818d1b56ee725bc8d3cb599b2623fa0 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -15,6 +15,7 @@ import ( "path/filepath" "reflect" "runtime" + "sort" "strconv" "strings" "testing" @@ -3507,6 +3508,30 @@ func TestIssue1008(t *testing.T) { }) } +func testDeclLineCount(t *testing.T, p *proc.Target, lineno int, tgtvars []string) { + sort.Strings(tgtvars) + + assertLineNumber(p, t, lineno, "Program did not continue to correct next location") + scope, err := proc.GoroutineScope(p.CurrentThread()) + assertNoError(err, t, fmt.Sprintf("GoroutineScope (:%d)", lineno)) + vars, err := scope.Locals() + assertNoError(err, t, fmt.Sprintf("Locals (:%d)", lineno)) + if len(vars) != len(tgtvars) { + t.Fatalf("wrong number of variables %d (:%d)", len(vars), lineno) + } + outvars := make([]string, len(vars)) + for i, v := range vars { + outvars[i] = v.Name + } + sort.Strings(outvars) + + for i := range outvars { + if tgtvars[i] != outvars[i] { + t.Fatalf("wrong variables, got: %q expected %q\n", outvars, tgtvars) + } + } +} + func TestDeclLine(t *testing.T) { ver, _ := goversion.Parse(runtime.Version()) if ver.Major > 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 10, Rev: -1}) { @@ -3514,24 +3539,34 @@ func TestDeclLine(t *testing.T) { } withTestProcess("decllinetest", t, func(p *proc.Target, fixture protest.Fixture) { + setFileBreakpoint(p, t, fixture.Source, 8) + setFileBreakpoint(p, t, fixture.Source, 9) + setFileBreakpoint(p, t, fixture.Source, 10) + setFileBreakpoint(p, t, fixture.Source, 11) + setFileBreakpoint(p, t, fixture.Source, 14) + assertNoError(p.Continue(), t, "Continue") - scope, err := proc.GoroutineScope(p.CurrentThread()) - assertNoError(err, t, "GoroutineScope (1)") - vars, err := scope.LocalVariables(normalLoadConfig) - assertNoError(err, t, "LocalVariables (1)") - if len(vars) != 1 { - t.Fatalf("wrong number of variables %d", len(vars)) + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) { + testDeclLineCount(t, p, 8, []string{}) + } else { + testDeclLineCount(t, p, 8, []string{"a"}) } assertNoError(p.Continue(), t, "Continue") - scope, err = proc.GoroutineScope(p.CurrentThread()) - assertNoError(err, t, "GoroutineScope (2)") - scope.LocalVariables(normalLoadConfig) - vars, err = scope.LocalVariables(normalLoadConfig) - assertNoError(err, t, "LocalVariables (2)") - if len(vars) != 2 { - t.Fatalf("wrong number of variables %d", len(vars)) + testDeclLineCount(t, p, 9, []string{"a"}) + + assertNoError(p.Continue(), t, "Continue") + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) { + testDeclLineCount(t, p, 10, []string{"a"}) + } else { + testDeclLineCount(t, p, 10, []string{"a", "b"}) } + + assertNoError(p.Continue(), t, "Continue") + testDeclLineCount(t, p, 11, []string{"a", "b"}) + + assertNoError(p.Continue(), t, "Continue") + testDeclLineCount(t, p, 14, []string{"a", "b"}) }) } diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go index a4b2b746b4f5dc5641e88e14812692652e631b9d..4b0fb82e8981bd2c8f1463a55480c766c34f2b1d 100644 --- a/pkg/proc/target_exec.go +++ b/pkg/proc/target_exec.go @@ -157,7 +157,7 @@ func (dbp *Target) Continue() error { return err } if dbp.GetDirection() == Forward { - text, err := disassembleCurrentInstruction(dbp, curthread) + text, err := disassembleCurrentInstruction(dbp, curthread, 0) if err != nil { return err } @@ -248,12 +248,12 @@ func pickCurrentThread(dbp *Target, trapthread Thread, threads []Thread) error { return dbp.SwitchThread(trapthread.ThreadID()) } -func disassembleCurrentInstruction(p Process, thread Thread) ([]AsmInstruction, error) { +func disassembleCurrentInstruction(p Process, thread Thread, off int64) ([]AsmInstruction, error) { regs, err := thread.Registers() if err != nil { return nil, err } - pc := regs.PC() + pc := regs.PC() + uint64(off) return disassemble(thread, regs, p.Breakpoints(), p.BinInfo(), pc, pc+uint64(p.BinInfo().Arch.MaxInstructionLength()), true) } diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go index 394e204a2da85b55debea9f07010a1939088a1fd..277dfecf9c5d351e94a5d3cea771b19b255cd02c 100644 --- a/pkg/proc/test/support.go +++ b/pkg/proc/test/support.go @@ -135,6 +135,8 @@ func BuildFixture(name string, flags BuildFlags) Fixture { } if flags&BuildModePIE != 0 { buildFlags = append(buildFlags, "-buildmode=pie") + } else { + buildFlags = append(buildFlags, "-buildmode=exe") } if flags&BuildModePlugin != 0 { buildFlags = append(buildFlags, "-buildmode=plugin") diff --git a/pkg/proc/x86_disasm.go b/pkg/proc/x86_disasm.go index 4ce861fa8023ed395b4ad81f1015e0ad47947d37..ad962bf8a45c16c3ca9bbae15787c61d4b5d0f75 100644 --- a/pkg/proc/x86_disasm.go +++ b/pkg/proc/x86_disasm.go @@ -30,6 +30,8 @@ func x86AsmDecode(asmInst *AsmInstruction, mem []byte, regs Registers, memrw Mem asmInst.Kind = CallInstruction case x86asm.RET, x86asm.LRET: asmInst.Kind = RetInstruction + case x86asm.INT: + asmInst.Kind = HardBreakInstruction } asmInst.DestLoc = resolveCallArgX86(&inst, asmInst.Loc.PC, asmInst.AtPC, regs, memrw, bi) diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go index fca7ba2f744e404935f9a4df180ad4db669cc7cc..3ffa97a0c20951236d8abfffebaa3c52d9bdf988 100644 --- a/pkg/terminal/command_test.go +++ b/pkg/terminal/command_test.go @@ -687,7 +687,7 @@ func TestListCmd(t *testing.T) { withTestTerminal("testvariables", t, func(term *FakeTerminal) { term.MustExec("continue") term.MustExec("continue") - listIsAt(t, term, "list", 24, 19, 29) + listIsAt(t, term, "list", 25, 20, 30) listIsAt(t, term, "list 69", 69, 64, 70) listIsAt(t, term, "frame 1 list", 62, 57, 67) listIsAt(t, term, "frame 1 list 69", 69, 64, 70) diff --git a/service/test/integration1_test.go b/service/test/integration1_test.go index f4f20e2b73210de00dadfb0dbd7ce29ec488cd4f..08966c529f0f97b121f25c3f9f3fef7e5ab9e323 100644 --- a/service/test/integration1_test.go +++ b/service/test/integration1_test.go @@ -417,7 +417,7 @@ func Test1ClientServer_switchThread(t *testing.T) { func Test1ClientServer_infoLocals(t *testing.T) { withTestClient1("testnextprog", t, func(c *rpc1.RPCClient) { fp := testProgPath(t, "testnextprog") - _, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 23}) + _, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 24}) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index f532cfe791cb522cf5818613c8de89198c49afc2..ebbf94a75dc78c896b86bb63bef64c0afb96b7e2 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -548,7 +548,7 @@ func TestClientServer_infoLocals(t *testing.T) { protest.AllowRecording(t) withTestClient2("testnextprog", t, func(c service.Client) { fp := testProgPath(t, "testnextprog") - _, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 23}) + _, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 24}) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1771,6 +1771,9 @@ func TestClientServerFunctionCallStacktrace(t *testing.T) { if runtime.GOARCH == "arm64" { t.Skip("arm64 does not support FunctionCall for now") } + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) { + t.Skip("Go 1.15 executes function calls in a different goroutine so the stack trace will not contain main.main or runtime.main") + } protest.MustSupportFunctionCalls(t, testBackend) withTestClient2("fncall", t, func(c service.Client) { mustHaveDebugCalls(t, c)