Repository: go-delve/delve
Pull request #2747 (2021-10-14 23:25:33)

service/dap: allow expression evaluation in log messages
From the DAP spec:   
```
If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead.
Expressions within {} are interpolated.
```

This change parses the log messages and stores the parsed values as a format string and list of expressions to evaluate and get the string value of.

Updates golang/vscode-go#123


Patch:
diff --git a/_fixtures/goroutinebreak.go b/_fixtures/goroutinebreak.go
new file mode 100644
index 00000000..438d1ed8
--- /dev/null
+++ b/_fixtures/goroutinebreak.go
@@ -0,0 +1,26 @@
+package main
+
+import "runtime"
+
+const N = 10
+
+func agoroutine(started chan<- struct{}, done chan<- struct{}, i int) {
+	started <- struct{}{}
+	done <- struct{}{}
+}
+
+func main() {
+	done := make(chan struct{})
+	started := make(chan struct{})
+	for i := 0; i < N; i++ {
+		runtime.Breakpoint()
+		go agoroutine(started, done, i)
+	}
+	for i := 0; i < N; i++ {
+		<-started
+	}
+	runtime.Gosched()
+	for i := 0; i < N; i++ {
+		<-done
+	}
+}
diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go
index 33ce65cc..15df5f63 100644
--- a/cmd/dlv/cmds/commands.go
+++ b/cmd/dlv/cmds/commands.go
@@ -673,6 +673,7 @@ func traceCmd(cmd *cobra.Command, args []string) {
 			done := make(chan struct{})
 			defer close(done)
 			go func() {
+				gFnEntrySeen := map[int]struct{}{}
 				for {
 					select {
 					case <-done:
@@ -694,7 +695,16 @@ func traceCmd(cmd *cobra.Command, args []string) {
 									params.WriteString(p.Value)
 								}
 							}
-							fmt.Fprintf(os.Stderr, "> (%d) %s(%s)\n", t.GoroutineID, t.FunctionName, params.String())
+							_, seen := gFnEntrySeen[t.GoroutineID]
+							if seen {
+								for _, p := range t.ReturnParams {
+									fmt.Fprintf(os.Stderr, "=> %#v\n", p.Value)
+								}
+								delete(gFnEntrySeen, t.GoroutineID)
+							} else {
+								gFnEntrySeen[t.GoroutineID] = struct{}{}
+								fmt.Fprintf(os.Stderr, "> (%d) %s(%s)\n", t.GoroutineID, t.FunctionName, params.String())
+							}
 						}
 					}
 				}
diff --git a/pkg/config/split.go b/pkg/config/split.go
index 8279f22b..22fe18cb 100644
--- a/pkg/config/split.go
+++ b/pkg/config/split.go
@@ -2,6 +2,11 @@ package config
 
 import (
 	"bytes"
+	"fmt"
+	"io"
+	"reflect"
+	"strconv"
+	"strings"
 	"unicode"
 )
 
@@ -61,3 +66,138 @@ func SplitQuotedFields(in string, quote rune) []string {
 
 	return r
 }
