Repository: go-delve/delve
Pull request #2335 (2021-02-02 23:51:08)

service/dap: avoid double removal of temp built binary
Because `signalDisconnect` can be called (see function comments), sometimes we try to remove the built temp binary twice, resulting in errors like: `could not remove /Users/polina/delve/service/dap/__debug_bin: remove /Users/polina/delve/service/dap/__debug_bin: no such file or directory`. Fix this and add a test.

aarzilli: We investigated the access is denied on delete on windows problem a while ago and couldn't figure out why it happened. I'm not sure if in this case it's the same thing or if it's a new problem. It seems too repeatable.
polinasok: What do you suggest?
aarzilli: Make sure Close is getting called before os.Remove and, if it is, disable the test on windows and open an issue.
polinasok: > Make sure Close is getting called before os.Remove and, if it is, disable the test on windows and open an issue.

So I went down the rabbit hole of googling on this one. Found https://github.com/golang/go/issues/32088, related [SO post](https://stackoverflow.com/questions/63559737/golang-os-close-function-works-but-os-remove-function-does-not), etc. I think I have the right general background now.

That said, I am not sure what to make out of your comment in the context of this change. 

This change fixes the issue that arises out of trying to remove a file that was already removed by the shutdown sequence that has a couple of different entrance points and executes some parts of the code more than once. There is a comment explaining when that happens in the code. And I already put a similar safeguard for closing a chan in that code. This problem is not Windows specific. I observed it on my Mac.

If you are referring to the need to have a call to `Close` before the call to `gobuild.Remove` (which in turn calls `os.Remove`), then I am not sure how to accomplish that. I followed the pattern used in the rpc server - calling `gobuild.GoBuild` to generate the binary and then `gobuild.Remove` to remove it. Does `exec.Command("go",...)` + `Run()` leave the file open and in need of `Close()`? How do we do that without the open file descriptor? Where is this handled in the rpc server? Shouldn't this be handled generically somewhere in `gobuild` then?

Which test do you recommend that I disable on Windows?

aarzilli: I meant BinaryInfo's Close not os.File's Close. Basically, just check that debugger.Debugger.Detach is called.

> Which test do you recommend that I disable on Windows?

The one added by this PR, that is failing.
polinasok: > I meant BinaryInfo's Close not os.File's Close. Basically, just check that debugger.Debugger.Detach is called.

Detach is always called, but it never makes it to bi.Close() because the target already terminated by the time it is called.
I set a breakpoint in Close() and it is never called by this test by whatever code does ContinueOnce() and then detects that the process has exited. Should this be fixed? Should the internal readers be explicitly closed in that case as well?

> > Which test do you recommend that I disable on Windows?
> 
> The one added by this PR, that is failing.

I finally figured out how to navigate TeamCity results.
polinasok: I wanted to debug this a bit more, but I don't have a Windows machine and the default backend is different on MacOS. Is it possible to enable TeamCity on my fork like I used to do with Travis?
aarzilli: > Is it possible to enable TeamCity on my fork like I used to do with Travis?

I think that's complicated. When I need to do some tests I use the custom build. Click on the "..." next to the "Run" button, select "run as personal build" and "Upload patch". But maybe I have that because I am an admin and you don't. In that case maybe the easiest thing is to give you some permissions on teamcity. cc @nd
nd: @polinasok I need an email to send you invite.
aarzilli: @nd since there is no problem I've sent the invite myself.
polinasok: > @nd since there is no problem I've sent the invite myself.

It all worked. Thank you. I am able to run personal builds.

Patch:
diff --git a/.teamcity/settings.kts b/.teamcity/settings.kts
index 8ea5cf63..8e0e0b63 100644
--- a/.teamcity/settings.kts
+++ b/.teamcity/settings.kts
@@ -4,6 +4,7 @@ import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.commitStatusPu
 import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.golang
 import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.pullRequests
 import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.dockerCommand
+import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.exec
 import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.powerShell
 import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.BuildFailureOnMetric
 import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.failOnMetricChange
@@ -41,8 +42,13 @@ val targets = arrayOf(
 
         "linux/386/1.16",
 
+        "linux/arm64/1.16",
+        "linux/arm64/tip",
+
         "windows/amd64/1.16",
-        "windows/amd64/tip"
+        "windows/amd64/tip",
+
+        "mac/amd64/1.16",
 )
 
 project {
@@ -101,6 +107,10 @@ class AggregatorBuild(tests: Collection<BuildType>) : BuildType({
             param("github_oauth_user", "")
         }
     }
+
+    failureConditions {
+        executionTimeoutMin = 60
+    }
 })
 
 class OSProject(os: String, tests: List<TestBuild>) : Project({
@@ -149,7 +159,13 @@ class TestBuild(val os: String, val arch: String, version: String, buildId: Abso
     steps {
         when (os) {
             "linux" -> {
-                val dockerArch = if (arch == "386") "i$arch" else arch
+                val dockerArch = when (arch) {
+                    "386" -> "i386"
+                    "arm64" -> "arm64v8"
+                    else -> {
+                        arch
+                    }
+                }
                 dockerCommand {
                     name = "Pull Ubuntu"
                     commandType = other {
@@ -181,10 +197,21 @@ class TestBuild(val os: String, val arch: String, version: String, buildId: Abso
                     param("jetbrains_powershell_scriptArguments", "-version ${"go$version"} -arch $arch")
                 }
             }
+            "mac" -> {
+                exec {
+                    name = "Test"
+                    path = "_scripts/test_mac.sh"
+                    arguments = "${"go$version"} $arch %system.teamcity.build.tempDir%"
+                }
+            }
         }
     }
 
     requirements {
+        when (arch) {
+            "386", "amd64" -> equals("teamcity.agent.jvm.os.arch", if (os == "mac") "x86_64" else "amd64")
+            "arm64" -> equals("teamcity.agent.jvm.os.arch", "aarch64")
+        }
         when (os) {
             "linux" -> {
                 matches("teamcity.agent.jvm.os.family", "Linux")
@@ -192,6 +219,9 @@ class TestBuild(val os: String, val arch: String, version: String, buildId: Abso
             "windows" -> {
                 matches("teamcity.agent.jvm.os.family", "Windows")
             }
+            "mac" -> {
+                matches("teamcity.agent.jvm.os.family", "Mac OS")
+            }
         }
     }
 
diff --git a/pkg/gobuild/gobuild.go b/pkg/gobuild/gobuild.go
index b441b436..2465ca54 100644
--- a/pkg/gobuild/gobuild.go
+++ b/pkg/gobuild/gobuild.go
@@ -20,7 +20,7 @@ func Remove(path string) {
 	// Open files can be removed on Unix, but not on Windows, where there also appears
 	// to be a delay in releasing the binary when the process exits. So we try again.
 	if err != nil && runtime.GOOS == "windows" {
-		time.Sleep(10 * time.Microsecond)
+		time.Sleep(100 * time.Microsecond)
 		err = os.Remove(path)
 	}
 	if err != nil {
diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go
index 594b12a1..d31cc4eb 100644
--- a/pkg/proc/bininfo.go
+++ b/pkg/proc/bininfo.go
@@ -1227,19 +1227,9 @@ func (bi *BinaryInfo) setGStructOffsetElf(image *Image, exe *elf.File, wg *sync.
 	// - Otherwise, Go asks the external linker to place the G pointer by
 	//   emitting runtime.tlsg, a TLS symbol, which is relocated to the chosen
 	//   offset in libc's TLS block.
-	symbols, err := exe.Symbols()
-	if err != nil {
-		image.setLoadError("could not parse ELF symbols: %v", err)
-		return
-	}
-	var tlsg *elf.Symbol
-	for _, symbol := range symbols {
-		if symbol.Name == "runtime.tlsg" {
-			s := symbol
-			tlsg = &s
-			break
-		}
-	}
+	// - On ARM64 (but really, any architecture other than i386 and 86x64) the
+	//   offset is calculate using runtime.tls_g and the formula is different.
+
 	var tls *elf.Prog
 	for _, prog := range exe.Progs {
 		if prog.Type == elf.PT_TLS {
@@ -1247,20 +1237,54 @@ func (bi *BinaryInfo) setGStructOffsetElf(image *Image, exe *elf.File, wg *sync.
 			break
 		}
 	}
-	if tlsg == nil || tls == nil {
-		bi.gStructOffset = ^uint64(bi.Arch.PtrSize()) + 1 //-ptrSize
-		return
+
+	switch exe.Machine {
+	case elf.EM_X86_64, elf.EM_386:
+		tlsg := getSymbol(image, exe, "runtime.tlsg")
+		if tlsg == nil || tls == nil {
+			bi.gStructOffset = ^uint64(bi.Arch.PtrSize()) + 1 //-ptrSize
+			return
+		}
+
+		// According to https://reviews.llvm.org/D61824, linkers must pad the actual
+		// size of the TLS segment to ensure that (tlsoffset%align) == (vaddr%align).
+		// This formula, copied from the lld code, matches that.
+		// https://github.com/llvm-mirror/lld/blob/9aef969544981d76bea8e4d1961d3a6980980ef9/ELF/InputSection.cpp#L643
+		memsz := tls.Memsz + (-tls.Vaddr-tls.Memsz)&(tls.Align-1)
+
+		// The TLS register points to the end of the TLS block, which is
+		// tls.Memsz long. runtime.tlsg is an offset from the beginning of that block.
+		bi.gStructOffset = ^(memsz) + 1 + tlsg.Value // -tls.Memsz + tlsg.Value
+
+	case elf.EM_AARCH64:
+		tlsg := getSymbol(image, exe, "runtime.tls_g")
+		if tlsg == nil || tls == nil {
+			bi.gStructOffset = 2 * uint64(bi.Arch.PtrSize())
+			return
+		}
+
+		bi.gStructOffset = tlsg.Value + uint64(bi.Arch.PtrSize()*2) + ((tls.Vaddr - uint64(bi.Arch.PtrSize()*2)) & (tls.Align - 1))
+
+	default:
+		// we should never get here
+		panic("architecture not supported")
 	}
+}
 
-	// According to https://reviews.llvm.org/D61824, linkers must pad the actual
-	// size of the TLS segment to ensure that (tlsoffset%align) == (vaddr%align).
-	// This formula, copied from the lld code, matches that.
-	// https://github.com/llvm-mirror/lld/blob/9aef969544981d76bea8e4d1961d3a6980980ef9/ELF/InputSection.cpp#L643
-	memsz := tls.Memsz + (-tls.Vaddr-tls.Memsz)&(tls.Align-1)
+func getSymbol(image *Image, exe *elf.File, name string) *elf.Symbol {
+	symbols, err := exe.Symbols()
+	if err != nil {
+		image.setLoadError("could not parse ELF symbols: %v", err)
+		return nil
+	}
 
-	// The TLS register points to the end of the TLS block, which is
-	// tls.Memsz long. runtime.tlsg is an offset from the beginning of that block.
-	bi.gStructOffset = ^(memsz) + 1 + tlsg.Value // -tls.Memsz + tlsg.Value
+	for _, symbol := range symbols {
+		if symbol.Name == name {
+			s := symbol
+			return &s
+		}
+	}
+	return nil
 }
 
 // PE ////////////////////////////////////////////////////////////////
diff --git a/pkg/proc/linutil/regs_arm64_arch.go b/pkg/proc/linutil/regs_arm64_arch.go
index a82b0b7d..5141e579 100644
--- a/pkg/proc/linutil/regs_arm64_arch.go
+++ b/pkg/proc/linutil/regs_arm64_arch.go
@@ -9,15 +9,17 @@ import (
 
 // Regs is a wrapper for sys.PtraceRegs.
 type ARM64Registers struct {
-	Regs     *ARM64PtraceRegs //general-purpose registers
-	Fpregs   []proc.Register  //Formatted floating point registers
-	Fpregset []byte           //holding all floating point register values
+	Regs      *ARM64PtraceRegs //general-purpose registers
+	iscgo     bool
+	tpidr_el0 uint64
+	Fpregs    []proc.Register //Formatted floating point registers
+	Fpregset  []byte          //holding all floating point register values
 
 	loadFpRegs func(*ARM64Registers) error
 }
 
-func NewARM64Registers(regs *ARM64PtraceRegs, loadFpRegs func(*ARM64Registers) error) *ARM64Registers {
-	return &ARM64Registers{Regs: regs, loadFpRegs: loadFpRegs}
+func NewARM64Registers(regs *ARM64PtraceRegs, iscgo bool, tpidr_el0 uint64, loadFpRegs func(*ARM64Registers) error) *ARM64Registers {
+	return &ARM64Registers{Regs: regs, iscgo: iscgo, tpidr_el0: tpidr_el0, loadFpRegs: loadFpRegs}
 }
 
 // ARM64PtraceRegs is the struct used by the linux kernel to return the
@@ -102,13 +104,16 @@ func (r *ARM64Registers) BP() uint64 {
 
 // TLS returns the address of the thread local storage memory segment.
 func (r *ARM64Registers) TLS() uint64 {
-	return 0
+	if !r.iscgo {
+		return 0
+	}
+	return r.tpidr_el0
 }
 
 // GAddr returns the address of the G variable if it is known, 0 and false
 // otherwise.
 func (r *ARM64Registers) GAddr() (uint64, bool) {
-	return r.Regs.Regs[28], true
+	return r.Regs.Regs[28], !r.iscgo
 }
 
 // Get returns the value of the n-th register (in arm64asm order).
diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go
index 00db889a..511b2772 100644
--- a/pkg/proc/native/proc.go
+++ b/pkg/proc/native/proc.go
@@ -41,6 +41,8 @@ type nativeProcess struct {
 	// this process.
 	ctty *os.File
 
+	iscgo bool
+
 	exited, detached bool
 }
 
@@ -281,12 +283,19 @@ func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc
 	if !dbp.childProcess {
 		stopReason = proc.StopAttached
 	}
-	return proc.NewTarget(dbp, dbp.memthread, proc.NewTargetConfig{
+	tgt, err := proc.NewTarget(dbp, dbp.memthread, proc.NewTargetConfig{
 		Path:                path,
 		DebugInfoDirs:       debugInfoDirs,
 		DisableAsyncPreempt: runtime.GOOS == "windows" || runtime.GOOS == "freebsd",
 		StopReason:          stopReason,
 		CanDump:             runtime.GOOS == "linux"})
+	if err != nil {
+		return nil, err
+	}
+	if dbp.bi.Arch.Name == "arm64" {
+		dbp.iscgo = tgt.IsCgo()
+	}
+	return tgt, nil
 }
 
 func (dbp *nativeProcess) handlePtraceFuncs() {
diff --git a/pkg/proc/native/registers_linux_arm64.go b/pkg/proc/native/registers_linux_arm64.go
index 19ee8aba..19971298 100644
--- a/pkg/proc/native/registers_linux_arm64.go
+++ b/pkg/proc/native/registers_linux_arm64.go
@@ -15,6 +15,7 @@ import (
 const (
 	_AARCH64_GREGS_SIZE  = 34 * 8
 	_AARCH64_FPREGS_SIZE = 32*16 + 8
+	_NT_ARM_TLS          = 0x401 // used in PTRACE_GETREGSET on ARM64 to retrieve the value of TPIDR_EL0, see source/include/uapi/linux/elf.h and source/arch/arm64/kernel/ptrace.c
 )
 
 func ptraceGetGRegs(pid int, regs *linutil.ARM64PtraceRegs) (err error) {
@@ -26,6 +27,15 @@ func ptraceGetGRegs(pid int, regs *linutil.ARM64PtraceRegs) (err error) {
 	return
 }
 
+func ptraceGetTpidr_el0(pid int, tpidr_el0 *uint64) (err error) {
+	iov := sys.Iovec{Base: (*byte)(unsafe.Pointer(tpidr_el0)), Len: uint64(unsafe.Sizeof(*tpidr_el0))}
+	_, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_GETREGSET, uintptr(pid), uintptr(_NT_ARM_TLS), uintptr(unsafe.Pointer(&iov)), 0, 0)
+	if err == syscall.Errno(0) {
+		err = nil
+	}
+	return
+}
+
 func ptraceSetGRegs(pid int, regs *linutil.ARM64PtraceRegs) (err error) {
 	iov := sys.Iovec{Base: (*byte)(unsafe.Pointer(regs)), Len: _AARCH64_GREGS_SIZE}
 	_, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_SETREGSET, uintptr(pid), uintptr(elf.NT_PRSTATUS), uintptr(unsafe.Pointer(&iov)), 0, 0)
@@ -92,7 +102,14 @@ func registers(thread *nativeThread) (proc.Registers, error) {
 	if err != nil {
 		return nil, err
 	}
-	r := linutil.NewARM64Registers(&regs, func(r *linutil.ARM64Registers) error {
+	var tpidr_el0 uint64
+	if thread.dbp.iscgo {
+		thread.dbp.execPtraceFunc(func() { err = ptraceGetTpidr_el0(thread.ID, &tpidr_el0) })
+		if err != nil {
+			return nil, err
+		}
+	}
+	r := linutil.NewARM64Registers(&regs, thread.dbp.iscgo, tpidr_el0, func(r *linutil.ARM64Registers) error {
 		var floatLoadError error
 		r.Fpregs, r.Fpregset, floatLoadError = thread.fpRegisters()
 		return floatLoadError
diff --git a/pkg/proc/target.go b/pkg/proc/target.go
index 1bda30f3..9f5235bd 100644
--- a/pkg/proc/target.go
+++ b/pkg/proc/target.go
@@ -61,6 +61,7 @@ type Target struct {
 	// have read and parsed from the targets memory.
 	// This must be cleared whenever the target is resumed.
 	gcache goroutineCache
+	iscgo  *bool
 }
 
 // ErrProcessExited indicates that the process has exited and contains both
@@ -181,6 +182,24 @@ func NewTarget(p Process, currentThread Thread, cfg NewTargetConfig) (*Target, e
 	return t, nil
 }
 
+// IsCgo returns the value of runtime.iscgo
+func (t *Target) IsCgo() bool {
+	if t.iscgo != nil {
+		return *t.iscgo
+	}
+	scope := globalScope(t.BinInfo(), t.BinInfo().Images[0], t.Memory())
+	iscgov, err := scope.findGlobal("runtime", "iscgo")
+	if err == nil {
+		iscgov.loadValue(loadFullValue)
+		if iscgov.Unreadable == nil {
+			t.iscgo = new(bool)
+			*t.iscgo = constant.BoolVal(iscgov.Value)
+			return constant.BoolVal(iscgov.Value)
+		}
+	}
+	return false
+}
+
 // SupportsFunctionCalls returns whether or not the backend supports
 // calling functions during a debug session.
 // Currently only non-recorded processes running on AMD64 support
diff --git a/service/dap/server.go b/service/dap/server.go
index 7069a87f..cfb67abc 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -87,6 +87,17 @@ var defaultArgs = launchAttachArgs{
 	showGlobalVariables: false,
 }
 
+// DefaultLoadConfig controls how variables are loaded from the target's memory, borrowing the
+// default value from the original vscode-go debug adapter and rpc server.
+// TODO(polina): Support setting config via launch/attach args or only rely on on-demand loading?
+var DefaultLoadConfig = proc.LoadConfig{
+	FollowPointers:     true,
+	MaxVariableRecurse: 1,
+	MaxStringLen:       64,
+	MaxArrayValues:     64,
+	MaxStructFields:    -1,
+}
+
 // NewServer creates a new DAP Server. It takes an opened Listener
 // via config and assumes its ownership. config.disconnectChan has to be set;
 // it will be closed by the server when the client disconnects or requests
@@ -171,6 +182,7 @@ func (s *Server) signalDisconnect() {
 	}
 	if s.binaryToRemove != "" {
 		gobuild.Remove(s.binaryToRemove)
+		s.binaryToRemove = ""
 	}
 }
 
@@ -825,11 +837,9 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
 
 	goid := sf.(stackFrame).goroutineID
 	frame := sf.(stackFrame).frameIndex
-	// TODO(polina): Support setting config via launch/attach args
-	cfg := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
 
 	// Retrieve arguments
-	args, err := s.debugger.FunctionArguments(goid, frame, 0, cfg)
+	args, err := s.debugger.FunctionArguments(goid, frame, 0, DefaultLoadConfig)
 	if err != nil {
 		s.sendErrorResponse(request.Request, UnableToListArgs, "Unable to list args", err.Error())
 		return
@@ -837,7 +847,7 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
 	argScope := &fullyQualifiedVariable{&proc.Variable{Name: "Arguments", Children: slicePtrVarToSliceVar(args)}, "", true}
 
 	// Retrieve local variables
-	locals, err := s.debugger.LocalVariables(goid, frame, 0, cfg)
+	locals, err := s.debugger.LocalVariables(goid, frame, 0, DefaultLoadConfig)
 	if err != nil {
 		s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", err.Error())
 		return
@@ -866,7 +876,7 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
 			return
 		}
 		currPkgFilter := fmt.Sprintf("^%s\\.", currPkg)
-		globals, err := s.debugger.PackageVariables(currPkgFilter, cfg)
+		globals, err := s.debugger.PackageVariables(currPkgFilter, DefaultLoadConfig)
 		if err != nil {
 			s.sendErrorResponse(request.Request, UnableToListGlobals, "Unable to list globals", err.Error())
 			return
@@ -908,8 +918,6 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
 		return
 	}
 	children := make([]dap.Variable, 0)
-	// TODO(polina): check and handle if variable loaded incompletely
-	// https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables
 
 	switch v.Kind {
 	case reflect.Map:
@@ -1045,6 +1053,11 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 		return
 	}
 	typeName := api.PrettyTypeName(v.DwarfType)
+
+	// As per https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables,
+	// some of the types might be fully or partially not loaded based on LoadConfig. For now, clearly
+	// communicate when values are not fully loaded. TODO(polina): look into loading on demand.
+
 	switch v.Kind {
 	case reflect.UnsafePointer:
 		if len(v.Children) == 0 {
@@ -1061,10 +1074,20 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 			value = "void"
 		} else {
 			value = fmt.Sprintf("<%s>(%#x)", typeName, v.Children[0].Addr)
-			variablesReference = maybeCreateVariableHandle(v)
+			if v.Children[0].OnlyAddr { // Not fully loaded
+				value += " (value not loaded)"
+				// Since child is not fully loaded, don't provide a reference to view it.
+			} else {
+				value = fmt.Sprintf("<%s>(%#x)", typeName, v.Children[0].Addr)
+				variablesReference = maybeCreateVariableHandle(v)
+			}
 		}
 	case reflect.Array:
-		value = "<" + typeName + ">"
+		if v.Len > int64(len(v.Children)) { // Not fully loaded
+			value = fmt.Sprintf("<%s> (length: %d, loaded: %d)", typeName, v.Len, len(v.Children))
+		} else {
+			value = fmt.Sprintf("<%s>", typeName)
+		}
 		if len(v.Children) > 0 {
 			variablesReference = maybeCreateVariableHandle(v)
 		}
@@ -1072,7 +1095,11 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 		if v.Base == 0 {
 			value = "nil <" + typeName + ">"
 		} else {
-			value = fmt.Sprintf("<%s> (length: %d, cap: %d)", typeName, v.Len, v.Cap)
+			if v.Len > int64(len(v.Children)) { // Not fully loaded
+				value = fmt.Sprintf("<%s> (length: %d, cap: %d, loaded: %d)", typeName, v.Len, v.Cap, len(v.Children))
+			} else {
+				value = fmt.Sprintf("<%s> (length: %d, cap: %d)", typeName, v.Len, v.Cap)
+			}
 			if len(v.Children) > 0 {
 				variablesReference = maybeCreateVariableHandle(v)
 			}
@@ -1081,7 +1108,11 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 		if v.Base == 0 {
 			value = "nil <" + typeName + ">"
 		} else {
-			value = fmt.Sprintf("<%s> (length: %d)", typeName, v.Len)
+			if v.Len > int64(len(v.Children)/2) { // Not fully loaded
+				value = fmt.Sprintf("<%s> (length: %d, loaded: %d)", typeName, v.Len, len(v.Children)/2)
+			} else {
+				value = fmt.Sprintf("<%s> (length: %d)", typeName, v.Len)
+			}
 			if len(v.Children) > 0 {
 				variablesReference = maybeCreateVariableHandle(v)
 			}
@@ -1089,7 +1120,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 	case reflect.String:
 		vvalue := constant.StringVal(v.Value)
 		lenNotLoaded := v.Len - int64(len(vvalue))
-		if lenNotLoaded > 0 {
+		if lenNotLoaded > 0 { // Not fully loaded
 			vvalue += fmt.Sprintf("...+%d more", lenNotLoaded)
 		}
 		value = fmt.Sprintf("%q", vvalue)
@@ -1111,21 +1142,35 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 		} else if len(v.Children) == 0 || v.Children[0].Kind == reflect.Invalid && v.Children[0].Addr == 0 {
 			value = "nil <" + typeName + ">"
 		} else {
-			value = "<" + typeName + "(" + v.Children[0].TypeString() + ")" + ">"
-			// TODO(polina): should we remove one level of indirection and skip "data"?
-			// Then we will have:
-			// Before:
-			//   i: <interface{}(int)>
-			//      data: 123
-			// After:
-			//   i: <interface{}(int)> 123
-			// Before:
-			//   i: <interface{}(main.MyStruct)>
-			//      data: <main.MyStruct>
-			//         field1: ...
-			// After:
-			//   i: <interface{}(main.MyStruct)>
-			//      field1: ...
+			if v.Children[0].OnlyAddr { // Not fully loaded
+				value = "<" + typeName + "(" + v.Children[0].TypeString() + ")" + ">" + " (data not loaded)"
+				// Since child is not fully loaded, don't provide a reference to view it.
+			} else {
+				value = "<" + typeName + "(" + v.Children[0].TypeString() + ")" + ">"
+				// TODO(polina): should we remove one level of indirection and skip "data"?
+				// Then we will have:
+				//   Before:
+				//     i: <interface{}(int)>
+				//        data: 123
+				//   After:
+				//     i: <interface{}(int)> 123
+				//   Before:
+				//     i: <interface{}(main.MyStruct)>
+				//        data: <main.MyStruct>
+				//           field1: ...
+				//   After:
+				//     i: <interface{}(main.MyStruct)>
+				//        field1: ...
+				variablesReference = maybeCreateVariableHandle(v)
+			}
+		}
+	case reflect.Struct:
+		if v.Len > int64(len(v.Children)) { // Not fully loaded
+			value = fmt.Sprintf("<%s> (fields: %d, loaded: %d)", typeName, v.Len, len(v.Children))
+		} else {
+			value = fmt.Sprintf("<%s>", typeName)
+		}
+		if len(v.Children) > 0 {
 			variablesReference = maybeCreateVariableHandle(v)
 		}
 	case reflect.Complex64, reflect.Complex128:
@@ -1142,7 +1187,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 			v.Children[1].Kind = reflect.Float64
 		}
 		fallthrough
-	default: // Struct, complex, scalar
+	default: // complex, scalar
 		vvalue := api.VariableValueAsString(v)
 		if vvalue != "" {
 			value = vvalue
@@ -1177,9 +1222,6 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 		goid = sf.(stackFrame).goroutineID
 		frame = sf.(stackFrame).frameIndex
 	}
-	// TODO(polina): Support config settings via launch/attach args vs auto-loading?
-	apiCfg := &api.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
-	prcCfg := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
 
 	response := &dap.EvaluateResponse{Response: *newResponse(request.Request)}
 	isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression)
@@ -1201,7 +1243,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 		}
 		state, err := s.debugger.Command(&api.DebuggerCommand{
 			Name:                 api.Call,
-			ReturnInfoLoadConfig: apiCfg,
+			ReturnInfoLoadConfig: api.LoadConfigFromProc(&DefaultLoadConfig),
 			Expr:                 strings.Replace(request.Arguments.Expression, "call ", "", 1),
 			UnsafeCall:           false,
 			GoroutineID:          goid,
@@ -1224,7 +1266,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 			if t.GoroutineID == stateBeforeCall.SelectedGoroutine.ID &&
 				t.Line == stateBeforeCall.SelectedGoroutine.CurrentLoc.Line && t.CallReturn {
 				// The call completed. Get the return values.
-				retVars, err = s.debugger.FindThreadReturnValues(t.ID, prcCfg)
+				retVars, err = s.debugger.FindThreadReturnValues(t.ID, DefaultLoadConfig)
 				if err != nil {
 					s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
 					return
@@ -1264,7 +1306,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 			}
 		}
 	} else { // {expression}
-		exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, prcCfg)
+		exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, DefaultLoadConfig)
 		if err != nil {
 			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
 			return
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 6994f363..7c336311 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -413,6 +413,7 @@ func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
 // If `kill` is true we will kill the process after
 // detaching.
 func (d *Debugger) Detach(kill bool) error {
+	d.log.Debug("detaching")
 	d.targetMutex.Lock()
 	defer d.targetMutex.Unlock()
 	if ok, _ := d.target.Valid(); !ok {


Test patch:
diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md
index bea2522f..5807bfd6 100644
--- a/Documentation/backend_test_health.md
+++ b/Documentation/backend_test_health.md
@@ -1,10 +1,11 @@
 Tests skipped by each supported backend:
 
-* 386 skipped = 2% (3/147)
+* 386 skipped = 2.7% (4/147)
 	* 1 broken
-	* 2 broken - cgo stacktraces
-* arm64 skipped = 2% (3/147)
+	* 3 broken - cgo stacktraces
+* arm64 skipped = 2.7% (4/147)
 	* 2 broken
+	* 1 broken - cgo stacktraces
 	* 1 broken - global variable symbolication
 * darwin/lldb skipped = 0.68% (1/147)
 	* 1 upstream issue
diff --git a/_fixtures/testvariables2.go b/_fixtures/testvariables2.go
index 4fcee1cf..a3a6deb7 100644
--- a/_fixtures/testvariables2.go
+++ b/_fixtures/testvariables2.go
@@ -357,6 +357,9 @@ func main() {
 		fmt.Println(amb1)
 	}
 
+	longarr := [100]int{}
+	longslice := make([]int, 100, 100)
+
 	runtime.Breakpoint()
-	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, errtypednil, emptyslice, emptymap, byteslice, runeslice, bytearray, runearray, longstr, nilstruct, as2, as2.NonPointerRecieverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5)
+	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, errtypednil, emptyslice, emptymap, byteslice, runeslice, bytearray, runearray, longstr, nilstruct, as2, as2.NonPointerRecieverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice)
 }
diff --git a/_scripts/test_mac.sh b/_scripts/test_mac.sh
new file mode 100644
index 00000000..88419938
--- /dev/null
+++ b/_scripts/test_mac.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+set -x
+set -e
+
+GOVERSION=$1
+ARCH=$2
+TMPDIR=$3
+
+if [ "$GOVERSION" = "gotip" ]; then
+    git clone https://go.googlesource.com/go $TMPDIR/go
+    export GOROOT_BOOTSTRAP=$GOROOT
+    cd $TMPDIR/go/src
+    ./make.bash
+    cd -
+else
+    cd $TMPDIR
+    curl -sSL "https://storage.googleapis.com/golang/$GOVERSION.darwin-$ARCH.tar.gz" | tar -vxz
+    cd -
+fi
+
+mkdir -p $TMPDIR/gopath
+
+export GOROOT="$TMPDIR/go"
+export GOPATH="$TMPDIR/gopath"
+export GOARCH="$ARCH"
+export PATH="$GOROOT/bin:$PATH"
+
+make test
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 4e7f0559..9149ccab 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -3289,9 +3289,8 @@ func TestCgoStacktrace(t *testing.T) {
 		}
 	}
 
-	if runtime.GOARCH == "386" {
-		t.Skip("cgo stacktraces not supported on i386 for now")
-	}
+	skipOn(t, "broken - cgo stacktraces", "386")
+	skipOn(t, "broken - cgo stacktraces", "arm64")
 	protest.MustHaveCgo(t)
 
 	// Tests that:
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 92c8b05a..96d6e769 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"flag"
 	"io"
+	"io/ioutil"
 	"net"
 	"os"
 	"os/exec"
@@ -1022,7 +1023,6 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 			// Breakpoints are set within the program
 			fixture.Source, []int{},
 			[]onBreakpoint{{
-				// Stop at line 317
 				execute: func() {
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
@@ -1035,7 +1035,6 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 				},
 				disconnect: false,
 			}, {
-				// Stop at line 322
 				execute: func() {
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
@@ -1275,15 +1274,77 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 						expectVarRegex(t, val, 0, "^$", `\(\*unread\)`, "unreadable <.+>", noChildren)
 						validateEvaluateName(t, client, val, 0)
 					}
+				},
+				disconnect: true,
+			}})
+	})
+}
+
+// TestVariablesLoading exposes test cases where variables might be partiall or
+// fully unloaded.
+func TestVariablesLoading(t *testing.T) {
+	runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Breakpoints are set within the program
+			fixture.Source, []int{},
+			[]onBreakpoint{{
+				execute:    func() {},
+				disconnect: false,
+			}, {
+				execute: func() {
+					// Change default config values to trigger certain unloaded corner cases
+					DefaultLoadConfig.MaxStructFields = 5
+					defer func() { DefaultLoadConfig.MaxStructFields = -1 }()
+
+					client.StackTraceRequest(1, 0, 0)
+					client.ExpectStackTraceResponse(t)
+
+					client.ScopesRequest(1000)
+					client.ExpectScopesResponse(t)
+
+					client.VariablesRequest(1001) // Locals
+					locals := client.ExpectVariablesResponse(t)
+
+					// String partially missing based on LoadConfig.MaxStringLen
+					expectVarExact(t, locals, -1, "longstr", "longstr", "\"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more\"", noChildren)
 
-					// Test that variables are not yet loaded completely.
-					ref = expectVarExact(t, locals, -1, "m1", "m1", "<map[string]main.astruct> (length: 66)", hasChildren)
+					// Array partially missing based on LoadConfig.MaxArrayValues
+					ref := expectVarExact(t, locals, -1, "longarr", "longarr", "<[100]int> (length: 100, loaded: 64)", hasChildren)
+					if ref > 0 {
+						client.VariablesRequest(ref)
+						longarr := client.ExpectVariablesResponse(t)
+						expectChildren(t, longarr, "longarr", 64)
+					}
+
+					// Slice partially missing based on LoadConfig.MaxArrayValues
+					ref = expectVarExact(t, locals, -1, "longslice", "longslice", "<[]int> (length: 100, cap: 100, loaded: 64)", hasChildren)
+					if ref > 0 {
+						client.VariablesRequest(ref)
+						longarr := client.ExpectVariablesResponse(t)
+						expectChildren(t, longarr, "longslice", 64)
+					}
+
+					// Map partially missing based on LoadConfig.MaxArrayValues
+					ref = expectVarExact(t, locals, -1, "m1", "m1", "<map[string]main.astruct> (length: 66, loaded: 64)", hasChildren)
 					if ref > 0 {
 						client.VariablesRequest(ref)
 						m1 := client.ExpectVariablesResponse(t)
-						expectChildren(t, m1, "m1", 64) // TODO(polina): should be 66.
+						expectChildren(t, m1, "m1", 64)
+					}
+
+					// Struct partially missing based on LoadConfig.MaxStructFields
+					ref = expectVarExact(t, locals, -1, "sd", "sd", "<main.D> (fields: 6, loaded: 5)", hasChildren)
+					if ref > 0 {
+						client.VariablesRequest(ref)
+						sd := client.ExpectVariablesResponse(t)
+						expectChildren(t, sd, "sd", 5)
 					}
 
+					// Struct fully missing due to hitting LoadConfig.MaxVariableRecurse (also tests evaluateName)
 					ref = expectVarExact(t, locals, -1, "c1", "c1", "<main.cstruct>", hasChildren)
 					if ref > 0 {
 						client.VariablesRequest(ref)
@@ -1299,12 +1360,13 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 								client.VariablesRequest(ref)
 								c1sa0 := client.ExpectVariablesResponse(t)
 								expectChildren(t, c1sa0, "c1.sa[0]", 1)
-								// TODO(polina): there should be a child here once we support auto loading
-								expectVarExact(t, c1sa0, 0, "", "(*c1.sa[0])", "<main.astruct>", noChildren)
+								// TODO(polina): there should be children here once we support auto loading
+								expectVarExact(t, c1sa0, 0, "", "(*c1.sa[0])", "<main.astruct> (fields: 2, loaded: 0)", noChildren)
 							}
 						}
 					}
 
+					// Struct fully missing due to hitting LoadConfig.MaxVariableRecurse (also tests evaluteName)
 					ref = expectVarExact(t, locals, -1, "aas", "aas", "<[]main.a> (length: 1, cap: 1)", hasChildren)
 					if ref > 0 {
 						client.VariablesRequest(ref)
@@ -1321,11 +1383,12 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 								aas0aas := client.ExpectVariablesResponse(t)
 								expectChildren(t, aas0aas, "aas[0].aas", 1)
 								// TODO(polina): there should be a child here once we support auto loading - test for "aas[0].aas[0].aas"
-								expectVarExact(t, aas0aas, 0, "[0]", "aas[0].aas[0]", "<main.a>", noChildren)
+								expectVarExact(t, aas0aas, 0, "[0]", "aas[0].aas[0]", "<main.a> (fields: 1, loaded: 0)", noChildren)
 							}
 						}
 					}
 
+					// Map fully missing due to hitting LoadConfig.MaxVariableRecurse (also tests evaluateName)
 					ref = expectVarExact(t, locals, -1, "tm", "tm", "<main.truncatedMap>", hasChildren)
 					if ref > 0 {
 						client.VariablesRequest(ref)
@@ -1337,9 +1400,62 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 							tm_v := client.ExpectVariablesResponse(t)
 							expectChildren(t, tm_v, "tm.v", 1)
 							// TODO(polina): there should be children here once we support auto loading - test for "tm.v[0]["gutters"]"
-							expectVarExact(t, tm_v, 0, "[0]", "tm.v[0]", "<map[string]main.astruct> (length: 66)", noChildren)
+							expectVarExact(t, tm_v, 0, "[0]", "tm.v[0]", "<map[string]main.astruct> (length: 66, loaded: 0)", noChildren)
+						}
+					}
+
+					// Interface fully missing due to hitting LoadConfig.MaxVariableRecurse
+					ref = expectVarRegex(t, locals, -1, "ptrinf", "ptrinf", `<\*interface {}>\(0x[0-9a-f]+\)`, hasChildren)
+					if ref > 0 {
+						client.VariablesRequest(ref)
+						ptrinf := client.ExpectVariablesResponse(t)
+						ref = expectVarExact(t, ptrinf, 0, "", "(*ptrinf)", "<interface {}(**interface {})>", hasChildren)
+						if ref > 0 {
+							client.VariablesRequest(ref)
+							ptrinfVal := client.ExpectVariablesResponse(t)
+							ref = expectVarRegex(t, ptrinfVal, 0, "", `\(\*ptrinf\)\.\(data\)`, `<\*\*interface {}>\(0x[0-9a-f]+\)`, hasChildren)
+							if ref > 0 {
+								client.VariablesRequest(ref)
+								ptrinfValData := client.ExpectVariablesResponse(t)
+								ref = expectVarRegex(t, ptrinfValData, 0, "", `\(\*\(\*ptrinf\)\.\(data\)\)`, `<\*interface {}>\(0x[0-9a-f]+\)`, hasChildren)
+								if ref > 0 {
+									client.VariablesRequest(ref)
+									iface6dataValVal := client.ExpectVariablesResponse(t)
+									expectVarExact(t, iface6dataValVal, 0, "", "(*(*(*ptrinf).(data)))", "<interface {}(**interface {})> (data not loaded)", noChildren)
+								}
+							}
 						}
 					}
+
+					// Pointer fully missing - see below
+				},
+				disconnect: true,
+			}})
+	})
+	runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Breakpoints are set within the program
+			fixture.Source, []int{},
+			[]onBreakpoint{{
+				execute: func() {
+					DefaultLoadConfig.FollowPointers = false
+					defer func() { DefaultLoadConfig.FollowPointers = true }()
+
+					client.StackTraceRequest(1, 0, 0)
+					client.ExpectStackTraceResponse(t)
+
+					client.ScopesRequest(1000)
+					client.ExpectScopesResponse(t)
+
+					client.VariablesRequest(1001) // Locals
+					locals := client.ExpectVariablesResponse(t)
+
+					// Pointer value not loaded
+					expectVarRegex(t, locals, -1, "ptrinf", "ptrinf", `<\*interface {}>\(0x[0-9a-f]+\) \(value not loaded\)`, noChildren)
 				},
 				disconnect: true,
 			}})
@@ -1580,6 +1696,11 @@ func TestEvaluateRequest(t *testing.T) {
 						validateEvaluateName(t, client, a5, 4)
 					}
 
+					// Variable lookup that's not fully loaded
+					client.EvaluateRequest("ba", 1000, "this context will be ignored")
+					got = client.ExpectEvaluateResponse(t)
+					expectEval(t, got, "<[]int> (length: 200, cap: 200, loaded: 64)", hasChildren)
+
 					// All (binary and unary) on basic types except <-, ++ and --
 					client.EvaluateRequest("1+1", 1000, "this context will be ignored")
 					got = client.ExpectEvaluateResponse(t)
@@ -1598,14 +1719,8 @@ func TestEvaluateRequest(t *testing.T) {
 					// Type casts of integer constants into any pointer type and vice versa
 					client.EvaluateRequest("(*int)(2)", 1000, "this context will be ignored")
 					got = client.ExpectEvaluateResponse(t)
-					ref = expectEval(t, got, "<*int>(0x2)", hasChildren)
-					if ref > 0 {
-						client.VariablesRequest(ref)
-						expr := client.ExpectVariablesResponse(t)
-						expectChildren(t, expr, "(*int)(2)", 1)
-						// TODO(polina): should this be printed as (unknown int) instead?
-						expectVarExact(t, expr, 0, "", "(*((*int)(2)))", "<int>", noChildren)
-					}
+					expectEval(t, got, "<*int>(0x2) (value not loaded)", noChildren)
+
 					// Type casts between string, []byte and []rune
 					client.EvaluateRequest("[]byte(\"ABC€\")", 1000, "this context will be ignored")
 					got = client.ExpectEvaluateResponse(t)
@@ -2126,15 +2241,37 @@ func runDebugSession(t *testing.T, client *daptest.Client, cmd string, cmdReques
 }
 
 func TestLaunchDebugRequest(t *testing.T) {
+	rescueStderr := os.Stderr
+	r, w, _ := os.Pipe()
+	os.Stderr = w
+
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
 		// We reuse the harness that builds, but ignore the built binary,
 		// only relying on the source to be built in response to LaunchRequest.
 		runDebugSession(t, client, "launch", func() {
 			// Use the default output directory.
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				"mode": "debug", "program": fixture.Source})
+				"mode": "debug", "program": fixture.Source, "output": "__mydir"})
 		}, fixture.Source)
 	})
+	// Wait for the test to finish to capture all stderr
+	time.Sleep(100 * time.Millisecond)
+
+	w.Close()
+	err, _ := ioutil.ReadAll(r)
+	t.Log(string(err))
+	os.Stderr = rescueStderr
+
+	rmErrRe, _ := regexp.Compile(`could not remove .*\n`)
+	rmErr := rmErrRe.FindString(string(err))
+	if rmErr != "" {
+		// On Windows, a file in use cannot be removed, resulting in "Access is denied".
+		// When the process exits, Delve releases the binary by calling
+		// BinaryInfo.Close(), but it appears that it is still in use (by Windows?)
+		// shortly after. gobuild.Remove has a delay to address this.
+		// If this test becomes flaky, see if the delay needs adjusting.
+		t.Fatalf("Binary removal failure:\n%s\n", rmErr)
+	}
 }
 
 func TestLaunchTestRequest(t *testing.T) {
@@ -2176,6 +2313,7 @@ func TestLaunchRequestWithBuildFlags(t *testing.T) {
 			client.LaunchRequestWithArgs(map[string]interface{}{
 				"mode": "debug", "program": fixture.Source,
 				"buildFlags": "-ldflags '-X main.Hello=World'"})
+			// will write to default output dir __debug_bin
 		}, fixture.Source)
 	})
 }