+
+func ConfigureSetSimple(rest string, cfgname string, field reflect.Value) error {
+	simpleArg := func(typ reflect.Type) (reflect.Value, error) {
+		switch typ.Kind() {
+		case reflect.Int:
+			n, err := strconv.Atoi(rest)
+			if err != nil {
+				return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number", cfgname)
+			}
+			if n < 0 {
+				return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number greater than zero", cfgname)
+			}
+			return reflect.ValueOf(&n), nil
+		case reflect.Bool:
+			v := rest == "true"
+			return reflect.ValueOf(&v), nil
+		case reflect.String:
+			return reflect.ValueOf(&rest), nil
+		default:
+			return reflect.ValueOf(nil), fmt.Errorf("unsupported type for configuration key %q", cfgname)
+		}
+	}
+
+	if field.Kind() == reflect.Ptr {
+		val, err := simpleArg(field.Type().Elem())
+		if err != nil {
+			return err
+		}
+		field.Set(val)
+	} else {
+		val, err := simpleArg(field.Type())
+		if err != nil {
+			return err
+		}
+		field.Set(val.Elem())
+	}
+	return nil
+}
+
+func ConfigureList(w io.Writer, config interface{}, tag string) {
+	it := IterateConfiguration(config, tag)
+	for it.Next() {
+		fieldName, field := it.Field()
+		if fieldName == "" {
+			continue
+		}
+
+		writeField(w, field, fieldName)
+	}
+}
+
+func writeField(w io.Writer, field reflect.Value, fieldName string) {
+	switch field.Kind() {
+	case reflect.Interface:
+		switch field := field.Interface().(type) {
+		case string:
+			fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
+		default:
+			fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
+		}
+	case reflect.Ptr:
+		if !field.IsNil() {
+			fmt.Fprintf(w, "%s\t%v\n", fieldName, field.Elem())
+		} else {
+			fmt.Fprintf(w, "%s\t<not defined>\n", fieldName)
+		}
+	case reflect.String:
+		fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
+	default:
+		fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
+	}
+}
+
+type configureIterator struct {
+	cfgValue reflect.Value
+	cfgType  reflect.Type
+	i        int
+	tag      string
+}
+
+func IterateConfiguration(conf interface{}, tag string) *configureIterator {
+	cfgValue := reflect.ValueOf(conf).Elem()
+	cfgType := cfgValue.Type()
+
+	return &configureIterator{cfgValue, cfgType, -1, tag}
+}
+
+func (it *configureIterator) Next() bool {
+	it.i++
+	return it.i < it.cfgValue.NumField()
+}
+
+func (it *configureIterator) Field() (name string, field reflect.Value) {
+	name = it.cfgType.Field(it.i).Tag.Get(it.tag)
+	if comma := strings.Index(name, ","); comma >= 0 {
+		name = name[:comma]
+	}
+	field = it.cfgValue.Field(it.i)
+	return
+}
+
+func ConfigureListByName(conf interface{}, name, tag string) string {
+	if name == "" {
+		return ""
+	}
+	it := IterateConfiguration(conf, tag)
+	for it.Next() {
+		fieldName, field := it.Field()
+		if fieldName == name {
+			var buf bytes.Buffer
+			writeField(&buf, field, fieldName)
+			return buf.String()
+		}
+	}
+	return ""
+}
+
+func ConfigureFindFieldByName(conf interface{}, name, tag string) reflect.Value {
+	it := IterateConfiguration(conf, tag)
+	for it.Next() {
+		fieldName, field := it.Field()
+		if fieldName == name {
+			return field
+		}
+	}
+	return reflect.ValueOf(nil)
+}
+
+func Split2PartsBySpace(s string) []string {
+	v := strings.SplitN(s, " ", 2)
+	for i := range v {
+		v[i] = strings.TrimSpace(v[i])
+	}
+	return v
+}
diff --git a/pkg/proc/amd64_arch.go b/pkg/proc/amd64_arch.go
index db17533a..c8bccdff 100644
--- a/pkg/proc/amd64_arch.go
+++ b/pkg/proc/amd64_arch.go
@@ -131,6 +131,10 @@ const amd64cgocallSPOffsetSaveSlot = 0x28
 
 func amd64SwitchStack(it *stackIterator, _ *op.DwarfRegisters) bool {
 	if it.frame.Current.Fn == nil {
+		if it.systemstack && it.g != nil && it.top {
+			it.switchToGoroutineStack()
+			return true
+		}
 		return false
 	}
 	switch it.frame.Current.Fn.Name {
diff --git a/pkg/proc/arm64_arch.go b/pkg/proc/arm64_arch.go
index 031e1f73..e846523d 100644
--- a/pkg/proc/arm64_arch.go
+++ b/pkg/proc/arm64_arch.go
@@ -132,6 +132,10 @@ const arm64cgocallSPOffsetSaveSlot = 0x8
 const prevG0schedSPOffsetSaveSlot = 0x10
 
 func arm64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) bool {
+	if it.frame.Current.Fn == nil && it.systemstack && it.g != nil && it.top {
+		it.switchToGoroutineStack()
+		return true
+	}
 	if it.frame.Current.Fn != nil {
 		switch it.frame.Current.Fn.Name {
 		case "runtime.asmcgocall", "runtime.cgocallback_gofunc", "runtime.sigpanic", "runtime.cgocallback":
diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go
index b8434c4f..16019f2d 100644
--- a/pkg/proc/breakpoints.go
+++ b/pkg/proc/breakpoints.go
@@ -494,7 +494,6 @@ func (t *Target) SetEBPFTracepoint(fnName string) error {
 	}
 	// Start putting together the argument map. This will tell the eBPF program
 	// all of the arguments we want to trace and how to find them.
-	var args []ebpf.UProbeArgMap
 	fn, ok := t.BinInfo().LookupFunc[fnName]
 	if !ok {
 		return fmt.Errorf("could not find function %s", fnName)
@@ -533,16 +532,14 @@ func (t *Target) SetEBPFTracepoint(fnName string) error {
 	}
 	_, l, _ := t.BinInfo().PCToLine(fn.Entry)
 
+	var args []ebpf.UProbeArgMap
 	varEntries := reader.Variables(dwarfTree, fn.Entry, l, variablesFlags)
 	for _, entry := range varEntries {
-		isret, _ := entry.Val(dwarf.AttrVarParam).(bool)
-		if isret {
-			continue
-		}
 		_, dt, err := readVarEntry(entry.Tree, fn.cu.image)
 		if err != nil {
 			return err
 		}
+
 		offset, pieces, _, err := t.BinInfo().Location(entry, dwarf.AttrLocation, fn.Entry, op.DwarfRegisters{}, nil)
 		if err != nil {
 			return err
@@ -553,8 +550,16 @@ func (t *Target) SetEBPFTracepoint(fnName string) error {
 				paramPieces = append(paramPieces, int(piece.Val))
 			}
 		}
+		isret, _ := entry.Val(dwarf.AttrVarParam).(bool)
 		offset += int64(t.BinInfo().Arch.PtrSize())
-		args = append(args, ebpf.UProbeArgMap{Offset: offset, Size: dt.Size(), Kind: dt.Common().ReflectKind, Pieces: paramPieces, InReg: len(pieces) > 0})
+		args = append(args, ebpf.UProbeArgMap{
+			Offset: offset,
+			Size:   dt.Size(),
+			Kind:   dt.Common().ReflectKind,
+			Pieces: paramPieces,
+			InReg:  len(pieces) > 0,
+			Ret:    isret,
+		})
 	}
 
 	// Finally, set the uprobe on the function.
diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go
index e28e04e4..392d9f93 100644
--- a/pkg/proc/core/core.go
+++ b/pkg/proc/core/core.go
@@ -281,6 +281,14 @@ func (dbp *process) SetUProbe(fnName string, goidOffset int64, args []ebpf.UProb
 // StartCallInjection notifies the backend that we are about to inject a function call.
 func (p *process) StartCallInjection() (func(), error) { return func() {}, nil }
 
+func (dbp *process) EnableURetProbes() error {
+	panic("not implemented")
+}
+
+func (dbp *process) DisableURetProbes() error {
+	panic("not implemented")
+}
+
 // ReadMemory will return memory from the core file at the specified location and put the
 // read memory into `data`, returning the length read, and returning an error if
 // the length read is shorter than the length of the `data` buffer.
diff --git a/pkg/proc/i386_arch.go b/pkg/proc/i386_arch.go
index f2ad59aa..6e0d485c 100644
--- a/pkg/proc/i386_arch.go
+++ b/pkg/proc/i386_arch.go
@@ -117,6 +117,10 @@ func i386FixFrameUnwindContext(fctxt *frame.FrameContext, pc uint64, bi *BinaryI
 // SwitchStack will use the current frame to determine if it's time to
 func i386SwitchStack(it *stackIterator, _ *op.DwarfRegisters) bool {
 	if it.frame.Current.Fn == nil {
+		if it.systemstack && it.g != nil && it.top {
+			it.switchToGoroutineStack()
+			return true
+		}
 		return false
 	}
 	switch it.frame.Current.Fn.Name {
diff --git a/pkg/proc/internal/ebpf/context.go b/pkg/proc/internal/ebpf/context.go
index c457dbc2..695b8105 100644
--- a/pkg/proc/internal/ebpf/context.go
+++ b/pkg/proc/internal/ebpf/context.go
@@ -13,6 +13,7 @@ type UProbeArgMap struct {
 	Kind   reflect.Kind // Kind of variable.
 	Pieces []int        // Pieces of the variables as stored in registers.
 	InReg  bool         // True if this param is contained in a register.
+	Ret    bool         // True if this param is a return value.
 }
 
 type RawUProbeParam struct {
@@ -26,7 +27,8 @@ type RawUProbeParam struct {
 }
 
 type RawUProbeParams struct {
-	FnAddr      int
-	GoroutineID int
-	InputParams []*RawUProbeParam
+	FnAddr       int
+	GoroutineID  int
+	InputParams  []*RawUProbeParam
+	ReturnParams []*RawUProbeParam
 }
diff --git a/pkg/proc/internal/ebpf/helpers.go b/pkg/proc/internal/ebpf/helpers.go
index 2936f2ca..0a0c66d0 100644
--- a/pkg/proc/internal/ebpf/helpers.go
+++ b/pkg/proc/internal/ebpf/helpers.go
@@ -6,6 +6,7 @@ package ebpf
 // #include "./trace_probe/function_vals.bpf.h"
 import "C"
 import (
+	"debug/elf"
 	_ "embed"
 	"encoding/binary"
 	"errors"
@@ -51,11 +52,11 @@ func (ctx *EBPFContext) AttachUprobe(pid int, name string, offset uint32) error
 	return err
 }
 
-func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64) error {
+func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64, isret bool) error {
 	if ctx.bpfArgMap == nil {
 		return errors.New("eBPF map not loaded")
 	}
-	params := createFunctionParameterList(key, goidOffset, args)
+	params := createFunctionParameterList(key, goidOffset, args, isret)
 	params.g_addr_offset = C.longlong(gAddrOffset)
 	return ctx.bpfArgMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&params))
 }
@@ -82,7 +83,7 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 	var ctx EBPFContext
 	var err error
 
-	ctx.bpfModule, err = bpf.NewModuleFromBuffer(TraceProbeBytes, "trace.o")
+	ctx.bpfModule, err = bpf.NewModuleFromBuffer(TraceProbeBytes, "trace_probe/trace.o")
 	if err != nil {
 		return nil, err
 	}
@@ -114,7 +115,7 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 				return
 			}
 
-			parsed := ParseFunctionParameterList(b)
+			parsed := parseFunctionParameterList(b)
 
 			ctx.m.Lock()
 			ctx.parsedBpfEvents = append(ctx.parsedBpfEvents, parsed)
@@ -125,7 +126,7 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 	return &ctx, nil
 }
 
-func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
+func parseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
 	params := (*C.function_parameter_list_t)(unsafe.Pointer(&rawParamBytes[0]))
 
 	defer runtime.KeepAlive(params) // Ensure the param is not garbage collected.
@@ -134,10 +135,10 @@ func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
 	rawParams.FnAddr = int(params.fn_addr)
 	rawParams.GoroutineID = int(params.goroutine_id)
 
-	for i := 0; i < int(params.n_parameters); i++ {
+	parseParam := func(param C.function_parameter_t) *RawUProbeParam {
 		iparam := &RawUProbeParam{}
 		data := make([]byte, 0x60)
-		ret := params.params[i]
+		ret := param
 		iparam.Kind = reflect.Kind(ret.kind)
 
 		val := C.GoBytes(unsafe.Pointer(&ret.val), C.int(ret.size))
@@ -161,22 +162,30 @@ func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
 			iparam.Base = FakeAddressBase + 0x30
 			iparam.Len = int64(strLen)
 		}
+		return iparam
+	}
 
-		rawParams.InputParams = append(rawParams.InputParams, iparam)
+	for i := 0; i < int(params.n_parameters); i++ {
+		rawParams.InputParams = append(rawParams.InputParams, parseParam(params.params[i]))
+	}
+	for i := 0; i < int(params.n_ret_parameters); i++ {
+		rawParams.ReturnParams = append(rawParams.ReturnParams, parseParam(params.ret_params[i]))
 	}
 
 	return rawParams
 }
 
-func createFunctionParameterList(entry uint64, goidOffset int64, args []UProbeArgMap) C.function_parameter_list_t {
+func createFunctionParameterList(entry uint64, goidOffset int64, args []UProbeArgMap, isret bool) C.function_parameter_list_t {
 	var params C.function_parameter_list_t
 	params.goid_offset = C.uint(goidOffset)
-	params.n_parameters = C.uint(len(args))
 	params.fn_addr = C.uint(entry)
-	for i, arg := range args {
+	params.is_ret = C.bool(isret)
+	params.n_parameters = C.uint(0)
+	params.n_ret_parameters = C.uint(0)
+	for _, arg := range args {
 		var param C.function_parameter_t
 		param.size = C.uint(arg.Size)
-		param.offset = C.uint(arg.Offset)
+		param.offset = C.int(arg.Offset)
 		param.kind = C.uint(arg.Kind)
 		if arg.InReg {
 			param.in_reg = true
@@ -188,7 +197,40 @@ func createFunctionParameterList(entry uint64, goidOffset int64, args []UProbeAr
 				param.reg_nums[i] = C.int(arg.Pieces[i])
 			}
 		}
-		params.params[i] = param
+		if !arg.Ret {
+			params.params[params.n_parameters] = param
+			params.n_parameters++
+		} else {
+			params.ret_params[params.n_ret_parameters] = param
+			params.n_ret_parameters++
+		}
 	}
 	return params
 }
+
+func AddressToOffset(f *elf.File, addr uint64) (uint32, error) {
+	sectionsToSearchForSymbol := []*elf.Section{}
+
+	for i := range f.Sections {
+		if f.Sections[i].Flags == elf.SHF_ALLOC+elf.SHF_EXECINSTR {
+			sectionsToSearchForSymbol = append(sectionsToSearchForSymbol, f.Sections[i])
+		}
+	}
+
+	var executableSection *elf.Section
+
+	// Find what section the symbol is in by checking the executable section's
+	// addr space.
+	for m := range sectionsToSearchForSymbol {
+		if addr > sectionsToSearchForSymbol[m].Addr &&
+			addr < sectionsToSearchForSymbol[m].Addr+sectionsToSearchForSymbol[m].Size {
+			executableSection = sectionsToSearchForSymbol[m]
+		}
+	}
+
+	if executableSection == nil {
+		return 0, errors.New("could not find symbol in executable sections of binary")
+	}
+
+	return uint32(addr - executableSection.Addr + executableSection.Offset), nil
+}
diff --git a/pkg/proc/internal/ebpf/helpers_disabled.go b/pkg/proc/internal/ebpf/helpers_disabled.go
index 8ea90950..1dc274ea 100644
--- a/pkg/proc/internal/ebpf/helpers_disabled.go
+++ b/pkg/proc/internal/ebpf/helpers_disabled.go
@@ -4,6 +4,7 @@
 package ebpf
 
 import (
+	"debug/elf"
 	"errors"
 )
 
@@ -18,7 +19,11 @@ func (ctx *EBPFContext) AttachUprobe(pid int, name string, offset uint32) error
 	return errors.New("eBPF is disabled")
 }
 
-func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64) error {
+func (ctx *EBPFContext) AttachURetprobe(pid int, name string, offset uint32) error {
+	return errors.New("eBPF is disabled")
+}
+
+func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64, isret bool) error {
 	return errors.New("eBPF is disabled")
 }
 
@@ -34,6 +39,6 @@ func LoadEBPFTracingProgram() (*EBPFContext, error) {
 	return nil, errors.New("eBPF disabled")
 }
 
-func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
-	return RawUProbeParams{}
+func AddressToOffset(f *elf.File, addr uint64) (uint32, error) {
+	return 0, errors.New("eBPF disabled")
 }
diff --git a/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h b/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h
index fec16670..0df7b9b7 100644
--- a/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h
+++ b/pkg/proc/internal/ebpf/trace_probe/function_vals.bpf.h
@@ -8,7 +8,7 @@ typedef struct function_parameter {
       unsigned int size; 
 
       // Offset from stack pointer. This should only be set from the Go side.
-      unsigned int offset; 
+      int offset;
 
       // If true, the parameter is passed in a register.
       bool in_reg;      
@@ -20,7 +20,7 @@ typedef struct function_parameter {
       int reg_nums[6]; 
 
       // The following are filled in by the eBPF program.
-      unsigned int daddr;   // Data address.
+      size_t daddr;   // Data address.
       char val[0x30];       // Value of the parameter.
       char deref_val[0x30]; // Dereference value of the parameter.
 } function_parameter_t;
@@ -33,6 +33,11 @@ typedef struct function_parameter_list {
       int goroutine_id;
 
       unsigned int fn_addr;
+      bool is_ret;
+
       unsigned int n_parameters;          // number of parameters.
       function_parameter_t params[6];     // list of parameters.
+
+      unsigned int n_ret_parameters;      // number of return parameters.
+      function_parameter_t ret_params[6]; // list of return parameters.
 } function_parameter_list_t;
diff --git a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c
index 505981c2..39fdff22 100644
--- a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c
+++ b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.c
@@ -124,9 +124,9 @@ int parse_param(struct pt_regs *ctx, function_parameter_t *param) {
     // a slice we will need some further processing below.
     int ret = 0;
     if (param->in_reg) {
-        parse_param_registers(ctx, param);
+        ret = parse_param_registers(ctx, param);
     } else {
-        parse_param_stack(ctx, param);
+        ret = parse_param_stack(ctx, param);
     }
     if (ret != 0) {
         return ret;
@@ -176,11 +176,30 @@ int get_goroutine_id(function_parameter_list_t *parsed_args) {
     return 1;
 }
 
+__always_inline
+void parse_params(struct pt_regs *ctx, unsigned int n_params, function_parameter_t params[6]) {
+    // Since we cannot loop in eBPF programs let's take adavantage of the
+    // fact that in C switch cases will pass through automatically.
+    switch (n_params) {
+        case 6:
+            parse_param(ctx, &params[5]);
+        case 5:
+            parse_param(ctx, &params[4]);
+        case 4:
+            parse_param(ctx, &params[3]);
+        case 3:
+            parse_param(ctx, &params[2]);
+        case 2:
+            parse_param(ctx, &params[1]);
+        case 1:
+            parse_param(ctx, &params[0]);
+    }
+}
+
 SEC("uprobe/dlv_trace")
 int uprobe__dlv_trace(struct pt_regs *ctx) {
     function_parameter_list_t *args;
     function_parameter_list_t *parsed_args;
-    function_parameter_t param;
     uint64_t key = ctx->ip;
 
     args = bpf_map_lookup_elem(&arg_map, &key);
@@ -192,28 +211,32 @@ int uprobe__dlv_trace(struct pt_regs *ctx) {
     if (!parsed_args) {
         return 1;
     }
-    memcpy(parsed_args, args, sizeof(function_parameter_list_t));
+
+    // Initialize the parsed_args struct.
+    parsed_args->goid_offset = args->goid_offset;
+    parsed_args->g_addr_offset = args->g_addr_offset;
+    parsed_args->goroutine_id = args->goroutine_id;
+    parsed_args->fn_addr = args->fn_addr;
+    parsed_args->n_parameters = args->n_parameters;
+    parsed_args->n_ret_parameters = args->n_ret_parameters;
+    memcpy(parsed_args->params, args->params, sizeof(args->params));
+    memcpy(parsed_args->ret_params, args->ret_params, sizeof(args->ret_params));
 
     if (!get_goroutine_id(parsed_args)) {
         bpf_ringbuf_discard(parsed_args, 0);
         return 1;
     }
 
-    // Since we cannot loop in eBPF programs let's take adavantage of the
-    // fact that in C switch cases will pass through automatically.
-    switch (args->n_parameters) {
-        case 6:
-            parse_param(ctx, &parsed_args->params[5]);
-        case 5:
-            parse_param(ctx, &parsed_args->params[4]);
-        case 4:
-            parse_param(ctx, &parsed_args->params[3]);
-        case 3:
-            parse_param(ctx, &parsed_args->params[2]);
-        case 2:
-            parse_param(ctx, &parsed_args->params[1]);
-        case 1:
-            parse_param(ctx, &parsed_args->params[0]);
+    if (!args->is_ret) {
+        // In uprobe at function entry.
+
+        // Parse input parameters.
+        parse_params(ctx, args->n_parameters, parsed_args->params);
+    } else {
+        // We are now stopped at the RET instruction for this function.
+
+        // Parse output parameters.
+        parse_params(ctx, args->n_ret_parameters, parsed_args->ret_params);
     }
 
     bpf_ringbuf_submit(parsed_args, 0);
diff --git a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h
index b46d2025..971db59f 100644
--- a/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h
+++ b/pkg/proc/internal/ebpf/trace_probe/trace.bpf.h
@@ -16,6 +16,7 @@ struct {
    __uint(max_entries, BPF_MAX_VAR_SIZ);
 } heap SEC(".maps");
 
+// Map which uses instruction address as key and function parameter info as the value.
 struct {
     __uint(max_entries, 42);
     __uint(type, BPF_MAP_TYPE_HASH);
diff --git a/pkg/proc/native/hwbreak_other.go b/pkg/proc/native/hwbreak_other.go
index d841cd9e..548f3336 100644
--- a/pkg/proc/native/hwbreak_other.go
+++ b/pkg/proc/native/hwbreak_other.go
@@ -1,4 +1,5 @@
-// +build !amd64
+//go:build (linux && 386) || (darwin && arm64)
+// +build linux,386 darwin,arm64
 
 package native
 
diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go
index 98734246..6b5613a2 100644
--- a/pkg/proc/native/proc_linux.go
+++ b/pkg/proc/native/proc_linux.go
@@ -3,6 +3,7 @@ package native
 import (
 	"bufio"
 	"bytes"
+	"debug/elf"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -708,13 +709,18 @@ func (dbp *nativeProcess) EntryPoint() (uint64, error) {
 func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf.UProbeArgMap) error {
 	// Lazily load and initialize the BPF program upon request to set a uprobe.
 	if dbp.os.ebpf == nil {
-		dbp.os.ebpf, _ = ebpf.LoadEBPFTracingProgram()
+		var err error
+		dbp.os.ebpf, err = ebpf.LoadEBPFTracingProgram()
+		if err != nil {
+			return err
+		}
 	}
 
-	// We only allow up to 6 args for a BPF probe.
+	// We only allow up to 12 args for a BPF probe.
+	// 6 inputs + 6 outputs.
 	// Return early if we have more.
-	if len(args) > 6 {
-		return errors.New("too many arguments in traced function, max is 6")
+	if len(args) > 12 {
+		return errors.New("too many arguments in traced function, max is 12 input+return")
 	}
 
 	fn, ok := dbp.bi.LookupFunc[fnName]
@@ -723,7 +729,7 @@ func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf
 	}
 
 	key := fn.Entry
-	err := dbp.os.ebpf.UpdateArgMap(key, goidOffset, args, dbp.BinInfo().GStructOffset())
+	err := dbp.os.ebpf.UpdateArgMap(key, goidOffset, args, dbp.BinInfo().GStructOffset(), false)
 	if err != nil {
 		return err
 	}
@@ -733,6 +739,49 @@ func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf
 	if err != nil {
 		return err
 	}
+
+	// First attach a uprobe at all return addresses. We do this instead of using a uretprobe
+	// for two reasons:
+	// 1. uretprobes do not play well with Go
+	// 2. uretprobes seem to not restore the function return addr on the stack when removed, destroying any
+	//    kind of workaround we could come up with.
+	// TODO(derekparker): this whole thing could likely be optimized a bit.
+	img := dbp.BinInfo().PCToImage(fn.Entry)
+	f, err := elf.Open(img.Path)
+	if err != nil {
+		return fmt.Errorf("could not open elf file to resolve symbol offset: %w", err)
+	}
+
+	var regs proc.Registers
+	mem := dbp.Memory()
+	regs, _ = dbp.memthread.Registers()
+	instructions, err := proc.Disassemble(mem, regs, &proc.BreakpointMap{}, dbp.BinInfo(), fn.Entry, fn.End)
+	if err != nil {
+		return err
+	}
+
+	var addrs []uint64
+	for _, instruction := range instructions {
+		if instruction.IsRet() {
+			addrs = append(addrs, instruction.Loc.PC)
+		}
+	}
+	addrs = append(addrs, proc.FindDeferReturnCalls(instructions)...)
+	for _, addr := range addrs {
+		err := dbp.os.ebpf.UpdateArgMap(addr, goidOffset, args, dbp.BinInfo().GStructOffset(), true)
+		if err != nil {
+			return err
+		}
+		off, err := ebpf.AddressToOffset(f, addr)
+		if err != nil {
+			return err
+		}
+		err = dbp.os.ebpf.AttachUprobe(dbp.Pid(), debugname, off)
+		if err != nil {
+			return err
+		}
+	}
+
 	return dbp.os.ebpf.AttachUprobe(dbp.Pid(), debugname, offset)
 }
 
diff --git a/pkg/proc/native/threads_linux_arm64.go b/pkg/proc/native/threads_linux_arm64.go
index f2f4d290..ea0e7642 100644
--- a/pkg/proc/native/threads_linux_arm64.go
+++ b/pkg/proc/native/threads_linux_arm64.go
@@ -2,6 +2,7 @@ package native
 
 import (
 	"debug/elf"
+	"errors"
 	"fmt"
 	"syscall"
 	"unsafe"
@@ -42,3 +43,148 @@ func (t *nativeThread) restoreRegisters(savedRegs proc.Registers) error {
 	}
 	return restoreRegistersErr
 }
+
+const (
+	_MAX_ARM64_WATCH = 16
+	_NT_ARM_HW_WATCH = 0x403
+	_TRAP_HWBKPT     = 0x4
+)
+
+type watchpointState struct {
+	num      uint8
+	debugVer uint8
+	words    []uint64
+}
+
+func (wpstate *watchpointState) set(idx uint8, addr, ctrl uint64) {
+	wpstate.words[1+idx*2] = addr
+	wpstate.words[1+idx*2+1] = ctrl
+}
+
+// getWatchpoints reads the NT_ARM_HW_WATCH ptrace register set.
+// The format of this register set is described by user_hwdebug_state in
+// arch/arm64/include/uapi/asm/ptrace.h.
+// It consists of one 64bit word containing:
+//  * 1byte number of watchpoints
+//  * 1byte debug architecture version (the 4 least significant bits of ID_AA64DFR0_EL1)
+//  * 6bytes padding
+// Followed by 2 64bit words for each watchpoint, up to a maximum of 16
+// watchpoints. The first word contains the address at which the watchpoint
+// is set (DBGWVRn_EL1), the second word is the control register for the
+// watchpoint (DBGWCRn_EL1), as described by
+// ARM - Architecture Reference Manual Armv8, for A-profile architectures
+// section D13.3.11
+// where only the BAS, LSC, PAC and E fields are accessible.
+func (t *nativeThread) getWatchpoints() (*watchpointState, error) {
+	words := make([]uint64, _MAX_ARM64_WATCH*2+1)
+	iov := sys.Iovec{Base: (*byte)(unsafe.Pointer(&words[0])), Len: uint64(len(words)) * uint64(unsafe.Sizeof(words[0]))}
+	var err error
+	t.dbp.execPtraceFunc(func() {
+		_, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_GETREGSET, uintptr(t.ID), _NT_ARM_HW_WATCH, uintptr(unsafe.Pointer(&iov)), 0, 0)
+	})
+	if err != syscall.Errno(0) {
+		return nil, err
+	}
+	wpstate := &watchpointState{num: uint8(words[0] & 0xff), debugVer: uint8((words[0] >> 8) & 0xff), words: words}
+	if wpstate.num > _MAX_ARM64_WATCH {
+		// According to the specification this should never be more than 16 but
+		// the code here will not work if this limit ever gets relaxed.
+		wpstate.num = _MAX_ARM64_WATCH
+	}
+	// remove the watchpoint registers that don't exist from the words slice so
+	// that we won't try later to write them (which would cause an ENOSPC
+	// error).
+	wpstate.words = wpstate.words[:wpstate.num*2+1]
+	return wpstate, nil
+}
+
+// setWatchpoints saves the watchpoint state of the given thread.
+// See (*nativeThread).getWatchpoints for a description of how this works.
+func (t *nativeThread) setWatchpoints(wpstate *watchpointState) error {
+	iov := sys.Iovec{Base: (*byte)(unsafe.Pointer(&(wpstate.words[0]))), Len: uint64(len(wpstate.words)) * uint64(unsafe.Sizeof(wpstate.words[0]))}
+	var err error
+	t.dbp.execPtraceFunc(func() {
+		_, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_SETREGSET, uintptr(t.ID), _NT_ARM_HW_WATCH, uintptr(unsafe.Pointer(&iov)), 0, 0)
+	})
+	if err != syscall.Errno(0) {
+		return err
+	}
+	return nil
+}
+
+type ptraceSiginfoArm64 struct {
+	signo uint32
+	errno uint32
+	code  uint32
+	addr  uint64    // only valid if Signo is SIGTRAP, SIGFPE, SIGILL, SIGBUS or SIGEMT
+	pad   [128]byte // the total size of siginfo_t on ARM64 is 128 bytes so this is more than enough padding for all the fields we don't care about
+}
+
+func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
+	var siginfo ptraceSiginfoArm64
+	var err error
+	t.dbp.execPtraceFunc(func() {
+		_, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_GETSIGINFO, uintptr(t.ID), 0, uintptr(unsafe.Pointer(&siginfo)), 0, 0)
+	})
+	if err != syscall.Errno(0) {
+		return nil, err
+	}
+	if siginfo.signo != uint32(sys.SIGTRAP) || (siginfo.code&0xffff) != _TRAP_HWBKPT {
+		return nil, nil
+	}
+
+	for _, bp := range t.dbp.Breakpoints().M {
+		if bp.WatchType != 0 && siginfo.addr >= bp.Addr && siginfo.addr < bp.Addr+uint64(bp.WatchType.Size()) {
+			return bp, nil
+		}
+	}
+
+	return nil, fmt.Errorf("could not find hardware breakpoint for address %#x", siginfo.addr)
+}
+
+func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
+	wpstate, err := t.getWatchpoints()
+	if err != nil {
+		return err
+	}
+	if idx >= wpstate.num {
+		return errors.New("hardware breakpoints exhausted")
+	}
+
+	const (
+		readBreakpoint  = 0x1
+		writeBreakpoint = 0x2
+		lenBitOffset    = 5
+		typeBitOffset   = 3
+		privBitOffset   = 1
+	)
+
+	var typ uint64
+	if wtype.Read() {
+		typ |= readBreakpoint
+	}
+	if wtype.Write() {
+		typ |= writeBreakpoint
+	}
+
+	len := uint64((1 << wtype.Size()) - 1) // arm wants the length expressed as address bitmask
+
+	priv := uint64(3)
+
+	ctrl := (len << lenBitOffset) | (typ << typeBitOffset) | (priv << privBitOffset) | 1
+	wpstate.set(idx, addr, ctrl)
+
+	return t.setWatchpoints(wpstate)
+}
+
+func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
+	wpstate, err := t.getWatchpoints()
+	if err != nil {
+		return err
+	}
+	if idx >= wpstate.num {
+		return errors.New("hardware breakpoints exhausted")
+	}
+	wpstate.set(idx, 0, 0)
+	return t.setWatchpoints(wpstate)
+}
diff --git a/pkg/proc/target.go b/pkg/proc/target.go
index 33485d87..dc503e19 100644
--- a/pkg/proc/target.go
+++ b/pkg/proc/target.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/go-delve/delve/pkg/dwarf/op"
 	"github.com/go-delve/delve/pkg/goversion"
+	"github.com/go-delve/delve/pkg/proc/internal/ebpf"
 )
 
 var (
@@ -401,35 +402,45 @@ func (t *Target) CurrentThread() Thread {
 }
 
 type UProbeTraceResult struct {
-	FnAddr      int
-	GoroutineID int
-	InputParams []*Variable
+	FnAddr       int
+	GoroutineID  int
+	InputParams  []*Variable
+	ReturnParams []*Variable
 }
 
 func (t *Target) GetBufferedTracepoints() []*UProbeTraceResult {
 	var results []*UProbeTraceResult
 	tracepoints := t.proc.GetBufferedTracepoints()
+	convertInputParamToVariable := func(ip *ebpf.RawUProbeParam) *Variable {
+		v := &Variable{}
+		v.RealType = ip.RealType
+		v.Len = ip.Len
+		v.Base = ip.Base
+		v.Addr = ip.Addr
+		v.Kind = ip.Kind
+
+		cachedMem := CreateLoadedCachedMemory(ip.Data)
+		compMem, _ := CreateCompositeMemory(cachedMem, t.BinInfo().Arch, op.DwarfRegisters{}, ip.Pieces)
+		v.mem = compMem
+
+		// Load the value here so that we don't have to export
+		// loadValue outside of proc.
+		v.loadValue(loadFullValue)
+
+		return v
+	}
 	for _, tp := range tracepoints {
 		r := &UProbeTraceResult{}
 		r.FnAddr = tp.FnAddr
 		r.GoroutineID = tp.GoroutineID
 		for _, ip := range tp.InputParams {
-			v := &Variable{}
-			v.RealType = ip.RealType
-			v.Len = ip.Len
-			v.Base = ip.Base
-			v.Addr = ip.Addr
-			v.Kind = ip.Kind
-
-			cachedMem := CreateLoadedCachedMemory(ip.Data)
-			compMem, _ := CreateCompositeMemory(cachedMem, t.BinInfo().Arch, op.DwarfRegisters{}, ip.Pieces)
-			v.mem = compMem
-
-			// Load the value here so that we don't have to export
-			// loadValue outside of proc.
-			v.loadValue(loadFullValue)
+			v := convertInputParamToVariable(ip)
 			r.InputParams = append(r.InputParams, v)
 		}
+		for _, ip := range tp.ReturnParams {
+			v := convertInputParamToVariable(ip)
+			r.ReturnParams = append(r.ReturnParams, v)
+		}
 		results = append(results, r)
 	}
 	return results
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index 028bd13f..5dfe5224 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -26,6 +26,7 @@ import (
 	"time"
 
 	"github.com/cosiner/argv"
+	"github.com/go-delve/delve/pkg/config"
 	"github.com/go-delve/delve/pkg/locspec"
 	"github.com/go-delve/delve/pkg/terminal/colorize"
 	"github.com/go-delve/delve/service"
@@ -985,16 +986,8 @@ func selectedGID(state *api.DebuggerState) int {
 	return state.SelectedGoroutine.ID
 }
 
-func split2PartsBySpace(s string) []string {
-	v := strings.SplitN(s, " ", 2)
-	for i := range v {
-		v[i] = strings.TrimSpace(v[i])
-	}
-	return v
-}
-
 func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
-	args := split2PartsBySpace(argstr)
+	args := config.Split2PartsBySpace(argstr)
 
 	if ctx.Prefix == onPrefix {
 		if len(args) != 1 || args[0] != "" {
@@ -1043,7 +1036,7 @@ func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, directi
 			return errors.New("not enough arguments")
 		}
 	} else {
-		args := split2PartsBySpace(argstr)
+		args := config.Split2PartsBySpace(argstr)
 		var err error
 		if frame, err = strconv.Atoi(args[0]); err != nil {
 			return err
@@ -1258,7 +1251,7 @@ func restart(t *Term, ctx callContext, args string) error {
 }
 
 func restartRecorded(t *Term, ctx callContext, args string) error {
-	v := split2PartsBySpace(args)
+	v := config.Split2PartsBySpace(args)
 
 	rerecord := false
 	resetArgs := false
@@ -1810,7 +1803,7 @@ func formatBreakpointAttrs(prefix string, bp *api.Breakpoint, includeTrace bool)
 }
 
 func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]*api.Breakpoint, error) {
-	args := split2PartsBySpace(argstr)
+	args := config.Split2PartsBySpace(argstr)
 
 	requestedBp := &api.Breakpoint{}
 	spec := ""
@@ -2212,7 +2205,7 @@ func types(t *Term, ctx callContext, args string) error {
 }
 
 func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) {
-	if v := split2PartsBySpace(args); len(v) >= 1 && v[0] == "-v" {
+	if v := config.Split2PartsBySpace(args); len(v) >= 1 && v[0] == "-v" {
 		if len(v) == 2 {
 			return v[1], t.loadConfig()
 		} else {
@@ -2486,7 +2479,7 @@ func disassCommand(t *Term, ctx callContext, args string) error {
 	var cmd, rest string
 
 	if args != "" {
-		argv := split2PartsBySpace(args)
+		argv := config.Split2PartsBySpace(args)
 		if len(argv) != 2 {
 			return errDisasmUsage
 		}
@@ -2517,7 +2510,7 @@ func disassCommand(t *Term, ctx callContext, args string) error {
 		}
 		disasm, disasmErr = t.client.DisassemblePC(ctx.Scope, locs[0].PC, flavor)
 	case "-a":
-		v := split2PartsBySpace(rest)
+		v := config.Split2PartsBySpace(rest)
 		if len(v) != 2 {
 			return errDisasmUsage
 		}
@@ -2839,7 +2832,7 @@ func getBreakpointByIDOrName(t *Term, arg string) (*api.Breakpoint, error) {
 }
 
 func (c *Commands) onCmd(t *Term, ctx callContext, argstr string) error {
-	args := split2PartsBySpace(argstr)
+	args := config.Split2PartsBySpace(argstr)
 
 	if len(args) < 2 {
 		return errors.New("not enough arguments")
@@ -2916,7 +2909,7 @@ func (c *Commands) parseBreakpointAttrs(t *Term, ctx callContext, r io.Reader) e
 }
 
 func conditionCmd(t *Term, ctx callContext, argstr string) error {
-	args := split2PartsBySpace(argstr)
+	args := config.Split2PartsBySpace(argstr)
 
 	if len(args) < 2 {
 		return fmt.Errorf("not enough arguments")
@@ -2930,7 +2923,7 @@ func conditionCmd(t *Term, ctx callContext, argstr string) error {
 			return nil
 		}
 
-		args = split2PartsBySpace(args[1])
+		args = config.Split2PartsBySpace(args[1])
 		if len(args) < 2 {
 			return fmt.Errorf("not enough arguments")
 		}
diff --git a/pkg/terminal/config.go b/pkg/terminal/config.go
index 2d1ef51e..a005c51a 100644
--- a/pkg/terminal/config.go
+++ b/pkg/terminal/config.go
@@ -4,8 +4,6 @@ import (
 	"fmt"
 	"os"
 	"reflect"
-	"strconv"
-	"strings"
 	"text/tabwriter"
 
 	"github.com/go-delve/delve/pkg/config"
@@ -32,80 +30,15 @@ func configureCmd(t *Term, ctx callContext, args string) error {
 	}
 }
 
-type configureIterator struct {
-	cfgValue reflect.Value
-	cfgType  reflect.Type
-	i        int
-}
-
-func iterateConfiguration(conf *config.Config) *configureIterator {
-	cfgValue := reflect.ValueOf(conf).Elem()
-	cfgType := cfgValue.Type()
-
-	return &configureIterator{cfgValue, cfgType, -1}
-}
-
-func (it *configureIterator) Next() bool {
-	it.i++
-	return it.i < it.cfgValue.NumField()
-}
-
-func (it *configureIterator) Field() (name string, field reflect.Value) {
-	name = it.cfgType.Field(it.i).Tag.Get("yaml")
-	if comma := strings.Index(name, ","); comma >= 0 {
-		name = name[:comma]
-	}
-	field = it.cfgValue.Field(it.i)
-	return
-}
-
-func configureFindFieldByName(conf *config.Config, name string) reflect.Value {
-	it := iterateConfiguration(conf)
-	for it.Next() {
-		fieldName, field := it.Field()
-		if fieldName == name {
-			return field
-		}
-	}
-	return reflect.ValueOf(nil)
-}
-
 func configureList(t *Term) error {
 	w := new(tabwriter.Writer)
 	w.Init(os.Stdout, 0, 8, 1, ' ', 0)
-
-	it := iterateConfiguration(t.conf)
-	for it.Next() {
-		fieldName, field := it.Field()
-		if fieldName == "" {
-			continue
-		}
-
-		switch field.Kind() {
-		case reflect.Interface:
-			switch field := field.Interface().(type) {
-			case string:
-				fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
-			default:
-				fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
-			}
-		case reflect.Ptr:
-			if !field.IsNil() {
-				fmt.Fprintf(w, "%s\t%v\n", fieldName, field.Elem())
-			} else {
-				fmt.Fprintf(w, "%s\t<not defined>\n", fieldName)
-			}
-		case reflect.String:
-			fmt.Fprintf(w, "%s\t%q\n", fieldName, field)
-		default:
-			fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
-		}
-	}
+	config.ConfigureList(w, t.conf, "yaml")
 	return w.Flush()
 }
 
 func configureSet(t *Term, args string) error {
-	v := split2PartsBySpace(args)
+	v := config.Split2PartsBySpace(args)
 
 	cfgname := v[0]
 	var rest string
@@ -117,7 +50,7 @@ func configureSet(t *Term, args string) error {
 		return configureSetAlias(t, rest)
 	}
 
-	field := configureFindFieldByName(t.conf, cfgname)
+	field := config.ConfigureFindFieldByName(t.conf, cfgname, "yaml")
 	if !field.CanAddr() {
 		return fmt.Errorf("%q is not a configuration parameter", cfgname)
 	}
@@ -126,41 +59,7 @@ func configureSet(t *Term, args string) error {
 		return configureSetSubstitutePath(t, rest)
 	}
 
-	simpleArg := func(typ reflect.Type) (reflect.Value, error) {
-		switch typ.Kind() {
-		case reflect.Int:
-			n, err := strconv.Atoi(rest)
-			if err != nil {
-				return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number", cfgname)
-			}
-			if n < 0 {
-				return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number greater than zero", cfgname)
-			}
-			return reflect.ValueOf(&n), nil
-		case reflect.Bool:
-			v := rest == "true"
-			return reflect.ValueOf(&v), nil
-		case reflect.String:
-			return reflect.ValueOf(&rest), nil
-		default:
-			return reflect.ValueOf(nil), fmt.Errorf("unsupported type for configuration key %q", cfgname)
-		}
-	}
-
-	if field.Kind() == reflect.Ptr {
-		val, err := simpleArg(field.Type().Elem())
-		if err != nil {
-			return err
-		}
-		field.Set(val)
-	} else {
-		val, err := simpleArg(field.Type())
-		if err != nil {
-			return err
-		}
-		field.Set(val.Elem())
-	}
-	return nil
+	return config.ConfigureSetSimple(rest, cfgname, field)
 }
 
 func configureSetSubstitutePath(t *Term, rest string) error {
diff --git a/service/dap/command.go b/service/dap/command.go
new file mode 100644
index 00000000..0b08c569
--- /dev/null
+++ b/service/dap/command.go
@@ -0,0 +1,115 @@
+package dap
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/go-delve/delve/pkg/config"
+)
+
+func (s *Session) delveCmd(goid, frame int, cmdstr string) (string, error) {
+	vals := strings.SplitN(strings.TrimSpace(cmdstr), " ", 2)
+	cmdname := vals[0]
+	var args string
+	if len(vals) > 1 {
+		args = strings.TrimSpace(vals[1])
+	}
+	for _, cmd := range debugCommands(s) {
+		for _, alias := range cmd.aliases {
+			if alias == cmdname {
+				return cmd.cmdFn(goid, frame, args)
+			}
+		}
+	}
+	return "", errNoCmd
+}
+
+type cmdfunc func(goid, frame int, args string) (string, error)
+
+type command struct {
+	aliases []string
+	helpMsg string
+	cmdFn   cmdfunc
+}
+
+const (
+	msgHelp = `Prints the help message.
+	
+help [command]
+
+Type "help" followed by the name of a command for more information about it.`
+
+	msgConfig = `Changes configuration parameters.
+	
+	config -list
+	
+	Show all configuration parameters.
+	
+	config <parameter> <value>
+	
+	Changes the value of a configuration parameter.
+	
+	config substitutePath <from> <to>
+	config substitutePath <from>
+	
+	Adds or removes a path substitution rule.`
+)
+
+// debugCommands returns a list of commands with default commands defined.
+func debugCommands(s *Session) []command {
+	return []command{
+		{aliases: []string{"help", "h"}, cmdFn: s.helpMessage, helpMsg: msgHelp},
+		{aliases: []string{"config"}, cmdFn: s.evaluateConfig, helpMsg: msgConfig},
+	}
+}
+
+var errNoCmd = errors.New("command not available")
+
+func (s *Session) helpMessage(_, _ int, args string) (string, error) {
+	var buf bytes.Buffer
+	if args != "" {
+		for _, cmd := range debugCommands(s) {
+			for _, alias := range cmd.aliases {
+				if alias == args {
+					return cmd.helpMsg, nil
+				}
+			}
+		}
+		return "", errNoCmd
+	}
+
+	fmt.Fprintln(&buf, "The following commands are available:")
+
+	for _, cmd := range debugCommands(s) {
+		h := cmd.helpMsg
+		if idx := strings.Index(h, "\n"); idx >= 0 {
+			h = h[:idx]
+		}
+		if len(cmd.aliases) > 1 {
+			fmt.Fprintf(&buf, "    %s (alias: %s) \t %s\n", cmd.aliases[0], strings.Join(cmd.aliases[1:], " | "), h)
+		} else {
+			fmt.Fprintf(&buf, "    %s \t %s\n", cmd.aliases[0], h)
+		}
+	}
+
+	fmt.Fprintln(&buf)
+	fmt.Fprintln(&buf, "Type help followed by a command for full documentation.")
+	return buf.String(), nil
+}
+
+func (s *Session) evaluateConfig(_, _ int, expr string) (string, error) {
+	argv := config.Split2PartsBySpace(expr)
+	name := argv[0]
+	switch name {
+	case "-list":
+		return listConfig(&s.args), nil
+	default:
+		res, err := configureSet(&s.args, expr)
+		if err != nil {
+			return "", err
+		}
+		return res, nil
+	}
+}
diff --git a/service/dap/config.go b/service/dap/config.go
new file mode 100644
index 00000000..ec824211
--- /dev/null
+++ b/service/dap/config.go
@@ -0,0 +1,80 @@
+package dap
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/go-delve/delve/pkg/config"
+)
+
+func listConfig(args *launchAttachArgs) string {
+	var buf bytes.Buffer
+	config.ConfigureList(&buf, args, "cfgName")
+	return buf.String()
+}
+
+func configureSet(sargs *launchAttachArgs, args string) (string, error) {
+	v := config.Split2PartsBySpace(args)
+
+	cfgname := v[0]
+	var rest string
+	if len(v) == 2 {
+		rest = v[1]
+	}
+
+	field := config.ConfigureFindFieldByName(sargs, cfgname, "cfgName")
+	if !field.CanAddr() {
+		return "", fmt.Errorf("%q is not a configuration parameter", cfgname)
+	}
+
+	// If there were no arguments provided, just list the value.
+	if len(v) == 1 {
+		return config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
+	}
+
+	if cfgname == "substitutePath" {
+		err := configureSetSubstitutePath(sargs, rest)
+		if err != nil {
+			return "", err
+		}
+		// Print the updated client to server and server to client maps.
+		return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
+	}
+
+	err := config.ConfigureSetSimple(rest, cfgname, field)
+	if err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
+}
+
+func configureSetSubstitutePath(args *launchAttachArgs, rest string) error {
+	argv := config.SplitQuotedFields(rest, '"')
+	switch len(argv) {
+	case 1: // delete substitute-path rule
+		for i := range args.substitutePathClientToServer {
+			if args.substitutePathClientToServer[i][0] == argv[0] {
+				copy(args.substitutePathClientToServer[i:], args.substitutePathClientToServer[i+1:])
+				args.substitutePathClientToServer = args.substitutePathClientToServer[:len(args.substitutePathClientToServer)-1]
+				copy(args.substitutePathServerToClient[i:], args.substitutePathServerToClient[i+1:])
+				args.substitutePathServerToClient = args.substitutePathServerToClient[:len(args.substitutePathServerToClient)-1]
+				return nil
+			}
+		}
+		return fmt.Errorf("could not find rule for %q", argv[0])
+	case 2: // add substitute-path rule
+		for i := range args.substitutePathClientToServer {
+			if args.substitutePathClientToServer[i][0] == argv[0] {
+				args.substitutePathClientToServer[i][1] = argv[1]
+				args.substitutePathServerToClient[i][0] = argv[1]
+				return nil
+			}
+		}
+		args.substitutePathClientToServer = append(args.substitutePathClientToServer, [2]string{argv[0], argv[1]})
+		args.substitutePathServerToClient = append(args.substitutePathServerToClient, [2]string{argv[1], argv[0]})
+
+	default:
+		return fmt.Errorf("too many arguments to \"config substitute-path\"")
+	}
+	return nil
+}
diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go
index 2f58956a..09567776 100644
--- a/service/dap/error_ids.go
+++ b/service/dap/error_ids.go
@@ -25,6 +25,8 @@ const (
 	UnableToGetExceptionInfo   = 2011
 	UnableToSetVariable        = 2012
 	UnableToDisassemble        = 2013
+	UnableToListRegisters      = 2014
+	UnableToRunDlvCommand      = 2015
 	// Add more codes as we support more requests
 	NoDebugIsRunning  = 3000
 	DebuggeeIsRunning = 4000
diff --git a/service/dap/server.go b/service/dap/server.go
index 8d9fc7b5..cee513ae 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -166,9 +166,9 @@ type Config struct {
 
 	// log is used for structured logging.
 	log *logrus.Entry
-	// stopTriggered is closed when the server is Stop()-ed.
+	// StopTriggered is closed when the server is Stop()-ed.
 	// Can be used to safeguard against duplicate shutdown sequences.
-	stopTriggered chan struct{}
+	StopTriggered chan struct{}
 }
 
 type process struct {
@@ -178,16 +178,22 @@ type process struct {
 
 // launchAttachArgs captures arguments from launch/attach request that
 // impact handling of subsequent requests.
+// The fields with cfgName tag can be updated through an evaluation request.
 type launchAttachArgs struct {
 	// stopOnEntry is set to automatically stop the debugee after start.
 	stopOnEntry bool
-	// stackTraceDepth is the maximum length of the returned list of stack frames.
-	stackTraceDepth int
-	// showGlobalVariables indicates if global package variables should be loaded.
-	showGlobalVariables bool
+	// StackTraceDepth is the maximum length of the returned list of stack frames.
+	StackTraceDepth int `cfgName:"stackTraceDepth"`
+	// ShowGlobalVariables indicates if global package variables should be loaded.
+	ShowGlobalVariables bool `cfgName:"showGlobalVariables"`
+	// ShowRegisters indicates if register values should be loaded.
+	ShowRegisters bool `cfgName:"showRegisters"`
+	// HideSystemGoroutines indicates if system goroutines should be removed from threads
+	// responses.
+	HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"`
 	// substitutePathClientToServer indicates rules for converting file paths between client and debugger.
 	// These must be directory paths.
-	substitutePathClientToServer [][2]string
+	substitutePathClientToServer [][2]string `cfgName:"substitutePath"`
 	// substitutePathServerToClient indicates rules for converting file paths between debugger and client.
 	// These must be directory paths.
 	substitutePathServerToClient [][2]string
@@ -198,8 +204,10 @@ type launchAttachArgs struct {
 // in favor of default*Config variables defined in types.go.
 var defaultArgs = launchAttachArgs{
 	stopOnEntry:                  false,
-	stackTraceDepth:              50,
-	showGlobalVariables:          false,
+	StackTraceDepth:              50,
+	ShowGlobalVariables:          false,
+	HideSystemGoroutines:         false,
+	ShowRegisters:                false,
 	substitutePathClientToServer: [][2]string{},
 	substitutePathServerToClient: [][2]string{},
 }
@@ -270,7 +278,7 @@ func NewServer(config *service.Config) *Server {
 		config: &Config{
 			Config:        config,
 			log:           logger,
-			stopTriggered: make(chan struct{}),
+			StopTriggered: make(chan struct{}),
 		},
 		listener: config.Listener,
 	}
@@ -279,11 +287,14 @@ func NewServer(config *service.Config) *Server {
 // NewSession creates a new client session that can handle DAP traffic.
 // It takes an open connection and provides a Close() method to shut it
 // down when the DAP session disconnects or a connection error occurs.
-func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
+func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debugger) *Session {
 	if config.log == nil {
 		config.log = logflags.DAPLogger()
 	}
 	config.log.Debug("DAP connection started")
+	if config.StopTriggered == nil {
+		config.log.Fatal("Session must be configured with StopTriggered")
+	}
 	return &Session{
 		config:            config,
 		conn:              conn,
@@ -291,6 +302,7 @@ func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
 		variableHandles:   newVariablesHandlesMap(),
 		args:              defaultArgs,
 		exceptionErr:      nil,
+		debugger:          debugger,
 	}
 }
 
@@ -299,9 +311,11 @@ func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
 func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
 	s.args.stopOnEntry = args.StopOnEntry
 	if depth := args.StackTraceDepth; depth > 0 {
-		s.args.stackTraceDepth = depth
+		s.args.StackTraceDepth = depth
 	}
-	s.args.showGlobalVariables = args.ShowGlobalVariables
+	s.args.ShowGlobalVariables = args.ShowGlobalVariables
+	s.args.ShowRegisters = args.ShowRegisters
+	s.args.HideSystemGoroutines = args.HideSystemGoroutines
 	if paths := args.SubstitutePath; len(paths) > 0 {
 		clientToServer := make([][2]string, 0, len(paths))
 		serverToClient := make([][2]string, 0, len(paths))
@@ -319,11 +333,11 @@ func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
 // connection. It shuts down the underlying debugger and kills the target
 // process if it was launched by it or stops the noDebug process.
 // This method mustn't be called more than once.
-// stopTriggered notifies other goroutines that stop is in progreess.
+// StopTriggered notifies other goroutines that stop is in progreess.
 func (s *Server) Stop() {
 	s.config.log.Debug("DAP server stopping...")
 	defer s.config.log.Debug("DAP server stopped")
-	close(s.config.stopTriggered)
+	close(s.config.StopTriggered)
 
 	if s.listener != nil {
 		// If run goroutine is blocked on accept, this will unblock it.
@@ -340,6 +354,7 @@ func (s *Server) Stop() {
 }
 
 // Close closes the underlying debugger/process and connection.
+// May be called more than once.
 func (s *Session) Close() {
 	s.mu.Lock()
 	defer s.mu.Unlock()
@@ -347,19 +362,25 @@ func (s *Session) Close() {
 	if s.debugger != nil {
 		killProcess := s.config.Debugger.AttachPid == 0
 		s.stopDebugSession(killProcess)
-	} else {
+	} else if s.noDebugProcess != nil {
 		s.stopNoDebugProcess()
 	}
 	// The binary is no longer in use by the debugger. It is safe to remove it.
 	if s.binaryToRemove != "" {
 		gobuild.Remove(s.binaryToRemove)
+		s.binaryToRemove = "" // avoid error printed on duplicate removal
 	}
 	// Close client connection last, so other shutdown stages
 	// can send client notifications.
-	// Unless Stop() was called after read loop in serveDAPCodec()
+	// Unless Stop() was called after read loop in ServeDAPCodec()
 	// returned, this will result in a closed connection error
 	// on next read, breaking out the read loop andd
 	// allowing the run goroutinee to exit.
+	// This connection is closed here and in serveDAPCodec().
+	// If this was a forced shutdown, external stop logic can close this first.
+	// If this was a client loop exit (on error or disconnect), serveDAPCodec()
+	// will be first.
+	// Duplicate close calls return an error, but are not fatal.
 	_ = s.conn.Close()
 }
 
@@ -372,14 +393,8 @@ func (s *Session) Close() {
 // and is currently only called from the run goroutine.
 func (c *Config) triggerServerStop() {
 	// Avoid accidentally closing the channel twice and causing a panic, when
-	// this function is called more than once. For example, we could have the
-	// following sequence of events:
-	// -- run goroutine: calls onDisconnectRequest()
-	// -- run goroutine: calls triggerServerStop()
-	// -- main goroutine: calls Stop()
-	// -- main goroutine: Stop() closes client connection (or client closed it)
-	// -- run goroutine: serveDAPCodec() gets "closed network connection"
-	// -- run goroutine: serveDAPCodec() returns and calls triggerServerStop()
+	// this function is called more than once because stop was triggered
+	// by multiple conditions simultenously.
 	if c.DisconnectChan != nil {
 		close(c.DisconnectChan)
 		c.DisconnectChan = nil
@@ -409,7 +424,7 @@ func (s *Server) Run() {
 		conn, err := s.listener.Accept() // listener is closed in Stop()
 		if err != nil {
 			select {
-			case <-s.config.stopTriggered:
+			case <-s.config.StopTriggered:
 			default:
 				s.config.log.Errorf("Error accepting client connection: %s\n", err)
 				s.config.triggerServerStop()
@@ -429,9 +444,9 @@ func (s *Server) Run() {
 
 func (s *Server) runSession(conn io.ReadWriteCloser) {
 	s.sessionMu.Lock()
-	s.session = NewSession(conn, s.config) // closed in Stop()
+	s.session = NewSession(conn, s.config, nil) // closed in Stop()
 	s.sessionMu.Unlock()
-	s.session.serveDAPCodec()
+	s.session.ServeDAPCodec()
 }
 
 // RunWithClient is similar to Run but works only with an already established
@@ -447,11 +462,13 @@ func (s *Server) RunWithClient(conn net.Conn) {
 	go s.runSession(conn)
 }
 
-// serveDAPCodec reads and decodes requests from the client
+// ServeDAPCodec reads and decodes requests from the client
 // until it encounters an error or EOF, when it sends
 // a disconnect signal and returns.
-func (s *Session) serveDAPCodec() {
-	// TODO(polina): defer-close conn/session like in serveJSONCodec
+func (s *Session) ServeDAPCodec() {
+	// Close conn, but not the debugger in case we are in AcceptMuli mode.
+	// If not, debugger will be shut down in Stop().
+	defer s.conn.Close()
 	reader := bufio.NewReader(s.conn)
 	for {
 		request, err := dap.ReadProtocolMessage(reader)
@@ -466,8 +483,11 @@ func (s *Session) serveDAPCodec() {
 		if err != nil {
 			s.config.log.Debug("DAP error: ", err)
 			select {
-			case <-s.config.stopTriggered:
+			case <-s.config.StopTriggered:
 			default:
+				if !s.config.AcceptMulti {
+					defer s.config.triggerServerStop()
+				}
 				if err != io.EOF { // EOF means client closed connection
 					if decodeErr, ok := err.(*dap.DecodeProtocolMessageFieldError); ok {
 						// Send an error response to the users if we were unable to process the message.
@@ -476,15 +496,15 @@ func (s *Session) serveDAPCodec() {
 					}
 					s.config.log.Error("DAP error: ", err)
 				}
-				if s.config.AcceptMulti {
-					s.conn.Close()
-				} else {
-					s.config.triggerServerStop()
-				}
 			}
 			return
 		}
 		s.handleRequest(request)
+
+		if _, ok := request.(*dap.DisconnectRequest); ok {
+			// disconnect already shut things down and triggered stopping
+			return
+		}
 	}
 }
 
@@ -1127,11 +1147,13 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 
 	if s.debugger != nil && s.config.AcceptMulti && !request.Arguments.TerminateDebuggee {
 		// This is a multi-use server/debugger, so a disconnect request that doesn't
-		// terminate the debuggee should clean up only the client connection, but not the server.
+		// terminate the debuggee should clean up only the client connection and pointer to debugger,
+		// but not the entire server.
 		s.logToConsole("Closing client session, but leaving multi-client DAP server running at " + s.config.Listener.Addr().String())
 		s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
 		s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
 		s.conn.Close()
+		s.debugger = nil
 		// The target is left in whatever state it is already in - halted or running.
 		// The users therefore have the flexibility to choose the appropriate state
 		// for their case before disconnecting. This is also desirable in case of
@@ -1152,7 +1174,7 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 		// overridden by an explicit request to terminate.
 		killProcess := s.config.Debugger.AttachPid == 0 || request.Arguments.TerminateDebuggee
 		err = s.stopDebugSession(killProcess)
-	} else {
+	} else if s.noDebugProcess != nil {
 		s.stopNoDebugProcess()
 	}
 	if err != nil {
@@ -1169,7 +1191,12 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 // Returns any detach error other than proc.ErrProcessExited.
 func (s *Session) stopDebugSession(killProcess bool) error {
 	s.changeStateMu.Lock()
-	defer s.changeStateMu.Unlock()
+	defer func() {
+		// Avoid running stop sequence twice.
+		// It's not fatal, but will result in duplicate logging.
+		s.debugger = nil
+		s.changeStateMu.Unlock()
+	}()
 	if s.debugger == nil {
 		return nil
 	}
@@ -1229,10 +1256,12 @@ func (s *Session) stopDebugSession(killProcess bool) error {
 // halt sends a halt request if the debuggee is running.
 // changeStateMu should be held when calling (*Server).halt.
 func (s *Session) halt() (*api.DebuggerState, error) {
+	s.config.log.Debug("halting")
 	// Only send a halt request if the debuggee is running.
 	if s.debugger.IsRunning() {
 		return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
 	}
+	s.config.log.Debug("process not running")
 	return s.debugger.State(false)
 }
 
@@ -1326,9 +1355,10 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
 		} else {
 			got.Cond = want.condition
 			got.HitCond = want.hitCondition
-			got.Tracepoint = want.logMessage != ""
-			got.UserData = want.logMessage
-			err = s.debugger.AmendBreakpoint(got)
+			err = setLogMessage(got, want.logMessage)
+			if err == nil {
+				err = s.debugger.AmendBreakpoint(got)
+			}
 		}
 		createdBps[want.name] = struct{}{}
 		s.updateBreakpointsResponse(breakpoints, i, err, got)
@@ -1351,19 +1381,20 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
 			if _, ok := createdBps[want.name]; ok {
 				err = fmt.Errorf("breakpoint already exists")
 			} else {
-				// Create new breakpoints.
-				got, err = s.debugger.CreateBreakpoint(
-					&api.Breakpoint{
-						Name:       want.name,
-						File:       wantLoc.file,
-						Line:       wantLoc.line,
-						Addr:       wantLoc.addr,
-						Addrs:      wantLoc.addrs,
-						Cond:       want.condition,
-						HitCond:    want.hitCondition,
-						Tracepoint: want.logMessage != "",
-						UserData:   want.logMessage,
-					})
+				bp := &api.Breakpoint{
+					Name:    want.name,
+					File:    wantLoc.file,
+					Line:    wantLoc.line,
+					Addr:    wantLoc.addr,
+					Addrs:   wantLoc.addrs,
+					Cond:    want.condition,
+					HitCond: want.hitCondition,
+				}
+				err = setLogMessage(bp, want.logMessage)
+				if err == nil {
+					// Create new breakpoints.
+					got, err = s.debugger.CreateBreakpoint(bp)
+				}
 			}
 		}
 		createdBps[want.name] = struct{}{}
@@ -1372,6 +1403,18 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
 	return breakpoints
 }
 
+func setLogMessage(bp *api.Breakpoint, msg string) error {
+	tracepoint, userdata, err := parseLogPoint(msg)
+	if err != nil {
+		return err
+	}
+	bp.Tracepoint = tracepoint
+	if userdata != nil {
+		bp.UserData = *userdata
+	}
+	return nil
+}
+
 func (s *Session) updateBreakpointsResponse(breakpoints []dap.Breakpoint, i int, err error, got *api.Breakpoint) {
 	breakpoints[i].Verified = (err == nil)
 	if err != nil {
@@ -1527,7 +1570,9 @@ func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneReque
 	}
 	s.debugger.Target().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints
 
+	s.logToConsole("Type 'dlv help' for list of commands.")
 	s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)})
+
 	if !s.args.stopOnEntry {
 		s.runUntilStopAndNotify(api.Continue, allowNextStateChange)
 	}
@@ -1573,6 +1618,12 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
 	var next int
 	if s.debugger != nil {
 		gs, next, err = s.debugger.Goroutines(0, maxGoroutines)
+		if err == nil && s.args.HideSystemGoroutines {
+			gs = s.debugger.FilterGoroutines(gs, []api.ListGoroutinesFilter{{
+				Kind:    api.GoroutineUser,
+				Negated: false,
+			}})
+		}
 	}
 
 	var threads []dap.Thread
@@ -1782,25 +1833,24 @@ func stoppedGoroutineID(state *api.DebuggerState) (id int) {
 // stoppedOnBreakpointGoroutineID gets the goroutine id of the first goroutine
 // that is stopped on a real breakpoint, starting with the selected goroutine.
 func (s *Session) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int, *api.Breakpoint) {
-	// Check if the selected goroutine is stopped on a real breakpoint
-	// since we would prefer to use that one.
-	goid := stoppedGoroutineID(state)
-	if g, _ := s.debugger.FindGoroutine(goid); g != nil && g.Thread != nil {
-		if bp := g.Thread.Breakpoint(); bp != nil && bp.Breakpoint != nil && !bp.Breakpoint.Tracepoint {
-			return goid, api.ConvertBreakpoint(bp.Breakpoint)
-		}
+	// Use the first goroutine that is stopped on a breakpoint.
+	gs := s.stoppedGs(state)
+	if len(gs) == 0 {
+		return 0, nil
 	}
-
-	// Some of the breakpoints may be log points, choose the goroutine
-	// that is not stopped on a tracepoint.
-	for _, th := range state.Threads {
-		if bp := th.Breakpoint; bp != nil {
-			if !bp.Tracepoint {
-				return th.GoroutineID, bp
-			}
-		}
+	goid := gs[0]
+	if goid == 0 {
+		return goid, state.CurrentThread.Breakpoint
+	}
+	g, _ := s.debugger.FindGoroutine(goid)
+	if g == nil || g.Thread == nil {
+		return goid, nil
 	}
-	return 0, nil
+	bp := g.Thread.Breakpoint()
+	if bp == nil || bp.Breakpoint == nil {
+		return goid, nil
+	}
+	return goid, api.ConvertBreakpoint(bp.Breakpoint)
 }
 
 // stepUntilStopAndNotify is a wrapper around runUntilStopAndNotify that
@@ -1882,7 +1932,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
 	if start < 0 {
 		start = 0
 	}
-	levels := s.args.stackTraceDepth
+	levels := s.args.StackTraceDepth
 	if request.Arguments.Levels > 0 {
 		levels = request.Arguments.Levels
 	}
@@ -1926,7 +1976,7 @@ func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
 		// We don't know the exact number of available stack frames, so
 		// add an arbitrary number so the client knows to request additional
 		// frames.
-		totalFrames += s.args.stackTraceDepth
+		totalFrames += s.args.StackTraceDepth
 	}
 	response := &dap.StackTraceResponse{
 		Response: *newResponse(request.Request),
@@ -1977,7 +2027,7 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
 	scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)}
 	scopes := []dap.Scope{scopeLocals}
 
-	if s.args.showGlobalVariables {
+	if s.args.ShowGlobalVariables {
 		// Limit what global variables we will return to the current package only.
 		// TODO(polina): This is how vscode-go currently does it to make
 		// the amount of the returned data manageable. In fact, this is
@@ -2011,6 +2061,27 @@ func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
 		scopeGlobals := dap.Scope{Name: globScope.Name, VariablesReference: s.variableHandles.create(globScope)}
 		scopes = append(scopes, scopeGlobals)
 	}
+
+	if s.args.ShowRegisters {
+		// Retrieve registers
+		regs, err := s.debugger.ScopeRegisters(goid, frame, 0, false)
+		if err != nil {
+			s.sendErrorResponse(request.Request, UnableToListRegisters, "Unable to list registers", err.Error())
+			return
+		}
+		outRegs := api.ConvertRegisters(regs, s.debugger.DwarfRegisterToString, false)
+		regsVar := make([]proc.Variable, len(outRegs))
+		for i, r := range outRegs {
+			regsVar[i] = proc.Variable{
+				Name:  r.Name,
+				Value: constant.MakeString(r.Value),
+				Kind:  reflect.Kind(proc.VariableConstant),
+			}
+		}
+		regsScope := &fullyQualifiedVariable{&proc.Variable{Name: "Registers", Children: regsVar}, "", true, 0}
+		scopeRegisters := dap.Scope{Name: regsScope.Name, VariablesReference: s.variableHandles.create(regsScope)}
+		scopes = append(scopes, scopeRegisters)
+	}
 	response := &dap.ScopesResponse{
 		Response: *newResponse(request.Request),
 		Body:     dap.ScopesResponseBody{Scopes: scopes},
@@ -2223,6 +2294,17 @@ func (s *Session) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Varia
 				name = fmt.Sprintf("(%s)", name)
 			}
 
+			if v.isScope && v.Name == "Registers" {
+				// Align all of the register names.
+				name = fmt.Sprintf("%6s", strings.ToLower(c.Name))
+				// Set the correct evaluate name for the register.
+				cfqname = fmt.Sprintf("_%s", strings.ToUpper(c.Name))
+				// Unquote the value
+				if ucvalue, err := strconv.Unquote(cvalue); err == nil {
+					cvalue = ucvalue
+				}
+			}
+
 			children[i] = dap.Variable{
 				Name:               name,
 				EvaluateName:       cfqname,
@@ -2499,6 +2581,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
 // Support the following expressions:
 // -- {expression} - evaluates the expression and returns the result as a variable
 // -- call {function} - injects a function call and returns the result as a variable
+// -- config {expression} - updates configuration paramaters
 // TODO(polina): users have complained about having to click to expand multi-level
 // variables, so consider also adding the following:
 // -- print {expression} - return the result as a string like from dlv cli
@@ -2518,9 +2601,20 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
 	}
 
 	response := &dap.EvaluateResponse{Response: *newResponse(request.Request)}
-	isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression)
-	if err == nil && isCall { // call {expression}
-		expr := strings.Replace(request.Arguments.Expression, "call ", "", 1)
+	expr := request.Arguments.Expression
+
+	if isConfig, err := regexp.MatchString(`^\s*dlv\s+\S+`, expr); err == nil && isConfig { // dlv {command}
+		expr := strings.Replace(expr, "dlv ", "", 1)
+		result, err := s.delveCmd(goid, frame, expr)
+		if err != nil {
+			s.sendErrorResponseWithOpts(request.Request, UnableToRunDlvCommand, "Unable to run dlv command", err.Error(), showErrorToUser)
+			return
+		}
+		response.Body = dap.EvaluateResponseBody{
+			Result: result,
+		}
+	} else if isCall, err := regexp.MatchString(`^\s*call\s+\S+`, expr); err == nil && isCall { // call {expression}
+		expr := strings.Replace(expr, "call ", "", 1)
 		_, retVars, err := s.doCall(goid, frame, expr)
 		if err != nil {
 			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
@@ -2542,7 +2636,7 @@ func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
 			}
 		}
 	} else { // {expression}
-		exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, DefaultLoadConfig)
+		exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, expr, DefaultLoadConfig)
 		if err != nil {
 			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
 			return
@@ -3135,7 +3229,7 @@ func (s *Session) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 }
 
 func (s *Session) stacktrace(goroutineID int, g *proc.G) (string, error) {
-	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
+	frames, err := s.debugger.Stacktrace(goroutineID, s.args.StackTraceDepth, 0)
 	if err != nil {
 		return "", err
 	}
@@ -3458,12 +3552,13 @@ func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange ch
 		return state, err
 	}
 
-	foundRealBreakpoint := s.handleLogPoints(state)
+	s.handleLogPoints(state)
+	gsOnBp := s.stoppedGs(state)
 
 	switch s.debugger.StopReason() {
 	case proc.StopBreakpoint, proc.StopManual:
 		// Make sure a real manual stop was requested or a real breakpoint was hit.
-		if foundRealBreakpoint || s.checkHaltRequested() {
+		if len(gsOnBp) > 0 || s.checkHaltRequested() {
 			s.setRunningCmd(false)
 		}
 	default:
@@ -3478,25 +3573,50 @@ func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange ch
 	return state, err
 }
 
-func (s *Session) handleLogPoints(state *api.DebuggerState) bool {
-	foundRealBreakpoint := false
+func (s *Session) handleLogPoints(state *api.DebuggerState) {
 	for _, th := range state.Threads {
 		if bp := th.Breakpoint; bp != nil {
-			logged := s.logBreakpointMessage(bp, th.GoroutineID)
-			if !logged {
-				foundRealBreakpoint = true
+			s.logBreakpointMessage(bp, th.GoroutineID)
+		}
+	}
+}
+
+func (s *Session) stoppedGs(state *api.DebuggerState) (gs []int) {
+	// Check the current thread first. There may be no selected goroutine.
+	if state.CurrentThread.Breakpoint != nil && !state.CurrentThread.Breakpoint.Tracepoint {
+		gs = append(gs, state.CurrentThread.GoroutineID)
+	}
+	if s.debugger.StopReason() == proc.StopHardcodedBreakpoint {
+		gs = append(gs, stoppedGoroutineID(state))
+	}
+	for _, th := range state.Threads {
+		// Some threads may be stopped on a hardcoded breakpoint.
+		// TODO(suzmue): This is a workaround for detecting hard coded breakpoints,
+		// though this check is likely not sufficient. It would be better to resolve
+		// this in the debugger layer instead.
+		if th.Function.Name() == "runtime.breakpoint" {
+			gs = append(gs, th.GoroutineID)
+			continue
+		}
+		// We already added the current thread if it had a breakpoint.
+		if th.ID == state.CurrentThread.ID {
+			continue
+		}
+		if bp := th.Breakpoint; bp != nil {
+			if !th.Breakpoint.Tracepoint {
+				gs = append(gs, th.GoroutineID)
 			}
 		}
 	}
-	return foundRealBreakpoint
+	return gs
 }
 
 func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool {
 	if !bp.Tracepoint {
 		return false
 	}
-	// TODO(suzmue): allow evaluate expressions within log points.
-	if msg, ok := bp.UserData.(string); ok {
+	if lMsg, ok := bp.UserData.(logMessage); ok {
+		msg := lMsg.evaluate(s, goid)
 		s.send(&dap.OutputEvent{
 			Event: *newEvent("output"),
 			Body: dap.OutputEventBody{
@@ -3512,6 +3632,19 @@ func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool {
 	return true
 }
 
+func (msg *logMessage) evaluate(s *Session, goid int) string {
+	evaluated := make([]interface{}, len(msg.args))
+	for i := range msg.args {
+		exprVar, err := s.debugger.EvalVariableInScope(goid, 0, 0, msg.args[i], DefaultLoadConfig)
+		if err != nil {
+			evaluated[i] = fmt.Sprintf("{eval err: %e}", err)
+			continue
+		}
+		evaluated[i] = s.convertVariableToString(exprVar)
+	}
+	return fmt.Sprintf(msg.format, evaluated...)
+}
+
 func (s *Session) toClientPath(path string) string {
 	if len(s.args.substitutePathServerToClient) == 0 {
 		return path
@@ -3533,3 +3666,64 @@ func (s *Session) toServerPath(path string) string {
 	}
 	return serverPath
 }
+
+type logMessage struct {
+	format string
+	args   []string
+}
+
+// parseLogPoint parses a log message according to the DAP spec:
+//   "Expressions within {} are interpolated."
+func parseLogPoint(msg string) (bool, *logMessage, error) {
+	// Note: All braces *must* come in pairs, even those within an
+	// expression to be interpolated.
+	// TODO(suzmue): support individual braces in string values in
+	// eval expressions.
+	var args []string
+
+	var isArg bool
+	var formatSlice, argSlice []rune
+	braceCount := 0
+	for _, r := range msg {
+		if isArg {
+			switch r {
+			case '}':
+				if braceCount--; braceCount == 0 {
+					argStr := strings.TrimSpace(string(argSlice))
+					if len(argStr) == 0 {
+						return false, nil, fmt.Errorf("empty evaluation string")
+					}
+					args = append(args, argStr)
+					formatSlice = append(formatSlice, '%', 's')
+					isArg = false
+					continue
+				}
+			case '{':
+				braceCount += 1
+			}
+			argSlice = append(argSlice, r)
+			continue
+		}
+
+		switch r {
+		case '}':
+			return false, nil, fmt.Errorf("invalid log point format, unexpected '}'")
+		case '{':
+			if braceCount++; braceCount == 1 {
+				isArg, argSlice = true, []rune{}
+				continue
+			}
+		}
+		formatSlice = append(formatSlice, r)
+	}
+	if isArg {
+		return false, nil, fmt.Errorf("invalid log point format")
+	}
+	if len(formatSlice) == 0 {
+		return false, nil, nil
+	}
+	return true, &logMessage{
+		format: string(formatSlice),
+		args:   args,
+	}, nil
+}
diff --git a/service/dap/types.go b/service/dap/types.go
index 24910315..3b442be9 100644
--- a/service/dap/types.go
+++ b/service/dap/types.go
@@ -149,6 +149,14 @@ type LaunchAttachCommonConfig struct {
 	// should be shown in the variables pane or not.
 	ShowGlobalVariables bool `json:"showGlobalVariables,omitempty"`
 
+	// Boolean value to indicate whether registers should be shown
+	// in the variables pane or not.
+	ShowRegisters bool `json:"showRegisters,omitempty"`
+
+	// Boolean value to indicate whether system goroutines
+	// should be should be hidden from the call stack view.
+	HideSystemGoroutines bool `json:"hideSystemGoroutines,omitempty"`
+
 	// An array of mappings from a local path (client) to the remote path (debugger).
 	// This setting is useful when working in a file system with symbolic links,
 	// running remote debugging, or debugging an executable compiled externally.
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 2562d672..eee82b8e 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -2157,6 +2157,9 @@ func (d *Debugger) GetBufferedTracepoints() []api.TracepointResult {
 		for _, p := range trace.InputParams {
 			results[i].InputParams = append(results[i].InputParams, *api.ConvertVar(p))
 		}
+		for _, p := range trace.ReturnParams {
+			results[i].ReturnParams = append(results[i].ReturnParams, *api.ConvertVar(p))
+		}
 	}
 	return results
 }
diff --git a/service/rpccommon/server.go b/service/rpccommon/server.go
index db88b9d5..328885fc 100644
--- a/service/rpccommon/server.go
+++ b/service/rpccommon/server.go
@@ -1,6 +1,7 @@
 package rpccommon
 
 import (
+	"bufio"
 	"bytes"
 	"encoding/json"
 	"fmt"
@@ -19,6 +20,7 @@ import (
 	"github.com/go-delve/delve/pkg/version"
 	"github.com/go-delve/delve/service"
 	"github.com/go-delve/delve/service/api"
+	"github.com/go-delve/delve/service/dap"
 	"github.com/go-delve/delve/service/debugger"
 	"github.com/go-delve/delve/service/internal/sameuser"
 	"github.com/go-delve/delve/service/rpc1"
@@ -156,7 +158,7 @@ func (s *ServerImpl) Run() error {
 				}
 			}
 
-			go s.serveJSONCodec(c)
+			go s.serveConnectionDemux(c)
 			if !s.config.AcceptMulti {
 				break
 			}
@@ -165,6 +167,28 @@ func (s *ServerImpl) Run() error {
 	return nil
 }
 
+type bufReadWriteCloser struct {
+	*bufio.Reader
+	io.WriteCloser
+}
+
+func (s *ServerImpl) serveConnectionDemux(c io.ReadWriteCloser) {
+	conn := &bufReadWriteCloser{bufio.NewReader(c), c}
+	b, err := conn.Peek(1)
+	if err != nil {
+		s.log.Warnf("error determining new connection protocol: %v", err)
+		return
+	}
+	if b[0] == 'C' { // C is for DAP's Content-Length
+		s.log.Debugf("serving DAP on new connection")
+		ds := dap.NewSession(conn, &dap.Config{Config: s.config, StopTriggered: s.stopChan}, s.debugger)
+		go ds.ServeDAPCodec()
+	} else {
+		s.log.Debugf("serving JSON-RPC on new connection")
+		go s.serveJSONCodec(conn)
+	}
+}
+
 // Precompute the reflect type for error.  Can't use error directly
 // because Typeof takes an empty interface value.  This is annoying.
 var typeOfError = reflect.TypeOf((*error)(nil)).Elem()


Test patch:
diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md
index ed231acf..27cf1bf5 100644
--- a/Documentation/backend_test_health.md
+++ b/Documentation/backend_test_health.md
@@ -16,9 +16,8 @@ Tests skipped by each supported backend:
 	* 4 not implemented
 * linux/386/pie skipped = 1
 	* 1 broken
-* linux/arm64 skipped = 4
+* linux/arm64 skipped = 1
 	* 1 broken - cgo stacktraces
-	* 3 not implemented
 * pie skipped = 2
 	* 2 upstream issue - https://github.com/golang/go/issues/29322
 * windows skipped = 2
diff --git a/_scripts/latestver.py b/_scripts/latestver.py
index cad2f80a..886d476e 100644
--- a/_scripts/latestver.py
+++ b/_scripts/latestver.py
@@ -2,11 +2,25 @@
 import json
 import urllib
 import sys
-from distutils.version import LooseVersion
+import re
+
+def splitver(x):
+	v = re.split(r'([^\d]+)', x)
+	v[0] = int(v[0])
+	if len(v) > 2:
+		v[2] = int(v[2])
+	if len(v) > 4:
+		v[4] = int(v[4])
+	# make rc/beta versions sort before normal versions
+	if len(v) > 3 and v[3] == '.':
+		v[3] = '~' 
+	elif len(v) == 3:
+		v.append('~')
+	return v
 
 ver = sys.argv[1]
 d = json.loads(urllib.urlopen('https://golang.org/dl/?mode=json&include=all').read())
-ds = sorted(d, reverse=True, key=lambda it: LooseVersion(it['version'][2:]))
+ds = sorted(d, reverse=True, key=lambda it: splitver(it['version'][2:]))
 for x in ds:
 	if x['version'][:len(ver)] == ver:
 		print x['version']
diff --git a/_scripts/test_linux.sh b/_scripts/test_linux.sh
index e7156bf2..5bd251f8 100755
--- a/_scripts/test_linux.sh
+++ b/_scripts/test_linux.sh
@@ -53,4 +53,12 @@ echo "$GOROOT"
 echo "$GOPATH"
 cd delve
 
+# Starting with go1.18 'go build' and 'go run' will try to stamp the build
+# with the current VCS revision, which does not work with TeamCity
+if [ "$version" = "gotip" ]; then
+	export GOFLAGS=-buildvcs=false
+elif [ ${version:4} -gt 17 ]; then
+	export GOFLAGS=-buildvcs=false
+fi
+
 make test
diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go
index 7743f522..565d868c 100644
--- a/cmd/dlv/dlv_test.go
+++ b/cmd/dlv/dlv_test.go
@@ -24,8 +24,10 @@ import (
 	"github.com/go-delve/delve/pkg/goversion"
 	protest "github.com/go-delve/delve/pkg/proc/test"
 	"github.com/go-delve/delve/pkg/terminal"
+	"github.com/go-delve/delve/service/dap"
 	"github.com/go-delve/delve/service/dap/daptest"
 	"github.com/go-delve/delve/service/rpc2"
+	godap "github.com/google/go-dap"
 	"golang.org/x/tools/go/packages"
 )
 
@@ -52,6 +54,7 @@ func TestMain(m *testing.M) {
 }
 
 func assertNoError(err error, t testing.TB, s string) {
+	t.Helper()
 	if err != nil {
 		_, file, line, _ := runtime.Caller(1)
 		fname := filepath.Base(file)
@@ -647,8 +650,8 @@ func TestTypecheckRPC(t *testing.T) {
 	}
 }
 
-// TestDap verifies that a dap server can be started and shut down.
-func TestDap(t *testing.T) {
+// TestDAPCmd verifies that a dap server can be started and shut down.
+func TestDAPCmd(t *testing.T) {
 	const listenAddr = "127.0.0.1:40575"
 
 	dlvbin, tmpdir := getDlvBin(t)
@@ -691,8 +694,179 @@ func TestDap(t *testing.T) {
 	cmd.Wait()
 }
 
-// TestDapWithClient tests dlv dap --client-addr can be started and shut down.
-func TestDapWithClient(t *testing.T) {
+func newDAPRemoteClient(t *testing.T, addr string) *daptest.Client {
+	c := daptest.NewClient(addr)
+	c.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
+	c.ExpectInitializedEvent(t)
+	c.ExpectAttachResponse(t)
+	c.ConfigurationDoneRequest()
+	c.ExpectStoppedEvent(t)
+	c.ExpectConfigurationDoneResponse(t)
+	return c
+}
+
+func TestRemoteDAPClient(t *testing.T) {
+	const listenAddr = "127.0.0.1:40576"
+
+	dlvbin, tmpdir := getDlvBin(t)
+	defer os.RemoveAll(tmpdir)
+
+	buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
+	cmd := exec.Command(dlvbin, "debug", "--headless", "--log-output=dap", "--log", "--listen", listenAddr)
+	cmd.Dir = buildtestdir
+	stdout, err := cmd.StdoutPipe()
+	assertNoError(err, t, "stdout pipe")
+	defer stdout.Close()
+	stderr, err := cmd.StderrPipe()
+	assertNoError(err, t, "stderr pipe")
+	defer stderr.Close()
+	assertNoError(cmd.Start(), t, "start headless instance")
+
+	scanOut := bufio.NewScanner(stdout)
+	scanErr := bufio.NewScanner(stderr)
+	// Wait for the debug server to start
+	scanOut.Scan()
+	t.Log(scanOut.Text())
+	go func() { // Capture logging
+		for scanErr.Scan() {
+			t.Log(scanErr.Text())
+		}
+	}()
+
+	client := newDAPRemoteClient(t, listenAddr)
+	client.ContinueRequest(1)
+	client.ExpectContinueResponse(t)
+	client.ExpectTerminatedEvent(t)
+
+	client.DisconnectRequest()
+	client.ExpectOutputEventProcessExited(t, 0)
+	client.ExpectOutputEventDetaching(t)
+	client.ExpectDisconnectResponse(t)
+	client.ExpectTerminatedEvent(t)
+	if _, err := client.ReadMessage(); err == nil {
+		t.Error("expected read error upon shutdown")
+	}
+	client.Close()
+	cmd.Wait()
+}
+
+func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client) {
+	c.DisconnectRequest()
+	c.ExpectOutputEventClosingClient(t)
+	c.ExpectDisconnectResponse(t)
+	c.ExpectTerminatedEvent(t)
+	c.Close()
+	time.Sleep(10 * time.Millisecond)
+}
+
+func TestRemoteDAPClientMulti(t *testing.T) {
+	const listenAddr = "127.0.0.1:40577"
+
+	dlvbin, tmpdir := getDlvBin(t)
+	defer os.RemoveAll(tmpdir)
+
+	buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
+	cmd := exec.Command(dlvbin, "debug", "--headless", "--accept-multiclient", "--log-output=debugger", "--log", "--listen", listenAddr)
+	cmd.Dir = buildtestdir
+	stdout, err := cmd.StdoutPipe()
+	assertNoError(err, t, "stdout pipe")
+	defer stdout.Close()
+	stderr, err := cmd.StderrPipe()
+	assertNoError(err, t, "stderr pipe")
+	defer stderr.Close()
+	assertNoError(cmd.Start(), t, "start headless instance")
+
+	scanOut := bufio.NewScanner(stdout)
+	scanErr := bufio.NewScanner(stderr)
+	// Wait for the debug server to start
+	scanOut.Scan()
+	t.Log(scanOut.Text())
+	go func() { // Capture logging
+		for scanErr.Scan() {
+			t.Log(scanErr.Text())
+		}
+	}()
+
+	// Client 1 connects and continues to main.main
+	dapclient := newDAPRemoteClient(t, listenAddr)
+	dapclient.SetFunctionBreakpointsRequest([]godap.FunctionBreakpoint{{Name: "main.main"}})
+	dapclient.ExpectSetFunctionBreakpointsResponse(t)
+	dapclient.ContinueRequest(1)
+	dapclient.ExpectContinueResponse(t)
+	dapclient.ExpectStoppedEvent(t)
+	dapclient.CheckStopLocation(t, 1, "main.main", 5)
+	closeDAPRemoteMultiClient(t, dapclient)
+
+	// Client 2 reconnects at main.main and continues to process exit
+	dapclient2 := newDAPRemoteClient(t, listenAddr)
+	dapclient2.CheckStopLocation(t, 1, "main.main", 5)
+	dapclient2.ContinueRequest(1)
+	dapclient2.ExpectContinueResponse(t)
+	dapclient2.ExpectTerminatedEvent(t)
+	closeDAPRemoteMultiClient(t, dapclient2)
+
+	// Attach to exited processs is an error
+	dapclient3 := daptest.NewClient(listenAddr)
+	dapclient3.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
+	dapclient3.ExpectErrorResponseWith(t, dap.FailedToAttach, `Process \d+ has exited with status 0`, true)
+	closeDAPRemoteMultiClient(t, dapclient3)
+
+	// But rpc clients can still connect and restart
+	rpcclient := rpc2.NewClient(listenAddr)
+	if _, err := rpcclient.Restart(false); err != nil {
+		t.Errorf("error restarting with rpc client: %v", err)
+	}
+	if err := rpcclient.Detach(true); err != nil {
+		t.Fatalf("error detaching from headless instance: %v", err)
+	}
+	cmd.Wait()
+}
+
+func TestRemoteDAPClientAfterContinue(t *testing.T) {
+	const listenAddr = "127.0.0.1:40578"
+
+	dlvbin, tmpdir := getDlvBin(t)
+	defer os.RemoveAll(tmpdir)
+
+	fixture := protest.BuildFixture("loopprog", 0)
+	cmd := exec.Command(dlvbin, "exec", fixture.Path, "--headless", "--continue", "--accept-multiclient", "--log-output=debugger,dap", "--log", "--listen", listenAddr)
+	stdout, err := cmd.StdoutPipe()
+	assertNoError(err, t, "stdout pipe")
+	defer stdout.Close()
+	stderr, err := cmd.StderrPipe()
+	assertNoError(err, t, "stderr pipe")
+	defer stderr.Close()
+	assertNoError(cmd.Start(), t, "start headless instance")
+
+	scanOut := bufio.NewScanner(stdout)
+	scanErr := bufio.NewScanner(stderr)
+	// Wait for the debug server to start
+	scanOut.Scan() // "API server listening...""
+	t.Log(scanOut.Text())
+	// Wait for the program to start
+	scanOut.Scan() // "past main"
+	t.Log(scanOut.Text())
+
+	go func() { // Capture logging
+		for scanErr.Scan() {
+			t.Log(scanErr.Text())
+		}
+	}()
+
+	c := newDAPRemoteClient(t, listenAddr)
+	c.DisconnectRequestWithKillOption(true)
+	c.ExpectOutputEventDetachingKill(t)
+	c.ExpectDisconnectResponse(t)
+	c.ExpectTerminatedEvent(t)
+	if _, err := c.ReadMessage(); err == nil {
+		t.Error("expected read error upon shutdown")
+	}
+	c.Close()
+	cmd.Wait()
+}
+
+// TestDAPCmdWithClient tests dlv dap --client-addr can be started and shut down.
+func TestDAPCmdWithClient(t *testing.T) {
 	listener, err := net.Listen("tcp", ":0")
 	if err != nil {
 		t.Fatalf("cannot setup listener required for testing: %v", err)
@@ -871,7 +1045,7 @@ func TestTraceEBPF(t *testing.T) {
 	dlvbin, tmpdir := getDlvBinEBPF(t)
 	defer os.RemoveAll(tmpdir)
 
-	expected := []byte("> (1) main.foo(99, 9801)\n")
+	expected := []byte("> (1) main.foo(99, 9801)\n=> \"9900\"")
 
 	fixtures := protest.FindFixturesDir()
 	cmd := exec.Command(dlvbin, "trace", "--ebpf", "--output", filepath.Join(tmpdir, "__debug"), filepath.Join(fixtures, "issue573.go"), "foo")
diff --git a/pkg/config/split_test.go b/pkg/config/split_test.go
index c49283b0..d2c258d0 100644
--- a/pkg/config/split_test.go
+++ b/pkg/config/split_test.go
@@ -35,3 +35,67 @@ func TestSplitDoubleQuotedFields(t *testing.T) {
 		}
 	}
 }
+
+func TestConfigureListByName(t *testing.T) {
+	type testConfig struct {
+		boolArg bool     `cfgName:"bool-arg"`
+		listArg []string `cfgName:"list-arg"`
+	}
+
+	type args struct {
+		sargs   *testConfig
+		cfgname string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "basic bool",
+			args: args{
+				sargs: &testConfig{
+					boolArg: true,
+					listArg: []string{},
+				},
+				cfgname: "bool-arg",
+			},
+			want: "bool-arg	true\n",
+		},
+		{
+			name: "list arg",
+			args: args{
+				sargs: &testConfig{
+					boolArg: true,
+					listArg: []string{"item 1", "item 2"},
+				},
+
+				cfgname: "list-arg",
+			},
+			want: "list-arg	[item 1 item 2]\n",
+		},
+		{
+			name: "empty",
+			args: args{
+				sargs:   &testConfig{},
+				cfgname: "",
+			},
+			want: "",
+		},
+		{
+			name: "invalid",
+			args: args{
+				sargs:   &testConfig{},
+				cfgname: "nonexistent",
+			},
+			want: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ConfigureListByName(tt.args.sargs, tt.args.cfgname, "cfgName"); got != tt.want {
+				t.Errorf("ConfigureListByName() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 32c1f719..7b08d515 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -5378,7 +5378,6 @@ func TestVariablesWithExternalLinking(t *testing.T) {
 func TestWatchpointsBasic(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
 	skipOn(t, "not implemented", "386")
-	skipOn(t, "not implemented", "linux", "arm64")
 	protest.AllowRecording(t)
 
 	position1 := 19
@@ -5409,7 +5408,7 @@ func TestWatchpointsBasic(t *testing.T) {
 			t.Fatal("breakpoint not set")
 		}
 
-		p.ClearBreakpoint(bp.Addr)
+		assertNoError(p.ClearBreakpoint(bp.Addr), t, "ClearBreakpoint")
 
 		assertNoError(p.Continue(), t, "Continue 2")
 		assertLineNumber(p, t, 21, "Continue 2") // Position 2
@@ -5437,7 +5436,6 @@ func TestWatchpointsBasic(t *testing.T) {
 func TestWatchpointCounts(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
 	skipOn(t, "not implemented", "386")
-	skipOn(t, "not implemented", "linux", "arm64")
 	protest.AllowRecording(t)
 
 	withTestProcess("databpcountstest", t, func(p *proc.Target, fixture protest.Fixture) {
@@ -5552,7 +5550,6 @@ func TestDwrapStartLocation(t *testing.T) {
 func TestWatchpointStack(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
 	skipOn(t, "not implemented", "386")
-	skipOn(t, "not implemented", "linux", "arm64")
 	protest.AllowRecording(t)
 
 	position1 := 17
diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go
index 4ca44865..9a6429fe 100644
--- a/pkg/terminal/command_test.go
+++ b/pkg/terminal/command_test.go
@@ -402,6 +402,7 @@ func TestScopePrefix(t *testing.T) {
 			stackOut := strings.Split(term.MustExec(fmt.Sprintf("goroutine %d stack", gid)), "\n")
 			fid := -1
 			for _, line := range stackOut {
+				line = strings.TrimLeft(line, " ")
 				space := strings.Index(line, " ")
 				if space < 0 {
 					continue
@@ -417,7 +418,7 @@ func TestScopePrefix(t *testing.T) {
 				}
 			}
 			if fid < 0 {
-				t.Fatalf("Could not find frame for goroutine %d: %v", gid, stackOut)
+				t.Fatalf("Could not find frame for goroutine %d: %q", gid, stackOut)
 			}
 			term.AssertExec(fmt.Sprintf("goroutine     %d    frame     %d     locals", gid, fid), "(no locals)\n")
 			argsOut := strings.Split(term.MustExec(fmt.Sprintf("goroutine %d frame %d args", gid, fid)), "\n")
diff --git a/service/dap/config_test.go b/service/dap/config_test.go
new file mode 100644
index 00000000..2b5eb0fa
--- /dev/null
+++ b/service/dap/config_test.go
@@ -0,0 +1,197 @@
+package dap
+
+import (
+	"testing"
+)
+
+func TestListConfig(t *testing.T) {
+	type args struct {
+		args *launchAttachArgs
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "empty",
+			args: args{
+				args: &launchAttachArgs{},
+			},
+			want: formatConfig(0, false, false, false, [][2]string{}),
+		},
+		{
+			name: "default values",
+			args: args{
+				args: &defaultArgs,
+			},
+			want: formatConfig(50, false, false, false, [][2]string{}),
+		},
+		{
+			name: "custom values",
+			args: args{
+				args: &launchAttachArgs{
+					StackTraceDepth:              35,
+					ShowGlobalVariables:          true,
+					substitutePathClientToServer: [][2]string{{"hello", "world"}},
+					substitutePathServerToClient: [][2]string{{"world", "hello"}},
+				},
+			},
+			want: formatConfig(35, true, false, false, [][2]string{{"hello", "world"}}),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := listConfig(tt.args.args); got != tt.want {
+				t.Errorf("listConfig() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestConfigureSetSubstitutePath(t *testing.T) {
+	type args struct {
+		args *launchAttachArgs
+		rest string
+	}
+	tests := []struct {
+		name      string
+		args      args
+		wantRules [][2]string
+		wantErr   bool
+	}{
+		// Test add rule.
+		{
+			name: "add rule",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{},
+					substitutePathServerToClient: [][2]string{},
+				},
+				rest: "/path/to/client/dir /path/to/server/dir",
+			},
+			wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
+			wantErr:   false,
+		},
+		{
+			name: "add rule (multiple)",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{
+						{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+						{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+					},
+					substitutePathServerToClient: [][2]string{
+						{"/path/to/server/dir/a", "/path/to/client/dir/a"},
+						{"/path/to/server/dir/b", "/path/to/client/dir/b"},
+					},
+				},
+				rest: "/path/to/client/dir/c /path/to/server/dir/b",
+			},
+			wantRules: [][2]string{
+				{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+				{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+				{"/path/to/client/dir/c", "/path/to/server/dir/b"},
+			},
+			wantErr: false,
+		},
+		// Test modify rule.
+		{
+			name: "modify rule",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
+					substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
+				},
+				rest: "/path/to/client/dir /new/path/to/server/dir",
+			},
+			wantRules: [][2]string{{"/path/to/client/dir", "/new/path/to/server/dir"}},
+			wantErr:   false,
+		},
+		{
+			name: "modify rule (multiple)",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{
+						{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+						{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+						{"/path/to/client/dir/c", "/path/to/server/dir/b"},
+					},
+					substitutePathServerToClient: [][2]string{
+						{"/path/to/server/dir/a", "/path/to/client/dir/a"},
+						{"/path/to/server/dir/b", "/path/to/client/dir/b"},
+						{"/path/to/server/dir/b", "/path/to/client/dir/c"},
+					},
+				},
+				rest: "/path/to/client/dir/b /new/path",
+			},
+			wantRules: [][2]string{
+				{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+				{"/path/to/client/dir/b", "/new/path"},
+				{"/path/to/client/dir/c", "/path/to/server/dir/b"},
+			},
+			wantErr: false,
+		},
+		// Test delete rule.
+		{
+			name: "delete rule",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
+					substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
+				},
+				rest: "/path/to/client/dir",
+			},
+			wantRules: [][2]string{},
+			wantErr:   false,
+		},
+		// Test invalid input.
+		{
+			name: "error on empty args",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{},
+					substitutePathServerToClient: [][2]string{},
+				},
+				rest: "  \n\r   ",
+			},
+			wantErr: true,
+		},
+		{
+			name: "error on delete nonexistent rule",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
+					substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
+				},
+				rest: "/path/to/server/dir",
+			},
+			wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
+			wantErr:   true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := configureSetSubstitutePath(tt.args.args, tt.args.rest)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("configureSetSubstitutePath() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if len(tt.args.args.substitutePathClientToServer) != len(tt.wantRules) {
+				t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer=%v, want %d rules", tt.args.args.substitutePathClientToServer, len(tt.wantRules))
+				return
+			}
+			gotClient2Server := tt.args.args.substitutePathClientToServer
+			gotServer2Client := tt.args.args.substitutePathServerToClient
+			for i, rule := range tt.wantRules {
+				if gotClient2Server[i][0] != rule[0] || gotClient2Server[i][1] != rule[1] {
+					t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], rule)
+				}
+				if gotServer2Client[i][1] != rule[0] || gotServer2Client[i][0] != rule[1] {
+					reverseRule := [2]string{rule[1], rule[0]}
+					t.Errorf("configureSetSubstitutePath() got substitutePathServerToClient[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], reverseRule)
+				}
+			}
+		})
+	}
+}
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index bf2143f6..0dfb1e23 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -88,11 +88,11 @@ func (c *Client) ExpectVisibleErrorResponse(t *testing.T) *dap.ErrorResponse {
 	return er
 }
 
-func (c *Client) expectErrorResponse(t *testing.T, id int, message string) *dap.ErrorResponse {
+func (c *Client) ExpectErrorResponseWith(t *testing.T, id int, message string, showUser bool) *dap.ErrorResponse {
 	t.Helper()
 	er := c.ExpectErrorResponse(t)
-	if er.Body.Error.Id != id || er.Message != message {
-		t.Errorf("\ngot %#v\nwant Id=%d Message=%q", er, id, message)
+	if matched, _ := regexp.MatchString(message, er.Body.Error.Format); !matched || er.Body.Error.Id != id || er.Body.Error.ShowUser != showUser {
+		t.Errorf("got %#v, want Id=%d Format=%q ShowUser=%v", er, id, message, showUser)
 	}
 	return er
 }
@@ -129,12 +129,12 @@ func pretty(v interface{}) string {
 
 func (c *Client) ExpectNotYetImplementedErrorResponse(t *testing.T) *dap.ErrorResponse {
 	t.Helper()
-	return c.expectErrorResponse(t, 7777, "Not yet implemented")
+	return c.ExpectErrorResponseWith(t, 7777, "Not yet implemented", false)
 }
 
 func (c *Client) ExpectUnsupportedCommandErrorResponse(t *testing.T) *dap.ErrorResponse {
 	t.Helper()
-	return c.expectErrorResponse(t, 9999, "Unsupported command")
+	return c.ExpectErrorResponseWith(t, 9999, "Unsupported command", false)
 }
 
 func (c *Client) ExpectOutputEventRegex(t *testing.T, want string) *dap.OutputEvent {
@@ -173,6 +173,27 @@ func (c *Client) ExpectOutputEventTerminating(t *testing.T) *dap.OutputEvent {
 	return c.ExpectOutputEventRegex(t, `Terminating process [0-9]+\n`)
 }
 
+func (c *Client) ExpectOutputEventClosingClient(t *testing.T) *dap.OutputEvent {
+	t.Helper()
+	return c.ExpectOutputEventRegex(t, `Closing client session, but leaving multi-client DAP server running at .+:[0-9]+\n`)
+}
+
+func (c *Client) CheckStopLocation(t *testing.T, thread int, name string, line int) {
+	t.Helper()
+	c.StackTraceRequest(thread, 0, 20)
+	st := c.ExpectStackTraceResponse(t)
+	if len(st.Body.StackFrames) < 1 {
+		t.Errorf("\ngot  %#v\nwant len(stackframes) => 1", st)
+	} else {
+		if line != -1 && st.Body.StackFrames[0].Line != line {
+			t.Errorf("\ngot  %#v\nwant Line=%d", st, line)
+		}
+		if st.Body.StackFrames[0].Name != name {
+			t.Errorf("\ngot  %#v\nwant Name=%q", st, name)
+		}
+	}
+}
+
 // InitializeRequest sends an 'initialize' request.
 func (c *Client) InitializeRequest() {
 	request := &dap.InitializeRequest{Request: *c.newRequest("initialize")}
diff --git a/service/dap/daptest/gen/main.go b/service/dap/daptest/gen/main.go
index 13010d4f..a5e3870e 100644
--- a/service/dap/daptest/gen/main.go
+++ b/service/dap/daptest/gen/main.go
@@ -44,7 +44,15 @@ func (c *Client) Check{{.}}(t *testing.T, m dap.Message) *dap.{{.}} {
 	if !ok {
 		t.Fatalf("got %#v, want *dap.ContinuedEvent", m)
 	}
-	m = c.ExpectMessage(t){{end}}
+	m = c.ExpectMessage(t){{else}}{{if (eq . "ConfigurationDoneResponse") }}
+	oe, ok := m.(*dap.OutputEvent)
+	if !ok {
+		t.Fatalf("got %#v, want *dap.OutputEvent", m)
+	}
+	if oe.Body.Output != "Type 'dlv help' for list of commands.\n" {
+		t.Fatalf("got %#v, want Output=%q", m, "Type 'dlv help' for list of commands.\n")
+	}
+	m = c.ExpectMessage(t){{end}}{{end}}
 	r, ok := m.(*dap.{{.}})
 	if !ok {
 		t.Fatalf("got %#v, want *dap.{{.}}", m)
diff --git a/service/dap/daptest/resp.go b/service/dap/daptest/resp.go
index 6501a51b..0a253299 100644
--- a/service/dap/daptest/resp.go
+++ b/service/dap/daptest/resp.go
@@ -128,6 +128,14 @@ func (c *Client) ExpectConfigurationDoneResponse(t *testing.T) *dap.Configuratio
 // CheckConfigurationDoneResponse fails the test if m is not *ConfigurationDoneResponse.
 func (c *Client) CheckConfigurationDoneResponse(t *testing.T, m dap.Message) *dap.ConfigurationDoneResponse {
 	t.Helper()
+	oe, ok := m.(*dap.OutputEvent)
+	if !ok {
+		t.Fatalf("got %#v, want *dap.OutputEvent", m)
+	}
+	if oe.Body.Output != "Type 'dlv help' for list of commands.\n" {
+		t.Fatalf("got %#v, want Output=%q", m, "Type 'dlv help' for list of commands.\n")
+	}
+	m = c.ExpectMessage(t)
 	r, ok := m.(*dap.ConfigurationDoneResponse)
 	if !ok {
 		t.Fatalf("got %#v, want *dap.ConfigurationDoneResponse", m)
@@ -441,6 +449,24 @@ func (c *Client) CheckLoadedSourcesResponse(t *testing.T, m dap.Message) *dap.Lo
 	return r
 }
 
+// ExpectMemoryEvent reads a protocol message from the connection
+// and fails the test if the read message is not *MemoryEvent.
+func (c *Client) ExpectMemoryEvent(t *testing.T) *dap.MemoryEvent {
+	t.Helper()
+	m := c.ExpectMessage(t)
+	return c.CheckMemoryEvent(t, m)
+}
+
+// CheckMemoryEvent fails the test if m is not *MemoryEvent.
+func (c *Client) CheckMemoryEvent(t *testing.T, m dap.Message) *dap.MemoryEvent {
+	t.Helper()
+	r, ok := m.(*dap.MemoryEvent)
+	if !ok {
+		t.Fatalf("got %#v, want *dap.MemoryEvent", m)
+	}
+	return r
+}
+
 // ExpectModuleEvent reads a protocol message from the connection
 // and fails the test if the read message is not *ModuleEvent.
 func (c *Client) ExpectModuleEvent(t *testing.T) *dap.ModuleEvent {
@@ -1085,3 +1111,21 @@ func (c *Client) CheckVariablesResponse(t *testing.T, m dap.Message) *dap.Variab
 	}
 	return r
 }
+
+// ExpectWriteMemoryResponse reads a protocol message from the connection
+// and fails the test if the read message is not *WriteMemoryResponse.
+func (c *Client) ExpectWriteMemoryResponse(t *testing.T) *dap.WriteMemoryResponse {
+	t.Helper()
+	m := c.ExpectMessage(t)
+	return c.CheckWriteMemoryResponse(t, m)
+}
+
+// CheckWriteMemoryResponse fails the test if m is not *WriteMemoryResponse.
+func (c *Client) CheckWriteMemoryResponse(t *testing.T, m dap.Message) *dap.WriteMemoryResponse {
+	t.Helper()
+	r, ok := m.(*dap.WriteMemoryResponse)
+	if !ok {
+		t.Fatalf("got %#v, want *dap.WriteMemoryResponse", m)
+	}
+	return r
+}
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 861eb4bc..cbd75b3a 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -33,6 +33,7 @@ import (
 const stopOnEntry bool = true
 const hasChildren bool = true
 const noChildren bool = false
+
 const localsScope = 1000
 const globalsScope = 1001
 
@@ -110,39 +111,189 @@ func startDAPServer(t *testing.T, serverStopped chan struct{}) (server *Server,
 	return server, forceStop
 }
 
-func TestForceStopNoClient(t *testing.T) {
-	serverStopped := make(chan struct{})
-	_, forceStop := startDAPServer(t, serverStopped)
-	close(forceStop)
-	<-serverStopped
+func verifyServerStopped(t *testing.T, server *Server) {
+	t.Helper()
+	if server.listener != nil {
+		if server.listener.Close() == nil {
+			t.Error("server should have closed listener after shutdown")
+		}
+	}
+	verifySessionStopped(t, server.session)
 }
 
-func TestForceStopNoTarget(t *testing.T) {
-	serverStopped := make(chan struct{})
-	server, forceStop := startDAPServer(t, serverStopped)
-	client := daptest.NewClient(server.config.Listener.Addr().String())
-	defer client.Close()
+func verifySessionStopped(t *testing.T, session *Session) {
+	t.Helper()
+	if session == nil {
+		return
+	}
+	if session.conn == nil {
+		t.Error("session must always have a set connection")
+	}
+	verifyConnStopped(t, session.conn)
+	if session.debugger != nil {
+		t.Error("session should have no pointer to debugger after shutdown")
+	}
+	if session.binaryToRemove != "" {
+		t.Error("session should have no binary to remove after shutdown")
+	}
+}
 
-	client.InitializeRequest()
-	client.ExpectInitializeResponseAndCapabilities(t)
-	close(forceStop)
-	<-serverStopped
+func verifyConnStopped(t *testing.T, conn io.ReadWriteCloser) {
+	t.Helper()
+	if conn.Close() == nil {
+		t.Error("client connection should be closed after shutdown")
+	}
 }
 
-func TestForceStopWithTarget(t *testing.T) {
-	serverStopped := make(chan struct{})
-	server, forceStop := startDAPServer(t, serverStopped)
-	client := daptest.NewClient(server.config.Listener.Addr().String())
-	defer client.Close()
+func TestStopNoClient(t *testing.T) {
+	for name, triggerStop := range map[string]func(s *Server, forceStop chan struct{}){
+		"force":        func(s *Server, forceStop chan struct{}) { close(forceStop) },
+		"accept error": func(s *Server, forceStop chan struct{}) { s.config.Listener.Close() },
+	} {
+		t.Run(name, func(t *testing.T) {
+			serverStopped := make(chan struct{})
+			server, forceStop := startDAPServer(t, serverStopped)
+			triggerStop(server, forceStop)
+			<-serverStopped
+			verifyServerStopped(t, server)
+		})
+	}
+}
 
-	client.InitializeRequest()
-	client.ExpectInitializeResponseAndCapabilities(t)
-	fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
-	client.LaunchRequest("exec", fixture.Path, stopOnEntry)
-	client.ExpectInitializedEvent(t)
-	client.ExpectLaunchResponse(t)
-	close(forceStop)
-	<-serverStopped
+func TestStopNoTarget(t *testing.T) {
+	for name, triggerStop := range map[string]func(c *daptest.Client, forceStop chan struct{}){
+		"force":      func(c *daptest.Client, forceStop chan struct{}) { close(forceStop) },
+		"read error": func(c *daptest.Client, forceStop chan struct{}) { c.Close() },
+		"disconnect": func(c *daptest.Client, forceStop chan struct{}) { c.DisconnectRequest() },
+	} {
+		t.Run(name, func(t *testing.T) {
+			serverStopped := make(chan struct{})
+			server, forceStop := startDAPServer(t, serverStopped)
+			client := daptest.NewClient(server.config.Listener.Addr().String())
+			defer client.Close()
+
+			client.InitializeRequest()
+			client.ExpectInitializeResponseAndCapabilities(t)
+			triggerStop(client, forceStop)
+			<-serverStopped
+			verifyServerStopped(t, server)
+		})
+	}
+}
+
+func TestStopWithTarget(t *testing.T) {
+	for name, triggerStop := range map[string]func(c *daptest.Client, forceStop chan struct{}){
+		"force":                  func(c *daptest.Client, forceStop chan struct{}) { close(forceStop) },
+		"read error":             func(c *daptest.Client, forceStop chan struct{}) { c.Close() },
+		"disconnect before exit": func(c *daptest.Client, forceStop chan struct{}) { c.DisconnectRequest() },
+		"disconnect after  exit": func(c *daptest.Client, forceStop chan struct{}) {
+			c.ContinueRequest(1)
+			c.ExpectContinueResponse(t)
+			c.ExpectTerminatedEvent(t)
+			c.DisconnectRequest()
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			serverStopped := make(chan struct{})
+			server, forceStop := startDAPServer(t, serverStopped)
+			client := daptest.NewClient(server.config.Listener.Addr().String())
+			defer client.Close()
+
+			client.InitializeRequest()
+			client.ExpectInitializeResponseAndCapabilities(t)
+			fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
+			client.LaunchRequest("debug", fixture.Source, stopOnEntry)
+			client.ExpectInitializedEvent(t)
+			client.ExpectLaunchResponse(t)
+			triggerStop(client, forceStop)
+			<-serverStopped
+			verifyServerStopped(t, server)
+		})
+	}
+}
+
+func TestSessionStop(t *testing.T) {
+	verifySessionState := func(t *testing.T, s *Session, binaryToRemoveSet bool, debuggerSet bool, disconnectChanSet bool) {
+		t.Helper()
+		if binaryToRemoveSet && s.binaryToRemove == "" || !binaryToRemoveSet && s.binaryToRemove != "" {
+			t.Errorf("binaryToRemove: got %s, want set=%v", s.binaryToRemove, binaryToRemoveSet)
+		}
+		if debuggerSet && s.debugger == nil || !debuggerSet && s.debugger != nil {
+			t.Errorf("debugger: got %v, want set=%v", s.debugger, debuggerSet)
+		}
+		if disconnectChanSet && s.config.DisconnectChan == nil || !disconnectChanSet && s.config.DisconnectChan != nil {
+			t.Errorf("disconnectChan: got %v, want set=%v", s.config.DisconnectChan, disconnectChanSet)
+		}
+	}
+	for name, stopSession := range map[string]func(s *Session, c *daptest.Client, serveDone chan struct{}){
+		"force": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			s.Close()
+			<-serveDone
+			verifySessionState(t, s, false /*binaryToRemoveSet*/, false /*debuggerSet*/, false /*disconnectChanSet*/)
+		},
+		"read error": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			c.Close()
+			<-serveDone
+			verifyConnStopped(t, s.conn)
+			verifySessionState(t, s, true /*binaryToRemoveSet*/, true /*debuggerSet*/, false /*disconnectChanSet*/)
+			s.Close()
+		},
+		"disconnect before exit": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			c.DisconnectRequest()
+			<-serveDone
+			verifyConnStopped(t, s.conn)
+			verifySessionState(t, s, true /*binaryToRemoveSet*/, false /*debuggerSet*/, false /*disconnectChanSet*/)
+			s.Close()
+		},
+		"disconnect after exit": func(s *Session, c *daptest.Client, serveDone chan struct{}) {
+			c.ContinueRequest(1)
+			c.ExpectContinueResponse(t)
+			c.ExpectTerminatedEvent(t)
+			c.DisconnectRequest()
+			<-serveDone
+			verifyConnStopped(t, s.conn)
+			verifySessionState(t, s, true /*binaryToRemoveSet*/, false /*debuggerSet*/, false /*disconnectChanSet*/)
+			s.Close()
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			listener, err := net.Listen("tcp", ":0")
+			if err != nil {
+				t.Fatalf("cannot setup listener required for testing: %v", err)
+			}
+			defer listener.Close()
+			acceptDone := make(chan struct{})
+			var conn net.Conn
+			go func() {
+				conn, err = listener.Accept()
+				close(acceptDone)
+			}()
+			time.Sleep(10 * time.Millisecond) // give time to start listening
+			client := daptest.NewClient(listener.Addr().String())
+			defer client.Close()
+			<-acceptDone
+			if err != nil {
+				t.Fatalf("cannot accept client requireed for testing: %v", err)
+			}
+			session := NewSession(conn, &Config{
+				Config:        &service.Config{DisconnectChan: make(chan struct{})},
+				StopTriggered: make(chan struct{})}, nil)
+			serveDAPCodecDone := make(chan struct{})
+			go func() {
+				session.ServeDAPCodec()
+				close(serveDAPCodecDone)
+			}()
+			time.Sleep(10 * time.Millisecond) // give time to start reading
+			client.InitializeRequest()
+			client.ExpectInitializeResponseAndCapabilities(t)
+			fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
+			client.LaunchRequest("debug", fixture.Source, stopOnEntry)
+			client.ExpectInitializedEvent(t)
+			client.ExpectLaunchResponse(t)
+			stopSession(session, client, serveDAPCodecDone)
+			verifySessionStopped(t, session)
+		})
+	}
 }
 
 func TestForceStopWhileStopping(t *testing.T) {
@@ -159,6 +310,7 @@ func TestForceStopWhileStopping(t *testing.T) {
 	time.Sleep(time.Microsecond)
 	close(forceStop) // depending on timing may trigger Stop()
 	<-serverStopped
+	verifyServerStopped(t, server)
 }
 
 // TestLaunchStopOnEntry emulates the message exchange that can be observed with
@@ -692,14 +844,14 @@ func checkStackFramesNamed(testName string, t *testing.T, got *dap.StackTraceRes
 // checkScope is a helper for verifying the values within a ScopesResponse.
 //     i - index of the scope within ScopesRespose.Body.Scopes array
 //     name - name of the scope
-//     varRef - reference to retrieve variables of this scope
+//     varRef - reference to retrieve variables of this scope. If varRef is negative, the reference is not checked.
 func checkScope(t *testing.T, got *dap.ScopesResponse, i int, name string, varRef int) {
 	t.Helper()
 	if len(got.Body.Scopes) <= i {
 		t.Errorf("\ngot  %d\nwant len(Scopes)>%d", len(got.Body.Scopes), i)
 	}
 	goti := got.Body.Scopes[i]
-	if goti.Name != name || goti.VariablesReference != varRef || goti.Expensive {
+	if goti.Name != name || (varRef >= 0 && goti.VariablesReference != varRef) || goti.Expensive {
 		t.Errorf("\ngot  %#v\nwant Name=%q VariablesReference=%d Expensive=false", goti, name, varRef)
 	}
 }
@@ -911,6 +1063,7 @@ func TestStackTraceRequest(t *testing.T) {
 					client.StackTraceRequest(1, 0, 0)
 					stResp = client.ExpectStackTraceResponse(t)
 					checkStackFramesExact(t, stResp, "main.main", 18, startHandle, 3, 3)
+
 				},
 				disconnect: false,
 			}})
@@ -1050,6 +1203,51 @@ func TestSelectedThreadsRequest(t *testing.T) {
 	})
 }
 
+func TestHideSystemGoroutinesRequest(t *testing.T) {
+	tests := []struct{ hideSystemGoroutines bool }{
+		{hideSystemGoroutines: true},
+		{hideSystemGoroutines: false},
+	}
+	for _, tt := range tests {
+		runTest(t, "goroutinestackprog", func(client *daptest.Client, fixture protest.Fixture) {
+			runDebugSessionWithBPs(t, client, "launch",
+				// Launch
+				func() {
+					client.LaunchRequestWithArgs(map[string]interface{}{
+						"mode":                 "exec",
+						"program":              fixture.Path,
+						"hideSystemGoroutines": tt.hideSystemGoroutines,
+						"stopOnEntry":          !stopOnEntry,
+					})
+				},
+				// Set breakpoints
+				fixture.Source, []int{25},
+				[]onBreakpoint{{
+					execute: func() {
+						checkStop(t, client, 1, "main.main", 25)
+
+						client.ThreadsRequest()
+						tr := client.ExpectThreadsResponse(t)
+
+						// The user process creates 10 goroutines in addition to the
+						// main goroutine, for a total of 11 goroutines.
+						userCount := 11
+						if tt.hideSystemGoroutines {
+							if len(tr.Body.Threads) != userCount {
+								t.Errorf("got %d goroutines, expected %d\n", len(tr.Body.Threads), userCount)
+							}
+						} else {
+							if len(tr.Body.Threads) <= userCount {
+								t.Errorf("got %d goroutines, expected >%d\n", len(tr.Body.Threads), userCount)
+							}
+						}
+					},
+					disconnect: true,
+				}})
+		})
+	}
+}
+
 // TestScopesAndVariablesRequests executes to a breakpoint and tests different
 // configurations of 'scopes' and 'variables' requests.
 func TestScopesAndVariablesRequests(t *testing.T) {
@@ -2074,7 +2272,7 @@ func TestGlobalScopeAndVariables(t *testing.T) {
 			// Launch
 			func() {
 				client.LaunchRequestWithArgs(map[string]interface{}{
-					"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
+					"mode": "exec", "program": fixture.Path, "showGlobalVariables": true, "showRegisters": true,
 				})
 			},
 			// Breakpoints are set within the program
@@ -2090,6 +2288,7 @@ func TestGlobalScopeAndVariables(t *testing.T) {
 					scopes := client.ExpectScopesResponse(t)
 					checkScope(t, scopes, 0, "Locals", localsScope)
 					checkScope(t, scopes, 1, "Globals (package main)", globalsScope)
+					checkScope(t, scopes, 2, "Registers", globalsScope+1)
 
 					client.VariablesRequest(globalsScope)
 					client.ExpectVariablesResponse(t)
@@ -2130,6 +2329,127 @@ func TestGlobalScopeAndVariables(t *testing.T) {
 	})
 }
 
+// TestRegisterScopeAndVariables launches the program with showRegisters
+// arg set, executes to a breakpoint in the main package and tests that the registers
+// got loaded. It then steps into a function in another package and tests that
+// the registers were updated by checking PC.
+func TestRegistersScopeAndVariables(t *testing.T) {
+	runTest(t, "consts", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequestWithArgs(map[string]interface{}{
+					"mode": "exec", "program": fixture.Path, "showRegisters": true,
+				})
+			},
+			// Breakpoints are set within the program
+			fixture.Source, []int{},
+			[]onBreakpoint{{
+				// Stop at line 36
+				execute: func() {
+					client.StackTraceRequest(1, 0, 20)
+					stack := client.ExpectStackTraceResponse(t)
+					checkStackFramesExact(t, stack, "main.main", 36, 1000, 3, 3)
+
+					client.ScopesRequest(1000)
+					scopes := client.ExpectScopesResponse(t)
+					checkScope(t, scopes, 0, "Locals", localsScope)
+					registersScope := localsScope + 1
+					checkScope(t, scopes, 1, "Registers", registersScope)
+
+					// Check that instructionPointer points to the InstructionPointerReference.
+					pc, err := getPC(t, client, 1)
+					if err != nil {
+						t.Error(pc)
+					}
+
+					client.VariablesRequest(registersScope)
+					vr := client.ExpectVariablesResponse(t)
+					if len(vr.Body.Variables) == 0 {
+						t.Fatal("no registers returned")
+					}
+					idx := findPcReg(vr.Body.Variables)
+					if idx < 0 {
+						t.Fatalf("got %#v, want a reg with instruction pointer", vr.Body.Variables)
+					}
+					pcReg := vr.Body.Variables[idx]
+					gotPc, err := strconv.ParseUint(pcReg.Value, 0, 64)
+					if err != nil {
+						t.Error(err)
+					}
+					name := strings.TrimSpace(pcReg.Name)
+					if gotPc != pc || pcReg.EvaluateName != fmt.Sprintf("_%s", strings.ToUpper(name)) {
+						t.Errorf("got %#v,\nwant Name=%s Value=%#x EvaluateName=%q", pcReg, name, pc, fmt.Sprintf("_%s", strings.ToUpper(name)))
+					}
+
+					// The program has no user-defined globals.
+					// Depending on the Go version, there might
+					// be some runtime globals (e.g. main..inittask)
+					// so testing for the total number is too fragile.
+
+					// Step into pkg.AnotherMethod()
+					client.StepInRequest(1)
+					client.ExpectStepInResponse(t)
+					client.ExpectStoppedEvent(t)
+
+					client.StackTraceRequest(1, 0, 20)
+					stack = client.ExpectStackTraceResponse(t)
+					checkStackFramesExact(t, stack, "", 13, 1000, 4, 4)
+
+					client.ScopesRequest(1000)
+					scopes = client.ExpectScopesResponse(t)
+					checkScope(t, scopes, 0, "Locals", localsScope)
+					checkScope(t, scopes, 1, "Registers", registersScope)
+
+					// Check that rip points to the InstructionPointerReference.
+					pc, err = getPC(t, client, 1)
+					if err != nil {
+						t.Error(pc)
+					}
+					client.VariablesRequest(registersScope)
+					vr = client.ExpectVariablesResponse(t)
+					if len(vr.Body.Variables) == 0 {
+						t.Fatal("no registers returned")
+					}
+
+					idx = findPcReg(vr.Body.Variables)
+					if idx < 0 {
+						t.Fatalf("got %#v, want a reg with instruction pointer", vr.Body.Variables)
+					}
+					pcReg = vr.Body.Variables[idx]
+					gotPc, err = strconv.ParseUint(pcReg.Value, 0, 64)
+					if err != nil {
+						t.Error(err)
+					}
+
+					if gotPc != pc || pcReg.EvaluateName != fmt.Sprintf("_%s", strings.ToUpper(name)) {
+						t.Errorf("got %#v,\nwant Name=%s Value=%#x EvaluateName=%q", pcReg, name, pc, fmt.Sprintf("_%s", strings.ToUpper(name)))
+					}
+				},
+				disconnect: false,
+			}})
+	})
+}
+
+func findPcReg(regs []dap.Variable) int {
+	for i, reg := range regs {
+		if isPcReg(reg) {
+			return i
+		}
+	}
+	return -1
+}
+
+func isPcReg(reg dap.Variable) bool {
+	pcRegNames := []string{"rip", "pc", "eip"}
+	for _, name := range pcRegNames {
+		if name == strings.TrimSpace(reg.Name) {
+			return true
+		}
+	}
+	return false
+}
+
 // TestShadowedVariables executes to a breakpoint and checks the shadowed
 // variable is named correctly.
 func TestShadowedVariables(t *testing.T) {
@@ -2523,7 +2843,9 @@ func TestHitBreakpointIds(t *testing.T) {
 					client.ContinueRequest(1)
 					client.ExpectContinueResponse(t)
 					se = client.ExpectStoppedEvent(t)
+
 					checkHitBreakpointIds(t, se, "function breakpoint", functionBps[1].Id)
+
 					checkStop(t, client, 1, "main.anotherFunction", 27)
 				},
 				disconnect: true,
@@ -2804,7 +3126,7 @@ func TestLogPoints(t *testing.T) {
 				execute: func() {
 					checkStop(t, client, 1, "main.main", 23)
 					bps := []int{6, 25, 27, 16}
-					logMessages := map[int]string{6: "in callme!", 16: "in callme2!"}
+					logMessages := map[int]string{6: "{i*2}: in callme!", 16: "in callme2!"}
 					client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages)
 					client.ExpectSetBreakpointsResponse(t)
 
@@ -2820,7 +3142,7 @@ func TestLogPoints(t *testing.T) {
 
 						client.ContinueRequest(1)
 						client.ExpectContinueResponse(t)
-						checkLogMessage(t, client.ExpectOutputEvent(t), 1, "in callme!", fixture.Source, 6)
+						checkLogMessage(t, client.ExpectOutputEvent(t), 1, fmt.Sprintf("%d: in callme!", i*2), fixture.Source, 6)
 					}
 					se := client.ExpectStoppedEvent(t)
 					if se.Body.Reason != "breakpoint" || se.Body.ThreadId != 1 {
@@ -2936,62 +3258,76 @@ func TestHaltPreventsAutoResume(t *testing.T) {
 	})
 }
 
-// TestConcurrentBreakpointsLogPoints executes to a breakpoint and then tests
-// that a breakpoint set in the main goroutine is hit the correct number of times
-// and log points set in the children goroutines produce the correct number of
-// output events.
+// TestConcurrentBreakpointsLogPoints tests that a breakpoint set in the main
+// goroutine is hit the correct number of times and log points set in the
+// children goroutines produce the correct number of output events.
 func TestConcurrentBreakpointsLogPoints(t *testing.T) {
 	if runtime.GOOS == "freebsd" {
 		t.SkipNow()
 	}
-	runTest(t, "goroutinestackprog", func(client *daptest.Client, fixture protest.Fixture) {
-		runDebugSessionWithBPs(t, client, "launch",
-			// Launch
-			func() {
-				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
-			},
-			// Set breakpoints
-			fixture.Source, []int{20},
-			[]onBreakpoint{{
-				// Stop at line 20
-				execute: func() {
-					checkStop(t, client, 1, "main.main", 20)
-					bps := []int{8, 23}
-					logMessages := map[int]string{8: "hello"}
-					client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages)
-					client.ExpectSetBreakpointsResponse(t)
-
-					client.ContinueRequest(1)
-					client.ExpectContinueResponse(t)
+	tests := []struct {
+		name        string
+		fixture     string
+		start       int
+		breakpoints []int
+	}{
+		{
+			name:        "source breakpoints",
+			fixture:     "goroutinestackprog",
+			breakpoints: []int{23},
+		},
+		{
+			name:    "hardcoded breakpoint",
+			fixture: "goroutinebreak",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			runTest(t, tt.fixture, func(client *daptest.Client, fixture protest.Fixture) {
+				client.InitializeRequest()
+				client.ExpectInitializeResponseAndCapabilities(t)
 
-					// There may be up to 1 breakpoint and any number of log points that are
-					// hit concurrently. We should get a stopped event everytime the breakpoint
-					// is hit and an output event for each log point hit.
-					var oeCount, seCount int
-					for oeCount < 10 || seCount < 10 {
-						switch m := client.ExpectMessage(t).(type) {
-						case *dap.StoppedEvent:
-							if m.Body.Reason != "breakpoint" || !m.Body.AllThreadsStopped || m.Body.ThreadId != 1 {
-								t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", m)
-							}
-							checkStop(t, client, 1, "main.main", 23)
-							seCount++
-							client.ContinueRequest(1)
-						case *dap.OutputEvent:
-							checkLogMessage(t, m, -1, "hello", fixture.Source, 8)
-							oeCount++
-						case *dap.ContinueResponse:
-						case *dap.TerminatedEvent:
-							t.Fatalf("\nexpected 10 output events and 10 stopped events, got %d output events and %d stopped events", oeCount, seCount)
-						default:
-							t.Fatalf("Unexpected message type: expect StoppedEvent, OutputEvent, or ContinueResponse, got %#v", m)
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+				client.ExpectInitializedEvent(t)
+				client.ExpectLaunchResponse(t)
+
+				bps := append([]int{8}, tt.breakpoints...)
+				logMessages := map[int]string{8: "hello"}
+				client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages)
+				client.ExpectSetBreakpointsResponse(t)
+
+				client.ConfigurationDoneRequest()
+				client.ExpectConfigurationDoneResponse(t)
+
+				// There may be up to 1 breakpoint and any number of log points that are
+				// hit concurrently. We should get a stopped event everytime the breakpoint
+				// is hit and an output event for each log point hit.
+				var oeCount, seCount int
+				for oeCount < 10 || seCount < 10 {
+					switch m := client.ExpectMessage(t).(type) {
+					case *dap.StoppedEvent:
+						if m.Body.Reason != "breakpoint" || !m.Body.AllThreadsStopped || m.Body.ThreadId != 1 {
+							t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", m)
 						}
+						seCount++
+						client.ContinueRequest(1)
+					case *dap.OutputEvent:
+						checkLogMessage(t, m, -1, "hello", fixture.Source, 8)
+						oeCount++
+					case *dap.ContinueResponse:
+					case *dap.TerminatedEvent:
+						t.Fatalf("\nexpected 10 output events and 10 stopped events, got %d output events and %d stopped events", oeCount, seCount)
+					default:
+						t.Fatalf("Unexpected message type: expect StoppedEvent, OutputEvent, or ContinueResponse, got %#v", m)
 					}
-					client.ExpectTerminatedEvent(t)
-				},
-				disconnect: false,
-			}})
-	})
+				}
+				// TODO(suzmue): The dap server may identify some false
+				// positives for hard coded breakpoints, so there may still
+				// be more stopped events.
+				client.DisconnectRequestWithKillOption(true)
+			})
+		})
+	}
 }
 
 func TestSetBreakpointWhileRunning(t *testing.T) {
@@ -3486,6 +3822,105 @@ func TestEvaluateRequest(t *testing.T) {
 	})
 }
 
+func formatConfig(depth int, showGlobals, showRegisters, hideSystemGoroutines bool, substitutePath [][2]string) string {
+	formatStr := `stackTraceDepth	%d
+showGlobalVariables	%v
+showRegisters	%v
+hideSystemGoroutines	%v
+substitutePath	%v
+`
+	return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, hideSystemGoroutines, substitutePath)
+}
+
+func TestEvaluateCommandRequest(t *testing.T) {
+	runTest(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			fixture.Source, []int{}, // Breakpoint set in the program
+			[]onBreakpoint{{ // Stop at first breakpoint
+				execute: func() {
+					checkStop(t, client, 1, "main.foobar", 66)
+
+					// Request help.
+					const dlvHelp = `The following commands are available:
+    help (alias: h) 	 Prints the help message.
+    config 	 Changes configuration parameters.
+
+Type help followed by a command for full documentation.
+`
+					client.EvaluateRequest("dlv help", 1000, "repl")
+					got := client.ExpectEvaluateResponse(t)
+					checkEval(t, got, dlvHelp, noChildren)
+
+					client.EvaluateRequest("dlv help config", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, msgConfig, noChildren)
+
+					// Test config.
+					client.EvaluateRequest("dlv config -list", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, formatConfig(50, false, false, false, [][2]string{}), noChildren)
+
+					// Read and modify showGlobalVariables.
+					client.EvaluateRequest("dlv config showGlobalVariables", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "showGlobalVariables\tfalse\n", noChildren)
+
+					client.ScopesRequest(1000)
+					scopes := client.ExpectScopesResponse(t)
+					if len(scopes.Body.Scopes) > 1 {
+						t.Errorf("\ngot  %#v\nwant len(scopes)=1 (Locals)", scopes)
+					}
+					checkScope(t, scopes, 0, "Locals", -1)
+
+					client.EvaluateRequest("dlv config showGlobalVariables true", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "showGlobalVariables\ttrue\n\nUpdated", noChildren)
+
+					client.EvaluateRequest("dlv config -list", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, formatConfig(50, true, false, false, [][2]string{}), noChildren)
+
+					client.ScopesRequest(1000)
+					scopes = client.ExpectScopesResponse(t)
+					if len(scopes.Body.Scopes) < 2 {
+						t.Errorf("\ngot  %#v\nwant len(scopes)=2 (Locals & Globals)", scopes)
+					}
+					checkScope(t, scopes, 0, "Locals", -1)
+					checkScope(t, scopes, 1, "Globals (package main)", -1)
+
+					// Read and modify substitutePath.
+					client.EvaluateRequest("dlv config substitutePath", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[]\n", noChildren)
+
+					client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "your/server/path"), 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[[my/client/path your/server/path]]\n\nUpdated", noChildren)
+
+					client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q %q", "my/client/path", "new/your/server/path"), 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[[my/client/path new/your/server/path]]\n\nUpdated", noChildren)
+
+					client.EvaluateRequest(fmt.Sprintf("dlv config substitutePath %q", "my/client/path"), 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					checkEval(t, got, "substitutePath\t[]\n\nUpdated", noChildren)
+
+					// Test bad inputs.
+					client.EvaluateRequest("dlv help bad", 1000, "repl")
+					client.ExpectErrorResponse(t)
+
+					client.EvaluateRequest("dlv bad", 1000, "repl")
+					client.ExpectErrorResponse(t)
+				},
+				disconnect: true,
+			}})
+	})
+}
+
 // From testvariables2 fixture
 const (
 	// As defined in the code
@@ -3851,6 +4286,30 @@ func TestNextAndStep(t *testing.T) {
 	})
 }
 
+func TestHardCodedBreakpoints(t *testing.T) {
+	runTest(t, "consts", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			fixture.Source, []int{28},
+			[]onBreakpoint{{ // Stop at line 28
+				execute: func() {
+					checkStop(t, client, 1, "main.main", 28)
+
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					se := client.ExpectStoppedEvent(t)
+					if se.Body.ThreadId != 1 || se.Body.Reason != "breakpoint" {
+						t.Errorf("\ngot  %#v\nwant ThreadId=1 Reason=\"breakpoint\"", se)
+					}
+				},
+				disconnect: false,
+			}})
+	})
+}
+
 // TestStepInstruction executes to a breakpoint and tests stepping
 // a single instruction
 func TestStepInstruction(t *testing.T) {
@@ -3938,13 +4397,13 @@ func TestStepInstruction(t *testing.T) {
 	})
 }
 
-func getPC(t *testing.T, client *daptest.Client, threadId int) (int64, error) {
+func getPC(t *testing.T, client *daptest.Client, threadId int) (uint64, error) {
 	client.StackTraceRequest(threadId, 0, 1)
 	st := client.ExpectStackTraceResponse(t)
 	if len(st.Body.StackFrames) < 1 {
 		t.Fatalf("\ngot  %#v\nwant len(stackframes) => 1", st)
 	}
-	return strconv.ParseInt(st.Body.StackFrames[0].InstructionPointerReference, 0, 64)
+	return strconv.ParseUint(st.Body.StackFrames[0].InstructionPointerReference, 0, 64)
 }
 
 func TestNextParked(t *testing.T) {
@@ -4430,23 +4889,6 @@ func TestFatalThrowBreakpoint(t *testing.T) {
 	})
 }
 
-func verifyStopLocation(t *testing.T, client *daptest.Client, thread int, name string, line int) {
-	t.Helper()
-
-	client.StackTraceRequest(thread, 0, 20)
-	st := client.ExpectStackTraceResponse(t)
-	if len(st.Body.StackFrames) < 1 {
-		t.Errorf("\ngot  %#v\nwant len(stackframes) => 1", st)
-	} else {
-		if line != -1 && st.Body.StackFrames[0].Line != line {
-			t.Errorf("\ngot  %#v\nwant Line=%d", st, line)
-		}
-		if st.Body.StackFrames[0].Name != name {
-			t.Errorf("\ngot  %#v\nwant Name=%q", st, name)
-		}
-	}
-}
-
 // checkStop covers the standard sequence of requests issued by
 // a client at a breakpoint or another non-terminal stop event.
 // The details have been tested by other tests,
@@ -4457,7 +4899,7 @@ func checkStop(t *testing.T, client *daptest.Client, thread int, fname string, l
 	client.ThreadsRequest()
 	client.ExpectThreadsResponse(t)
 
-	verifyStopLocation(t, client, thread, fname, line)
+	client.CheckStopLocation(t, thread, fname, line)
 
 	client.ScopesRequest(1000)
 	client.ExpectScopesResponse(t)
@@ -4566,6 +5008,15 @@ func TestLaunchDebugRequest(t *testing.T) {
 	rescueStderr := os.Stderr
 	r, w, _ := os.Pipe()
 	os.Stderr = w
+	done := make(chan struct{})
+
+	var err []byte
+
+	go func() {
+		err, _ = ioutil.ReadAll(r)
+		t.Log(string(err))
+		close(done)
+	}()
 
 	tmpBin := "__tmpBin"
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
@@ -4580,8 +5031,7 @@ func TestLaunchDebugRequest(t *testing.T) {
 	time.Sleep(100 * time.Millisecond)
 
 	w.Close()
-	err, _ := ioutil.ReadAll(r)
-	t.Log(string(err))
+	<-done
 	os.Stderr = rescueStderr
 
 	rmErrRe, _ := regexp.Compile(`could not remove .*\n`)
@@ -4974,7 +5424,7 @@ func TestPauseAndContinue(t *testing.T) {
 			fixture.Source, []int{6},
 			[]onBreakpoint{{
 				execute: func() {
-					verifyStopLocation(t, client, 1, "main.loop", 6)
+					client.CheckStopLocation(t, 1, "main.loop", 6)
 
 					// Continue resumes all goroutines, so thread id is ignored
 					client.ContinueRequest(12345)
@@ -5791,7 +6241,6 @@ func runTestWithDebugger(t *testing.T, dbg *debugger.Debugger, test func(c *dapt
 	server, _ := startDAPServer(t, serverStopped)
 	client := daptest.NewClient(server.listener.Addr().String())
 	time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
-	// TODO(polina): update once the server interface is refactored to take debugger as arg
 	server.sessionMu.Lock()
 	if server.session == nil {
 		t.Fatal("DAP session is not ready")
@@ -5885,22 +6334,23 @@ func TestAttachRemoteToRunningTargetContinueOnEntry(t *testing.T) {
 // TestMultiClient tests that that remote attach doesn't take down
 // the server in multi-client mode unless terminateDebugee is explicitely set.
 func TestAttachRemoteMultiClient(t *testing.T) {
-	closingClientSession := "Closing client session, but leaving multi-client DAP server running at"
+	closingClientSessionOnly := "Closing client session, but leaving multi-client DAP server running at"
 	detachingAndTerminating := "Detaching and terminating target process"
 	tests := []struct {
 		name              string
 		disconnectRequest func(client *daptest.Client)
 		expect            string
 	}{
-		{"default", func(c *daptest.Client) { c.DisconnectRequest() }, closingClientSession},
+		{"default", func(c *daptest.Client) { c.DisconnectRequest() }, closingClientSessionOnly},
 		{"terminate=true", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(true) }, detachingAndTerminating},
-		{"terminate=false", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(false) }, closingClientSession},
+		{"terminate=false", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(false) }, closingClientSessionOnly},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
 			serverStopped := make(chan struct{})
 			server, forceStop := startDAPServer(t, serverStopped)
 			client := daptest.NewClient(server.listener.Addr().String())
+			defer client.Close()
 			time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
 			server.sessionMu.Lock()
 			if server.session == nil {
@@ -5909,7 +6359,6 @@ func TestAttachRemoteMultiClient(t *testing.T) {
 			// DAP server doesn't support accept-multiclient, but we can use this
 			// hack to test the inner connection logic that can be used by a server that does.
 			server.session.config.AcceptMulti = true
-			// TODO(polina): update once the server interface is refactored to take debugger as arg
 			_, server.session.debugger = launchDebuggerWithTargetHalted(t, "increment")
 			server.sessionMu.Unlock()
 
@@ -5930,12 +6379,13 @@ func TestAttachRemoteMultiClient(t *testing.T) {
 			}
 			client.ExpectDisconnectResponse(t)
 			client.ExpectTerminatedEvent(t)
-			client.Close()
+			time.Sleep(10 * time.Millisecond) // give time for things to shut down
 
-			if tc.expect == closingClientSession {
-				// At this point a multi-client server is still running, but since
-				// it is a dap server, it cannot accept another client, so the only
-				// way to take down the server is too force kill it.
+			if tc.expect == closingClientSessionOnly {
+				// At this point a multi-client server is still running.
+				verifySessionStopped(t, server.session)
+				// Since it is a dap server, it cannot accept another client, so the only
+				// way to take down the server is to force-kill it.
 				close(forceStop)
 			}
 			<-serverStopped
@@ -6078,6 +6528,83 @@ func TestBadlyFormattedMessageToServer(t *testing.T) {
 	})
 }
 
+func TestParseLogPoint(t *testing.T) {
+	tests := []struct {
+		name           string
+		msg            string
+		wantTracepoint bool
+		wantFormat     string
+		wantArgs       []string
+		wantErr        bool
+	}{
+		// Test simple log messages.
+		{name: "simple string", msg: "hello, world!", wantTracepoint: true, wantFormat: "hello, world!"},
+		{name: "empty string", msg: "", wantTracepoint: false, wantErr: false},
+		// Test parse eval expressions.
+		{
+			name:           "simple eval",
+			msg:            "{x}",
+			wantTracepoint: true,
+			wantFormat:     "%s",
+			wantArgs:       []string{"x"},
+		},
+		{
+			name:           "type cast",
+			msg:            "hello {string(x)}",
+			wantTracepoint: true,
+			wantFormat:     "hello %s",
+			wantArgs:       []string{"string(x)"},
+		},
+		{
+			name:           "multiple eval",
+			msg:            "{x} {y} {z}",
+			wantTracepoint: true,
+			wantFormat:     "%s %s %s",
+			wantArgs:       []string{"x", "y", "z"},
+		},
+		{
+			name:           "eval expressions contain braces",
+			msg:            "{interface{}(x)} {myType{y}} {[]myType{{z}}}",
+			wantTracepoint: true,
+			wantFormat:     "%s %s %s",
+			wantArgs:       []string{"interface{}(x)", "myType{y}", "[]myType{{z}}"},
+		},
+		// Test parse errors.
+		{name: "empty evaluation", msg: "{}", wantErr: true},
+		{name: "empty space evaluation", msg: "{   \n}", wantErr: true},
+		{name: "open brace missing closed", msg: "{", wantErr: true},
+		{name: "closed brace missing open", msg: "}", wantErr: true},
+		{name: "open brace in expression", msg: `{m["{"]}`, wantErr: true},
+		{name: "closed brace in expression", msg: `{m["}"]}`, wantErr: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotTracepoint, gotLogMessage, err := parseLogPoint(tt.msg)
+			if gotTracepoint != tt.wantTracepoint {
+				t.Errorf("parseLogPoint() tracepoint = %v, wantTracepoint %v", gotTracepoint, tt.wantTracepoint)
+				return
+			}
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseLogPoint() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !tt.wantTracepoint {
+				return
+			}
+			if gotLogMessage == nil {
+				t.Errorf("parseLogPoint() gotLogMessage = nil, want log message")
+				return
+			}
+			if gotLogMessage.format != tt.wantFormat {
+				t.Errorf("parseLogPoint() gotFormat = %v, want %v", gotLogMessage.format, tt.wantFormat)
+			}
+			if !reflect.DeepEqual(gotLogMessage.args, tt.wantArgs) {
+				t.Errorf("parseLogPoint() gotArgs = %v, want %v", gotLogMessage.args, tt.wantArgs)
+			}
+		})
+	}
+}
+
 func TestDisassemble(t *testing.T) {
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
 		runDebugSessionWithBPs(t, client, "launch",