diff --git a/service/test/integration1_test.go b/service/test/integration1_test.go
index 8b5afdab..3cb3a995 100644
--- a/service/test/integration1_test.go
+++ b/service/test/integration1_test.go
@@ -728,7 +728,7 @@ func Test1ClientServer_FullStacktrace(t *testing.T) {
 		assertNoError(err, t, "GoroutinesInfo()")
 		found := make([]bool, 10)
 		for _, g := range gs {
-			frames, err := c.Stacktrace(g.ID, 10, true)
+			frames, err := c.Stacktrace(g.ID, 40, true)
 			assertNoError(err, t, fmt.Sprintf("Stacktrace(%d)", g.ID))
 			t.Logf("goroutine %d", g.ID)
 			for i, frame := range frames {
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index e0468a6b..5cab11d8 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -923,7 +923,7 @@ func TestClientServer_FullStacktrace(t *testing.T) {
 		assertNoError(err, t, "GoroutinesInfo()")
 		found := make([]bool, 10)
 		for _, g := range gs {
-			frames, err := c.Stacktrace(g.ID, 10, 0, &normalLoadConfig)
+			frames, err := c.Stacktrace(g.ID, 40, 0, &normalLoadConfig)
 			assertNoError(err, t, fmt.Sprintf("Stacktrace(%d)", g.ID))
 			t.Logf("goroutine %d", g.ID)
 			for i, frame := range frames {
diff --git a/service/test/variables_test.go b/service/test/variables_test.go
index 64ec5dc9..77bdcd94 100644
--- a/service/test/variables_test.go
+++ b/service/test/variables_test.go
@@ -1550,6 +1550,10 @@ func TestPluginVariables(t *testing.T) {
 func TestCgoEval(t *testing.T) {
 	protest.MustHaveCgo(t)
 
+	if runtime.GOARCH == "arm64" {
+		t.Skip("cgo evaluation broken on arm64")
+	}
+
 	testcases := []varTest{
 		{"s", true, `"a string"`, `"a string"`, "*char", nil},
 		{"longstring", true, `"averylongstring0123456789a0123456789b0123456789c0123456789d01234...+1 more"`, `"averylongstring0123456789a0123456789b0123456789c0123456789d01234...+1 more"`, "*const char", nil},