Repository: go-delve/delve

Issue:
Wrong handling of `string([]byte)` ?
I have a variable which is `[]byte`:

```
(dlv) p p.output
[]uint8 len: 26, cap: 16384, [116,121,112,101,...+22 more]
```

I want to see it as a string:

```
(dlv) p string(p.output)
"type T struct {\f\f\t�fldcom�...+-4 more"
```

First - why so short?  I have `max-array-values` = 4 and `max-string-len` = 256.  This is neither of those.

Second: What does `+-4` mean - At first I thought it was just 4, but:

```
(dlv) p string(p.output[8:])
"truct {\f\f\t�fldcom�...+-4 more"
```

???  I can only guess those mystery characters are screwing something up?  Here's raw data:

```
(dlv) p p.output[0:26]
[]uint8 len: 26, cap: 16384, [116,121,112,101,32,84,32,115,116,114,117,99,116,32,123,12,12,9,255,102,108,100,99,111,109,255]
```

My environment:

```
$ dlv version
Delve Debugger
Version: 1.25.0
Build: $Id: e323af07680631b9cbdcc9d02e0a37891d12d972 $
```

```
$ go version
go version go1.24.1 linux/amd64
```

```
$ uname -a
Linux thockin-glaptop4 6.12.27-1rodete1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.27-1rodete1 (2025-05-19) x86_64 GNU/Linux
```



Hints from issue comments:

Solution patch:
diff --git a/.teamcity/settings.kts b/.teamcity/settings.kts
index e1308443..71356faf 100644
--- a/.teamcity/settings.kts
+++ b/.teamcity/settings.kts
@@ -53,6 +53,9 @@ val targets = arrayOf(
 
         "linux/ppc64le/1.25",
 
+        "linux/riscv64/1.25",
+        "linux/riscv64/tip",
+
         "windows/amd64/1.25",
         "windows/amd64/tip",
 
@@ -269,7 +272,7 @@ class TestBuild(val os: String, val arch: String, val version: String, buildId:
             "386", "amd64" -> equals("teamcity.agent.jvm.os.arch", if (os == "mac") "x86_64" else "amd64")
             "arm64" -> equals("teamcity.agent.jvm.os.arch", "aarch64")
             "ppc64le" -> equals("teamcity.agent.jvm.os.arch", "ppc64le")
-            // "riscv64" -> equals("teamcity.agent.jvm.os.arch", "riscv64") // The riscv64 needs a builder
+            "riscv64" -> equals("teamcity.agent.jvm.os.arch", "riscv64")
             "loong64" -> equals("teamcity.agent.jvm.os.arch", "loongarch64")
         }
         when (os) {
diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md
index 6ff6fb10..9b8f7d4f 100644
--- a/Documentation/cli/README.md
+++ b/Documentation/cli/README.md
@@ -141,7 +141,7 @@ Aliases: b
 
 ## breakpoints
 Print out info for active breakpoints.
-	
+
 	breakpoints [-a]
 
 Specifying -a prints all physical breakpoint, including internal breakpoints.
@@ -150,9 +150,9 @@ Aliases: bp
 
 ## call
 Resumes process, injecting a function call (EXPERIMENTAL!!!)
-	
+
 	call [-unsafe] <function call expression>
-	
+
 Current limitations:
 - only pointers to stack-allocated objects can be passed as argument.
 - only some automatic type conversions are supported.
@@ -226,7 +226,7 @@ With the -hitcount option a condition on the breakpoint hit count can be set, th
 The -per-g-hitcount option works like -hitcount, but use per goroutine hitcount to compare with n.
 
 With the -clear option a condition on the breakpoint can removed.
-	
+
 The '% n' form means we should stop at the breakpoint when the hitcount is a multiple of n.
 
 Examples:
@@ -344,7 +344,7 @@ The core dump is always written in ELF, even on systems (windows, macOS) where t
 Open where you are in $DELVE_EDITOR or $EDITOR
 
 	edit [locspec]
-	
+
 If locspec is omitted edit will open the current source file in the editor, otherwise it will open the specified location.
 
 Aliases: ed
@@ -373,9 +373,9 @@ Aliases: x
 
 ## exit
 Exit the debugger.
-		
+
 	exit [-c]
-	
+
 When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.
 
 Aliases: quit q
@@ -443,34 +443,34 @@ To only display goroutines where the specified location contains (or does not co
 	curloc: filter by the location of the topmost stackframe (including frames inside private runtime functions)
 	goloc: filter by the location of the go instruction that created the goroutine
 	startloc: filter by the location of the start function
-	
+
 To only display goroutines that have (or do not have) the specified label key and value, use:
 
 	goroutines -with label key=value
 	goroutines -without label key=value
-	
+
 To only display goroutines that have (or do not have) the specified label key, use:
 
 	goroutines -with label key
 	goroutines -without label key
-	
+
 To only display goroutines that are running (or are not running) on a OS thread, use:
 
 
 	goroutines -with running
 	goroutines -without running
-	
+
 To only display user (or runtime) goroutines, use:
 
 	goroutines -with user
 	goroutines -without user
 
 CHANNELS
-	
+
 To only show goroutines waiting to send to or receive from a specific channel use:
 
 	goroutines -chan expr
-	
+
 Note that 'expr' must not contain spaces.
 
 GROUPING
@@ -512,7 +512,7 @@ Aliases: h
 
 ## libraries
 List loaded dynamic libraries.
-	
+
 	libraries [-d N]
 
 If used with the -d option it will re-attempt to download the debug symbols for library N, using debuginfod-find.
@@ -564,11 +564,14 @@ Executes a command when a breakpoint is hit.
 
 	on <breakpoint name or id> <command>
 	on <breakpoint name or id> -edit
-	
 
-Supported commands: print, stack, goroutine, trace and cond. 
+
+Supported commands: print, stack, goroutine, trace and cond.
+
+All custom starlark commands can also be used with the 'on' prefix. See [Documentation/cli/starlark.md](//github.com/go-delve/delve/tree/master/Documentation/cli/starlark.md) for more information.
+
 To convert a breakpoint into a tracepoint use:
-	
+
 	on <breakpoint name or id> trace
 
 The command 'on &lt;bp> cond &lt;cond-arguments>' is equivalent to 'cond &lt;bp> &lt;cond-arguments>'.
@@ -615,7 +618,7 @@ For recorded targets the command takes the following forms:
 	restart					resets to the start of the recording
 	restart [checkpoint]			resets the recording to the given checkpoint
 	restart -r [newargv...]	[redirects...]	re-records the target process
-	
+
 For live targets the command takes the following forms:
 
 	restart [newargv...] [redirects...]	restarts the process
@@ -654,7 +657,7 @@ See [Documentation/cli/expr.md](//github.com/go-delve/delve/tree/master/Document
 Executes a file containing a list of delve commands
 
 	source <path>
-	
+
 If path ends with the .star extension it will be interpreted as a starlark script. See [Documentation/cli/starlark.md](//github.com/go-delve/delve/tree/master/Documentation/cli/starlark.md) for the syntax.
 
 If path is a single '-' character an interactive starlark interpreter will start instead. Type 'exit' to exit.
@@ -783,9 +786,9 @@ If regex is specified only package variables with a name matching it will be ret
 
 ## watch
 Set watchpoint.
-	
+
 	watch [-r|-w|-rw] <expr>
-	
+
 	-r	stops when the memory location is read
 	-w	stops when the memory location is written
 	-rw	stops when the memory location is read or written
diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md
index bf1aac65..75119cfb 100644
--- a/Documentation/cli/starlark.md
+++ b/Documentation/cli/starlark.md
@@ -98,6 +98,23 @@ Otherwise arguments passed on the command line are interpreted as starlark expre
 
 If the command function has a doc string it will be used as a help message.
 
+## Using custom commands with the `on` prefix
+
+All custom starlark commands can be used with the `on` command (see `help on`) to execute when a breakpoint is hit:
+
+```python
+def command_my_trace(args):
+	"""Custom trace command that executes when a breakpoint is hit."""
+	print("Tracing:", args)
+```
+
+You can use this command like this:
+
+```
+(dlv) break main.go:10
+(dlv) on 1 my_trace
+```
+
 # Working with variables
 
 Variables of the target program can be accessed using `local_vars`, `function_args` or the `eval` functions. Each variable will be returned as a [Variable](https://pkg.go.dev/github.com/go-delve/delve/service/api#Variable) struct, with one special field: `Value`.
diff --git a/Documentation/faq.md b/Documentation/faq.md
index 8065ad48..ff8d628c 100644
--- a/Documentation/faq.md
+++ b/Documentation/faq.md
@@ -17,6 +17,7 @@ Currently Delve supports (GOOS / GOARCH):
 * linux / arm64 (AARCH64)
 * linux / 386
 * windows / amd64
+* windows / arm64
 * darwin (macOS) / amd64
 
 There is no planned ETA for support of other architectures or operating systems. Bugs tracking requested support are:
diff --git a/Documentation/usage/dlv_debug.md b/Documentation/usage/dlv_debug.md
index 5329c089..85f4f8b1 100644
--- a/Documentation/usage/dlv_debug.md
+++ b/Documentation/usage/dlv_debug.md
@@ -19,9 +19,9 @@ dlv debug [package] [flags]
 
 ```
       --continue        Continue the debugged process on start.
-      --rr-cleanup      Delete directory containing debug recording on detach. (default true)
   -h, --help            help for debug
       --output string   Output path for the binary.
+      --rr-cleanup      Delete directory containing debug recording on detach. (default true)
       --tty string      TTY to use for the target program
 ```
 
diff --git a/Documentation/usage/dlv_exec.md b/Documentation/usage/dlv_exec.md
index fbdb61b1..9afd5533 100644
--- a/Documentation/usage/dlv_exec.md
+++ b/Documentation/usage/dlv_exec.md
@@ -19,10 +19,10 @@ dlv exec <path/to/binary> [flags]
 ### Options
 
 ```
-      --continue      Continue the debugged process on start.
+      --continue     Continue the debugged process on start.
+  -h, --help         help for exec
       --rr-cleanup   Delete directory containing debug recording on detach. (default true)
-  -h, --help          help for exec
-      --tty string    TTY to use for the target program
+      --tty string   TTY to use for the target program
 ```
 
 ### Options inherited from parent commands
diff --git a/Documentation/usage/dlv_trace.md b/Documentation/usage/dlv_trace.md
index d4d16b25..be2d383c 100644
--- a/Documentation/usage/dlv_trace.md
+++ b/Documentation/usage/dlv_trace.md
@@ -23,7 +23,7 @@ dlv trace [package] regexp [flags]
 ```
       --ebpf               Trace using eBPF (experimental).
   -e, --exec string        Binary file to exec and trace.
-      --follow-calls int   Trace all children of the function to the required depth
+      --follow-calls int   Trace all children of the function to the required depth. Trace also supports defer functions and cases where functions are dynamically returned and passed as parameters.
   -h, --help               help for trace
       --output string      Output path for the binary.
   -p, --pid int            Pid to attach to.
diff --git a/_fixtures/multinamedreturns.go b/_fixtures/multinamedreturns.go
new file mode 100644
index 00000000..5a5ca98d
--- /dev/null
+++ b/_fixtures/multinamedreturns.go
@@ -0,0 +1,30 @@
+package main
+
+import "fmt"
+
+//go:noinline
+func ManyArgsWithNamedReturns(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p int) (sum int, product int) {
+	sum = a + b + c + d + e + f + g + h
+	product = 1
+
+	temp1 := i * j
+	temp2 := k * l
+	temp3 := m * n
+	temp4 := o * p
+
+	sum += temp1 + temp2 + temp3 + temp4
+
+	product = (a + 1) * (b + 1) * (c + 1) * (d + 1)
+	product += (e + f) * (g + h) * (i + j) * (k + l)
+	product += (m + n) * (o + p)
+
+	sum = sum * 2
+	product = product / 2
+
+	return sum, product
+}
+
+func main() {
+	sum, product := ManyArgsWithNamedReturns(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+	fmt.Println(sum, product)
+}
diff --git a/_fixtures/nestedbp.go b/_fixtures/nestedbp.go
new file mode 100644
index 00000000..39061429
--- /dev/null
+++ b/_fixtures/nestedbp.go
@@ -0,0 +1,20 @@
+package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("start")
+	a()
+	fmt.Println("end")
+}
+
+func a() {
+	fmt.Println("in a")
+	b()
+	fmt.Println("done a")
+}
+
+func b() {
+	fmt.Println("in b")
+	fmt.Println("done b")
+}
diff --git a/_fixtures/readmem_json.go b/_fixtures/readmem_json.go
new file mode 100644
index 00000000..857fa4e5
--- /dev/null
+++ b/_fixtures/readmem_json.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"runtime"
+	"strings"
+	"unsafe"
+)
+
+// Long json for MemoryReference
+func main() {
+	runtime.Breakpoint()
+
+	payload := strings.Repeat("AB", 2500)
+
+	b, _ := json.Marshal(struct {
+		Data string `json:"data"`
+	}{Data: payload})
+
+	bytesString := []byte("this\nis\nit")
+	nonprint := []byte{242, 243, 244, 245}
+	maps := map[string]string{"some": "non"}
+
+	jsonString := string(b)
+
+	hashed := sha256.Sum256(b)
+	jsonHash := hex.EncodeToString(hashed[:]) // used to validate fullness of a string
+
+	ptr := unsafe.StringData(jsonString)
+	jsonAddr := fmt.Sprintf("%p", ptr) // used to validate string address
+
+	runtime.Breakpoint()
+
+	fmt.Println(jsonString)
+	fmt.Println(jsonHash)
+	fmt.Println(jsonAddr)
+	fmt.Println(bytesString)
+	fmt.Println(nonprint)
+	fmt.Println(maps)
+}
diff --git a/_fixtures/test_allow_on.star b/_fixtures/test_allow_on.star
new file mode 100644
index 00000000..d5f588b1
--- /dev/null
+++ b/_fixtures/test_allow_on.star
@@ -0,0 +1,6 @@
+# Test fixture for custom commands with on prefix
+# All custom starlark commands automatically allow the on prefix
+
+def command_test_on_allowed(args):
+	"""Test command for on prefix functionality."""
+	print("test_on_allowed called with:", args)
diff --git a/_fixtures/test_custom_cmd_continue.star b/_fixtures/test_custom_cmd_continue.star
new file mode 100644
index 00000000..9cf13a56
--- /dev/null
+++ b/_fixtures/test_custom_cmd_continue.star
@@ -0,0 +1,15 @@
+# Test fixture for custom commands that trigger continue
+
+def command_test_cmd_before_continue(args):
+	"""Test command that runs before continue."""
+	print("BEFORE_CONTINUE")
+
+def command_test_continue_cmd(args):
+	"""Test command that calls continue."""
+	print("CONTINUE_CMD")
+
+	dlv_command("continue")
+
+def command_test_cmd_after_continue(args):
+	"""Test command that runs after continue (should not run)."""
+	print("AFTER_CONTINUE")
diff --git a/_fixtures/test_custom_cmd_nested.star b/_fixtures/test_custom_cmd_nested.star
new file mode 100644
index 00000000..248f3550
--- /dev/null
+++ b/_fixtures/test_custom_cmd_nested.star
@@ -0,0 +1,20 @@
+def command_test_bp1_before_continue(args):
+	"""Test command on BP1 before continue."""
+	print("BP1_BEFORE_CONTINUE")
+
+def command_test_bp1_continue_cmd(args):
+	"""Test command on BP1 that calls continue to hit BP2."""
+	print("BP1_CONTINUE_CMD")
+	dlv_command("continue")
+
+def command_test_bp1_after_continue(args):
+	"""Test command on BP1 after continue (should not run)."""
+	print("BP1_AFTER_CONTINUE")
+
+def command_test_bp2_cmd(args):
+	"""Test command on BP2 (should not run because BP1 invalidated state)."""
+	print("BP2_CMD_EXECUTED")
+
+def command_test_bp3_cmd(args):
+	"""Test command on BP3."""
+	print("BP3_CMD_EXECUTED")
diff --git a/_scripts/gen-capslock-all.go b/_scripts/gen-capslock-all.go
index 4e5d0688..9d6d69c2 100644
--- a/_scripts/gen-capslock-all.go
+++ b/_scripts/gen-capslock-all.go
@@ -26,7 +26,7 @@ var platforms = []Platform{
 	{GOOS: "darwin", GOARCH: "amd64"},
 	{GOOS: "darwin", GOARCH: "arm64"},
 	{GOOS: "windows", GOARCH: "amd64"},
-	{GOOS: "windows", GOARCH: "arm64", BuildTags: "exp.winarm64"},
+	{GOOS: "windows", GOARCH: "arm64"},
 }
 
 const (
diff --git a/_scripts/make.go b/_scripts/make.go
index f6e163cd..5f18e5ce 100644
--- a/_scripts/make.go
+++ b/_scripts/make.go
@@ -305,9 +305,6 @@ func tagFlags(isTest bool) string {
 		tags = append(tags, mactags)
 	}
 	if isTest {
-		if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" {
-			tags = append(tags, "exp.winarm64")
-		}
 		if runtime.GOOS == "linux" && runtime.GOARCH == "ppc64le" {
 			tags = append(tags, "exp.linuxppc64le")
 		}
diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go
index 48ec7a74..3f740659 100644
--- a/cmd/dlv/cmds/commands.go
+++ b/cmd/dlv/cmds/commands.go
@@ -369,7 +369,7 @@ only see the output of the trace operations you can redirect stdout.`,
 	must(traceCommand.RegisterFlagCompletionFunc("stack", cobra.NoFileCompletions))
 	traceCommand.Flags().String("output", "", "Output path for the binary.")
 	must(traceCommand.MarkFlagFilename("output"))
-	traceCommand.Flags().IntVarP(&traceFollowCalls, "follow-calls", "", 0, "Trace all children of the function to the required depth")
+	traceCommand.Flags().IntVarP(&traceFollowCalls, "follow-calls", "", 0, "Trace all children of the function to the required depth. Trace also supports defer functions and cases where functions are dynamically returned and passed as parameters.")
 	rootCommand.AddCommand(traceCommand)
 
 	coreCommand := &cobra.Command{
@@ -435,6 +435,7 @@ https://github.com/mozilla/rr
 			},
 			Run: func(cmd *cobra.Command, args []string) {
 				backend = "rr"
+				rrDelOnDetach = false
 				os.Exit(execute(0, []string{}, conf, args[0], debugger.ExecutingOther, args, buildFlags))
 			},
 			ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
diff --git a/pkg/dwarf/line/state_machine.go b/pkg/dwarf/line/state_machine.go
index fadf1578..18c0d20c 100644
--- a/pkg/dwarf/line/state_machine.go
+++ b/pkg/dwarf/line/state_machine.go
@@ -351,21 +351,16 @@ func (lineInfo *DebugLineInfo) PrologueEndPC(start, end uint64) (pc uint64, file
 	}
 }
 
-// FirstStmtForLine looks in the half open interval [start, end) for the
-// first PC address marked as stmt for the line at address 'start'.
-func (lineInfo *DebugLineInfo) FirstStmtForLine(start, end uint64) (pc uint64, file string, line int, ok bool) {
-	first := true
+// FirstStmt looks in the half open interval [start, end) for the
+// first PC address marked as stmt.
+func (lineInfo *DebugLineInfo) FirstStmt(start, end uint64) (pc uint64, file string, line int, ok bool) {
 	sm := lineInfo.stateMachineForEntry(start)
 	for {
 		if sm.valid {
 			if sm.address >= end {
 				return 0, "", 0, false
 			}
-			if first {
-				first = false
-				file, line = sm.file, sm.line
-			}
-			if sm.isStmt && sm.file == file && sm.line == line {
+			if sm.isStmt {
 				return sm.address, sm.file, sm.line, true
 			}
 		}
diff --git a/pkg/gobuild/gobuild.go b/pkg/gobuild/gobuild.go
index d1dc3ea2..4eb90d6a 100644
--- a/pkg/gobuild/gobuild.go
+++ b/pkg/gobuild/gobuild.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/go-delve/delve/pkg/config"
+	"github.com/go-delve/delve/pkg/logflags"
 )
 
 // Remove the file at path and issue a warning to stderr if this fails.
@@ -128,5 +129,6 @@ func gocommandExecCmd(command string, args ...string) (string, *exec.Cmd) {
 	allargs := []string{command}
 	allargs = append(allargs, args...)
 	goBuild := exec.Command("go", allargs...)
+	logflags.DebuggerLogger().Debugf("gobuild args: %v", allargs)
 	return strings.Join(append([]string{"go"}, allargs...), " "), goBuild
 }
diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go
index 9f5ffe24..e97a0aa0 100644
--- a/pkg/proc/bininfo.go
+++ b/pkg/proc/bininfo.go
@@ -38,7 +38,7 @@ import (
 	"github.com/go-delve/delve/pkg/logflags"
 	"github.com/go-delve/delve/pkg/proc/debuginfod"
 	"github.com/go-delve/delve/pkg/proc/evalop"
-	"github.com/hashicorp/golang-lru/v2"
+	lru "github.com/hashicorp/golang-lru/v2"
 )
 
 const (
@@ -405,7 +405,7 @@ func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error
 		// Look for the first instruction with the stmt flag set, so that setting a
 		// breakpoint with file:line and with the function name always result on
 		// the same instruction being selected.
-		if pc2, _, _, ok := fn.cu.lineInfo.FirstStmtForLine(fn.Entry, fn.End); ok {
+		if pc2, _, _, ok := fn.cu.lineInfo.FirstStmt(fn.Entry, fn.End); ok {
 			return pc2, nil
 		}
 	}
diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go
index 2514b5c7..8b508198 100644
--- a/pkg/proc/breakpoints.go
+++ b/pkg/proc/breakpoints.go
@@ -113,6 +113,11 @@ type Breaklet struct {
 	watchpoint *Breakpoint
 }
 
+// SetCallback sets the call back field, this was primarily added to prevent exporting callback field
+func (b *Breaklet) SetCallback(callback func(th Thread, p *Target) (bool, error)) {
+	b.callback = callback
+}
+
 // BreakpointKind determines the behavior of delve when the
 // breakpoint is reached.
 type BreakpointKind uint16
@@ -1085,6 +1090,8 @@ type LogicalBreakpoint struct {
 	LoadArgs    *LoadConfig
 	LoadLocals  *LoadConfig
 
+	CustomCommands []string // Custom starlark commands to execute when the breakpoint is hit
+
 	HitCount      map[int64]uint64 // Number of times a breakpoint has been reached in a certain goroutine
 	TotalHitCount uint64           // Number of times a breakpoint has been reached
 	HitCondPerG   bool             // Use per goroutine hitcount as HitCond operand, instead of total hitcount
diff --git a/pkg/proc/native/support_sentinel_windows.go b/pkg/proc/native/support_sentinel_windows.go
index 05c758e9..24894375 100644
--- a/pkg/proc/native/support_sentinel_windows.go
+++ b/pkg/proc/native/support_sentinel_windows.go
@@ -1,4 +1,4 @@
-//go:build windows && !amd64 && !(arm64 && exp.winarm64)
+//go:build windows && !amd64 && !arm64
 
 // This file is used to detect build on unsupported GOOS/GOARCH combinations.
 
diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go
index 607e5c6f..e665fe10 100644
--- a/pkg/proc/target_exec.go
+++ b/pkg/proc/target_exec.go
@@ -168,7 +168,11 @@ func (grp *TargetGroup) Continue() error {
 				log.Debugf("callInjection protocol on:")
 				for _, th := range threads {
 					regs, _ := th.Registers()
-					log.Debugf("\t%d PC=%#x", th.ThreadID(), regs.PC())
+					if regs != nil {
+						log.Debugf("\t%d PC=%#x", th.ThreadID(), regs.PC())
+					} else {
+						log.Debugf("\t%d (no registers)", th.ThreadID())
+					}
 				}
 			}
 			callInjectionDoneThis, callErrThis := callInjectionProtocol(dbp, trapthread, threads)
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index b8d29757..f13a3092 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -147,9 +147,9 @@ A tracepoint is a breakpoint that does not stop the execution of the program, in
 
 See also: "help on", "help cond" and "help clear"`},
 		{aliases: []string{"watch"}, group: breakCmds, cmdFn: watchpoint, helpMsg: `Set watchpoint.
-	
+
 	watch [-r|-w|-rw] <expr>
-	
+
 	-r	stops when the memory location is read
 	-w	stops when the memory location is written
 	-rw	stops when the memory location is read or written
@@ -171,7 +171,7 @@ For recorded targets the command takes the following forms:
 	restart					resets to the start of the recording
 	restart [checkpoint]			resets the recording to the given checkpoint
 	restart -r [newargv...]	[redirects...]	re-records the target process
-	
+
 For live targets the command takes the following forms:
 
 	restart [newargv...] [redirects...]	restarts the process
@@ -208,9 +208,9 @@ Optional [count] argument allows you to skip multiple lines.
 `},
 		{aliases: []string{"stepout", "so"}, group: runCmds, allowedPrefixes: revPrefix, cmdFn: c.stepout, helpMsg: "Step out of the current function."},
 		{aliases: []string{"call"}, group: runCmds, cmdFn: c.call, helpMsg: `Resumes process, injecting a function call (EXPERIMENTAL!!!)
-	
+
 	call [-unsafe] <function call expression>
-	
+
 Current limitations:
 - only pointers to stack-allocated objects can be passed as argument.
 - only some automatic type conversions are supported.
@@ -269,34 +269,34 @@ To only display goroutines where the specified location contains (or does not co
 	curloc: filter by the location of the topmost stackframe (including frames inside private runtime functions)
 	goloc: filter by the location of the go instruction that created the goroutine
 	startloc: filter by the location of the start function
-	
+
 To only display goroutines that have (or do not have) the specified label key and value, use:
 
 	goroutines -with label key=value
 	goroutines -without label key=value
-	
+
 To only display goroutines that have (or do not have) the specified label key, use:
 
 	goroutines -with label key
 	goroutines -without label key
-	
+
 To only display goroutines that are running (or are not running) on a OS thread, use:
 
 
 	goroutines -with running
 	goroutines -without running
-	
+
 To only display user (or runtime) goroutines, use:
 
 	goroutines -with user
 	goroutines -without user
 
 CHANNELS
-	
+
 To only show goroutines waiting to send to or receive from a specific channel use:
 
 	goroutines -chan expr
-	
+
 Note that 'expr' must not contain spaces.
 
 GROUPING
@@ -334,7 +334,7 @@ Called without arguments it will show information about the current goroutine.
 Called with a single argument it will switch to the specified goroutine.
 Called with more arguments it will execute a command on the specified goroutine.`},
 		{aliases: []string{"breakpoints", "bp"}, group: breakCmds, cmdFn: breakpoints, helpMsg: `Print out info for active breakpoints.
-	
+
 	breakpoints [-a]
 
 Specifying -a prints all physical breakpoint, including internal breakpoints.`},
@@ -396,9 +396,9 @@ If regex is specified only package variables with a name matching it will be ret
 
 Argument -a shows more registers. Individual registers can also be displayed by 'print' and 'display'. See Documentation/cli/expr.md.`},
 		{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: `Exit the debugger.
-		
+
 	exit [-c]
-	
+
 When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.`},
 		{aliases: []string{"list", "ls", "l"}, cmdFn: listCommand, helpMsg: `Show source code.
 
@@ -468,7 +468,7 @@ Executes the specified command (print, args, locals) in the context of the n-th
 		{aliases: []string{"source"}, cmdFn: c.sourceCommand, helpMsg: `Executes a file containing a list of delve commands
 
 	source <path>
-	
+
 If path ends with the .star extension it will be interpreted as a starlark script. See Documentation/cli/starlark.md for the syntax.
 
 If path is a single '-' character an interactive starlark interpreter will start instead. Type 'exit' to exit.`},
@@ -484,11 +484,14 @@ If no argument is specified the function being executed in the selected stack fr
 
 	on <breakpoint name or id> <command>
 	on <breakpoint name or id> -edit
-	
 
-Supported commands: print, stack, goroutine, trace and cond. 
+
+Supported commands: print, stack, goroutine, trace and cond.
+
+All custom starlark commands can also be used with the 'on' prefix. See Documentation/cli/starlark.md for more information.
+
 To convert a breakpoint into a tracepoint use:
-	
+
 	on <breakpoint name or id> trace
 
 The command 'on <bp> cond <cond-arguments>' is equivalent to 'cond <bp> <cond-arguments>'.
@@ -518,7 +521,7 @@ With the -hitcount option a condition on the breakpoint hit count can be set, th
 The -per-g-hitcount option works like -hitcount, but use per goroutine hitcount to compare with n.
 
 With the -clear option a condition on the breakpoint can removed.
-	
+
 The '% n' form means we should stop at the breakpoint when the hitcount is a multiple of n.
 
 Examples:
@@ -567,10 +570,10 @@ Adds, removes or clears debug-info-directories.`},
 		{aliases: []string{"edit", "ed"}, cmdFn: edit, helpMsg: `Open where you are in $DELVE_EDITOR or $EDITOR
 
 	edit [locspec]
-	
+
 If locspec is omitted edit will open the current source file in the editor, otherwise it will open the specified location.`},
 		{aliases: []string{"libraries"}, cmdFn: libraries, helpMsg: `List loaded dynamic libraries.
-	
+
 	libraries [-d N]
 
 If used with the -d option it will re-attempt to download the debug symbols for library N, using debuginfod-find.`},
@@ -729,7 +732,11 @@ func (c *Commands) CallWithContext(cmdstr string, t *Term, ctx callContext) erro
 	if len(vals) > 1 {
 		args = strings.TrimSpace(vals[1])
 	}
-	return c.Find(cmdname, ctx.Prefix).cmdFn(t, ctx, args)
+	cmd := c.Find(cmdname, ctx.Prefix)
+	if t != nil && len(t.customCommandsInvalidated) > 0 && cmd.group == runCmds {
+		t.customCommandsInvalidated[len(t.customCommandsInvalidated)-1] = true
+	}
+	return cmd.cmdFn(t, ctx, args)
 }
 
 // Call takes a command to execute.
@@ -1492,7 +1499,6 @@ func stepInstruction(t *Term, ctx callContext, frame int, skipCalls bool) error
 	}
 
 	defer t.onStop()
-
 	var fn func(bool) (*api.DebuggerState, error)
 	if ctx.Prefix == revPrefix {
 		fn = t.client.ReverseStepInstruction
@@ -1740,6 +1746,9 @@ func formatBreakpointAttrs(prefix string, bp *api.Breakpoint, includeTrace bool)
 	for i := range bp.Variables {
 		attrs = append(attrs, fmt.Sprintf("%sprint %s", prefix, bp.Variables[i]))
 	}
+	for i := range bp.CustomCommands {
+		attrs = append(attrs, fmt.Sprintf("%s%s", prefix, bp.CustomCommands[i]))
+	}
 	if includeTrace && bp.Tracepoint {
 		attrs = append(attrs, fmt.Sprintf("%strace", prefix))
 	}
@@ -2841,6 +2850,33 @@ func printcontextThread(t *Term, th *api.Thread) {
 	printBreakpointInfo(t, th, false)
 }
 
+// executeBreakpointCustomCommands executes custom starlark commands
+// associated with breakpoints for any thread stopped at a breakpoint.
+func (c *Commands) executeBreakpointCustomCommands(t *Term) {
+	state, err := t.client.GetState()
+	if err != nil || state == nil {
+		return
+	}
+
+	t.customCommandsInvalidated = append(t.customCommandsInvalidated, false)
+	defer func() { t.customCommandsInvalidated = t.customCommandsInvalidated[:len(t.customCommandsInvalidated)-1] }()
+
+	for _, th := range state.Threads {
+		if th.Breakpoint == nil || len(th.Breakpoint.CustomCommands) == 0 {
+			continue
+		}
+		for _, cmdName := range th.Breakpoint.CustomCommands {
+			err := c.Call(cmdName, t)
+			if err != nil {
+				fmt.Fprintf(t.stdout, "Error executing custom command %s: %v\n", cmdName, err)
+			}
+			if t.customCommandsInvalidated[len(t.customCommandsInvalidated)-1] {
+				return
+			}
+		}
+	}
+}
+
 func printBreakpointInfo(t *Term, th *api.Thread, tracepointOnNewline bool) {
 	if th.BreakpointInfo == nil {
 		return
@@ -2894,6 +2930,7 @@ func printBreakpointInfo(t *Term, th *api.Thread, tracepointOnNewline bool) {
 			printStack(t, t.stdout, bpi.Stacktrace, "\t\t", false)
 		}
 	}
+
 }
 
 func printTracepoint(t *Term, th *api.Thread, bpname string, fn *api.Function, args string, hasReturnValue bool) {
diff --git a/pkg/terminal/starbind/starlark.go b/pkg/terminal/starbind/starlark.go
index 789a3c6c..dfb23ff3 100644
--- a/pkg/terminal/starbind/starlark.go
+++ b/pkg/terminal/starbind/starlark.go
@@ -47,7 +47,7 @@ var defaultSyntaxFileOpts = &syntax.FileOptions{
 // It contains methods to call API functions, command line commands, etc.
 type Context interface {
 	Client() service.Client
-	RegisterCommand(name, helpMsg string, cmdfn func(args string) error)
+	RegisterCommand(name, helpMsg string, cmdfn func(args string) error, allowedPrefixes int)
 	CallCommand(cmdstr string) error
 	Scope() api.EvalScope
 	LoadConfig() api.LoadConfig
@@ -298,12 +298,15 @@ func (env *Env) createCommand(name string, val starlark.Value) error {
 		helpMsg = "user defined"
 	}
 
+	// All custom starlark commands are allowed to use the on prefix
+	allowedPrefixes := 2 // onPrefix
+
 	if fnval.NumParams() == 1 {
 		if p0, _ := fnval.Param(0); p0 == "args" {
 			env.ctx.RegisterCommand(name, helpMsg, func(args string) error {
 				_, err := starlark.Call(env.newThread(), fnval, starlark.Tuple{starlark.String(args)}, nil)
 				return err
-			})
+			}, allowedPrefixes)
 			return nil
 		}
 	}
@@ -320,7 +323,7 @@ func (env *Env) createCommand(name string, val starlark.Value) error {
 		}
 		_, err = starlark.Call(thread, fnval, argtuple, nil)
 		return err
-	})
+	}, allowedPrefixes)
 	return nil
 }
 
diff --git a/pkg/terminal/starlark.go b/pkg/terminal/starlark.go
index 3616015d..c28d01d8 100644
--- a/pkg/terminal/starlark.go
+++ b/pkg/terminal/starlark.go
@@ -1,10 +1,11 @@
 package terminal
 
 import (
+	"slices"
+
 	"github.com/go-delve/delve/pkg/terminal/starbind"
 	"github.com/go-delve/delve/service"
 	"github.com/go-delve/delve/service/api"
-	"slices"
 )
 
 type starlarkContext struct {
@@ -17,8 +18,20 @@ func (ctx starlarkContext) Client() service.Client {
 	return ctx.term.client
 }
 
-func (ctx starlarkContext) RegisterCommand(name, helpMsg string, fn func(args string) error) {
-	cmdfn := func(t *Term, ctx callContext, args string) error {
+func (ctx starlarkContext) RegisterCommand(name, helpMsg string, fn func(args string) error, allowedPrefixes int) {
+	cmdfn := func(t *Term, callCtx callContext, args string) error {
+		// If called with onPrefix, add the full command (name + args) to the breakpoint's CustomCommands
+		if callCtx.Prefix == onPrefix {
+			if callCtx.Breakpoint == nil {
+				return nil
+			}
+			fullCmd := name
+			if args != "" {
+				fullCmd = name + " " + args
+			}
+			callCtx.Breakpoint.CustomCommands = append(callCtx.Breakpoint.CustomCommands, fullCmd)
+			return nil
+		}
 		return fn(args)
 	}
 
@@ -28,6 +41,7 @@ func (ctx starlarkContext) RegisterCommand(name, helpMsg string, fn func(args st
 		if slices.Contains(cmd.aliases, name) {
 			cmd.cmdFn = cmdfn
 			cmd.helpMsg = helpMsg
+			cmd.allowedPrefixes = cmdPrefix(allowedPrefixes)
 			found = true
 		}
 		if found {
@@ -36,9 +50,10 @@ func (ctx starlarkContext) RegisterCommand(name, helpMsg string, fn func(args st
 	}
 	if !found {
 		newcmd := command{
-			aliases: []string{name},
-			helpMsg: helpMsg,
-			cmdFn:   cmdfn,
+			aliases:         []string{name},
+			helpMsg:         helpMsg,
+			cmdFn:           cmdfn,
+			allowedPrefixes: cmdPrefix(allowedPrefixes),
 		}
 		ctx.term.cmds.cmds = append(ctx.term.cmds.cmds, newcmd)
 	}
diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go
index 9b28b169..3d3eba9d 100644
--- a/pkg/terminal/terminal.go
+++ b/pkg/terminal/terminal.go
@@ -86,6 +86,8 @@ type Term struct {
 	downloadsInProgress bool
 
 	goVersionCache *goversion.GoVersion
+	// customCommandsInvalidated is set when a runCmd is executed during custom command execution
+	customCommandsInvalidated []bool
 }
 
 type displayEntry struct {
@@ -654,6 +656,7 @@ func (t *Term) printDisplays() {
 
 func (t *Term) onStop() {
 	t.printDisplays()
+	t.cmds.executeBreakpointCustomCommands(t)
 }
 
 func (t *Term) longCommandCancel() {
diff --git a/service/api/conversions.go b/service/api/conversions.go
index 97b09b45..9d8a9116 100644
--- a/service/api/conversions.go
+++ b/service/api/conversions.go
@@ -28,6 +28,7 @@ func ConvertLogicalBreakpoint(lbp *proc.LogicalBreakpoint) *Breakpoint {
 		Variables:        lbp.Variables,
 		LoadArgs:         LoadConfigFromProc(lbp.LoadArgs),
 		LoadLocals:       LoadConfigFromProc(lbp.LoadLocals),
+		CustomCommands:   lbp.CustomCommands,
 		TotalHitCount:    lbp.TotalHitCount,
 		Disabled:         !lbp.Enabled(),
 		UserData:         lbp.UserData,
diff --git a/service/api/types.go b/service/api/types.go
index ed2af6dc..88c22b79 100644
--- a/service/api/types.go
+++ b/service/api/types.go
@@ -2,11 +2,13 @@ package api
 
 import (
 	"bytes"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"reflect"
 	"strconv"
 	"unicode"
+	"unicode/utf8"
 
 	"github.com/go-delve/delve/pkg/proc"
 )
@@ -118,6 +120,8 @@ type Breakpoint struct {
 	LoadArgs *LoadConfig
 	// LoadLocals requests loading function locals when the breakpoint is hit
 	LoadLocals *LoadConfig
+	// CustomCommands are custom starlark commands to execute when the breakpoint is hit
+	CustomCommands []string `json:"customCommands,omitempty"`
 
 	// WatchExpr is the expression used to create this watchpoint
 	WatchExpr string
@@ -326,6 +330,14 @@ type Variable struct {
 	// Function variables will store the name of the function in this field
 	Value string `json:"value"`
 
+	// StringValueBytes contains the Value for string variables that can not be
+	// represented in unicode.
+	// Specifically if v.Kind is reflect.String and v.Value is not a valid
+	// unicode string the JSON marshaller will convert it to a valid unicode
+	// string, altering its contents. This fields contains the unaltered string
+	// contents.
+	StringValueBytes []byte
+
 	// Number of elements in an array or a slice, number of keys for a map, number of struct members for a struct, length of strings, number of captured variables for functions
 	Len int64 `json:"len"`
 	// Cap value for slices
@@ -353,6 +365,36 @@ type Variable struct {
 	DeclLine int64
 }
 
+type variableWithoutMarshaler Variable
+
+func (v *Variable) MarshalJSON() (text []byte, err error) {
+	var v2 *variableWithoutMarshaler = (*variableWithoutMarshaler)(v)
+	if v2.Kind == reflect.String && !utf8.ValidString(v2.Value) {
+		value := v2.Value
+		// Should we blot out the value with something obviously wrong? Like:
+		//
+		// 	v2.Value = "<invalid-utf8-string>"
+		// 	defer func() {
+		// 		v2.Value = value
+		// 	}()
+		v2.StringValueBytes = []byte(value)
+	}
+	return json.Marshal(v2)
+}
+
+func (v *Variable) UnmarshalJSON(buf []byte) error {
+	var v2 *variableWithoutMarshaler = (*variableWithoutMarshaler)(v)
+	err := json.Unmarshal(buf, v2)
+	if err != nil {
+		return err
+	}
+	if v.Kind == reflect.String && len(v.StringValueBytes) > 0 {
+		v.Value = string(v.StringValueBytes)
+		v.StringValueBytes = nil
+	}
+	return nil
+}
+
 // LoadConfig describes how to load values from target's memory
 type LoadConfig struct {
 	// FollowPointers requests pointers to be automatically dereferenced.
diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go
index 72223291..598e2258 100644
--- a/service/dap/error_ids.go
+++ b/service/dap/error_ids.go
@@ -28,6 +28,7 @@ const (
 	UnableToDisassemble        = 2013
 	UnableToListRegisters      = 2014
 	UnableToRunDlvCommand      = 2015
+	UnableToReadMemory         = 2016
 
 	// Add more codes as we support more requests
 
diff --git a/service/dap/server.go b/service/dap/server.go
index 99920cae..da4f7665 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -11,6 +11,7 @@ package dap
 import (
 	"bufio"
 	"bytes"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -104,6 +105,71 @@ type Server struct {
 	sessionMu sync.Mutex
 }
 
+// memRef describe address and its size to stream data from
+type memRef struct {
+	addr uint64
+	size int64
+}
+
+type referencesCollection struct {
+	mu   sync.Mutex
+	refs map[string]memRef
+}
+
+func (r *referencesCollection) get(reference string) (memRef, bool) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	ref, ok := r.refs[reference]
+
+	return ref, ok
+}
+
+func isAddressable(v *proc.Variable) bool {
+	if v == nil || v.Unreadable != nil {
+		return false
+	}
+
+	switch v.Kind {
+	case reflect.Slice, reflect.String:
+		return true
+	}
+
+	return false
+}
+
+func (r *referencesCollection) put(v *proc.Variable) string {
+	if !isAddressable(v) {
+		return ""
+	}
+
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	if r.refs == nil {
+		r.refs = make(map[string]memRef)
+	}
+
+	addr := v.Addr
+	if v.Base != 0 {
+		addr = v.Base
+	}
+
+	ref := fmt.Sprintf("0x%x", addr)
+	r.refs[ref] = memRef{addr: addr, size: v.Len}
+
+	return ref
+}
+
+func (r *referencesCollection) reset() {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	if len(r.refs) > 0 {
+		r.refs = make(map[string]memRef)
+	}
+}
+
 // Session is an abstraction for serving and shutting down
 // a DAP debug session with a pre-connected client.
 // TODO(polina): move this to a different file/package
@@ -119,6 +185,8 @@ type Session struct {
 	// Reset at every stop.
 	// See also comment for convertVariable.
 	variableHandles *handlesMap[*fullyQualifiedVariable]
+	// referencesCollection track references map for DAP client
+	referencesCollection referencesCollection
 	// args tracks special settings for handling debug session requests.
 	args launchAttachArgs
 	// exceptionErr tracks the runtime error that last occurred.
@@ -774,7 +842,7 @@ func (s *Session) handleRequest(request dap.Message) {
 	case *dap.LoadedSourcesRequest: // Optional (capability 'supportsLoadedSourcesRequest')
 		/*TODO*/ s.onLoadedSourcesRequest(request) // Not yet implemented
 	case *dap.ReadMemoryRequest: // Optional (capability 'supportsReadMemoryRequest')
-		/*TODO*/ s.onReadMemoryRequest(request) // Not yet implemented
+		s.onReadMemoryRequest(request)
 	case *dap.CancelRequest: // Optional (capability 'supportsCancelRequest')
 		/*TODO*/ s.onCancelRequest(request) // Not yet implemented (does this make sense?)
 	case *dap.ModulesRequest: // Optional (capability 'supportsModulesRequest')
@@ -871,7 +939,7 @@ func (s *Session) onInitializeRequest(request *dap.InitializeRequest) {
 	response.Body.SupportsRestartRequest = true
 	response.Body.SupportsSetExpression = false
 	response.Body.SupportsLoadedSourcesRequest = false
-	response.Body.SupportsReadMemoryRequest = false
+	response.Body.SupportsReadMemoryRequest = true
 	response.Body.SupportsCancelRequest = false
 	response.Body.ExceptionBreakpointFilters = []dap.ExceptionBreakpointsFilter{
 		{Filter: proc.UnrecoveredPanic, Label: "Unrecovered Panics", Default: true},
@@ -2656,6 +2724,7 @@ func (s *Session) childrenToDAPVariables(v *fullyQualifiedVariable) []dap.Variab
 				VariablesReference: cvarref,
 				IndexedVariables:   getIndexedVariableCount(c),
 				NamedVariables:     getNamedVariableCount(c),
+				MemoryReference:    s.referencesCollection.put(c),
 			}
 		}
 	}
@@ -3355,10 +3424,92 @@ func (s *Session) onLoadedSourcesRequest(request *dap.LoadedSourcesRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
-// onReadMemoryRequest sends a not-yet-implemented error response.
-// Capability 'supportsReadMemoryRequest' is not set 'initialize' response.
+// onReadMemoryRequest handles DAP read memory requests
 func (s *Session) onReadMemoryRequest(request *dap.ReadMemoryRequest) {
-	s.sendNotYetImplementedErrorResponse(request.Request)
+	args := request.Arguments
+
+	if args.Count < 0 {
+		s.sendErrorResponse(request.Request, UnableToReadMemory, "Unable to read memory", "negative count")
+		return
+	}
+
+	ref, ok := s.referencesCollection.get(args.MemoryReference)
+	if !ok {
+		s.sendErrorResponse(request.Request, UnableToReadMemory, "Unable to read memory", "unknown memoryReference")
+		return
+	}
+
+	if args.Count == 0 {
+		s.send(makeReadMemoryResponse(request.Request, ref.addr, nil, 0))
+		return
+	}
+
+	endReq := int64(args.Offset + args.Count)
+
+	startRead := min(max(int64(args.Offset), 0), ref.size)
+	endRead := min(max(endReq, 0), ref.size)
+
+	memAddr := ref.addr + uint64(startRead)
+
+	readCount := endRead - startRead
+	if readCount <= 0 {
+		unreadable := max(args.Count, 0)
+
+		s.send(makeReadMemoryResponse(request.Request, memAddr, nil, unreadable))
+		return
+	}
+
+	unreadable := max(int64(args.Count)-readCount, 0)
+
+	data, n, err := s.readTargetMemory(memAddr, readCount)
+	if err != nil {
+		s.sendErrorResponse(request.Request, UnableToReadMemory, "Unable to read memory", err.Error())
+		return
+	}
+
+	if int64(n) < readCount {
+		unreadable += readCount - int64(n)
+	}
+
+	s.send(makeReadMemoryResponse(request.Request, memAddr, data, int(unreadable)))
+}
+
+func (s *Session) readTargetMemory(addr uint64, count int64) (data []byte, n int, err error) {
+	if count <= 0 {
+		return nil, 0, nil
+	}
+
+	buf := make([]byte, count)
+
+	tgrp, unlock := s.debugger.LockTargetGroup()
+	defer unlock()
+
+	n, err = tgrp.Selected.Memory().ReadMemory(buf, addr)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	if n > 0 {
+		data = buf[:n]
+	}
+
+	return data, n, nil
+}
+
+func makeReadMemoryResponse(req dap.Request, addr uint64, data []byte, unreadable int) *dap.ReadMemoryResponse {
+	var response string
+	if len(data) > 0 {
+		response = base64.StdEncoding.EncodeToString(data)
+	}
+
+	return &dap.ReadMemoryResponse{
+		Response: *newResponse(req),
+		Body: dap.ReadMemoryResponseBody{
+			Address:         fmt.Sprintf("%#x", addr),
+			Data:            response,
+			UnreadableBytes: unreadable,
+		},
+	}
 }
 
 var invalidInstruction = dap.DisassembledInstruction{
@@ -3814,6 +3965,7 @@ Use 'Continue' to resume the original step command.`
 func (s *Session) resetHandlesForStoppedEvent() {
 	s.stackFrameHandles.reset()
 	s.variableHandles.reset()
+	s.referencesCollection.reset()
 	s.exceptionErr = nil
 }
 
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 9551a6b1..5f0d60c5 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -409,13 +409,10 @@ func (d *Debugger) LastModified() time.Time {
 	return d.target.Selected.BinInfo().LastModified()
 }
 
-// FunctionReturnLocations returns all return locations
-// for the given function, a list of addresses corresponding
-// to 'ret' or 'call runtime.deferreturn'.
-func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
-	d.targetMutex.Lock()
-	defer d.targetMutex.Unlock()
-
+// functionReturnLocationsInternal is same as FunctionReturnLocations
+// except that it does not have a lock and unlock as its called from
+// within the callback which has already acquired a lock.
+func (d *Debugger) functionReturnLocationsInternal(fnName string) ([]uint64, error) {
 	if len(d.target.Targets()) > 1 {
 		return nil, ErrNotImplementedWithMultitarget
 	}
@@ -454,6 +451,15 @@ func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
 	return addrs, nil
 }
 
+// FunctionReturnLocations returns all return locations
+// for the given function, a list of addresses corresponding
+// to 'ret' or 'call runtime.deferreturn'.
+func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
+	d.targetMutex.Lock()
+	defer d.targetMutex.Unlock()
+	return d.functionReturnLocationsInternal(fnName)
+}
+
 // Detach detaches from the target process.
 // If `kill` is true we will kill the process after
 // detaching.
@@ -900,6 +906,7 @@ func (d *Debugger) copyLogicalBreakpointInfo(lbp *proc.LogicalBreakpoint, reques
 	lbp.Variables = requested.Variables
 	lbp.LoadArgs = api.LoadConfigToProc(requested.LoadArgs)
 	lbp.LoadLocals = api.LoadConfigToProc(requested.LoadLocals)
+	lbp.CustomCommands = requested.CustomCommands
 	lbp.UserData = requested.UserData
 	lbp.RootFuncName = requested.RootFuncName
 	lbp.TraceFollowCalls = requested.TraceFollowCalls
@@ -1371,7 +1378,7 @@ func (d *Debugger) Functions(filter string, followCalls int) ([]string, error) {
 		for _, f := range t.BinInfo().Functions {
 			if regex.MatchString(f.Name) {
 				if followCalls > 0 {
-					newfuncs, err := traverse(t, &f, 1, followCalls)
+					newfuncs, err := d.traverse(t, &f, 1, followCalls, filter)
 					if err != nil {
 						return nil, fmt.Errorf("traverse failed with error %w", err)
 					}
@@ -1387,12 +1394,13 @@ func (d *Debugger) Functions(filter string, followCalls int) ([]string, error) {
 	return funcs, nil
 }
 
-func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int) ([]string, error) {
+func (d *Debugger) traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int, rootstr string) ([]string, error) {
 	type TraceFunc struct {
 		Func    *proc.Function
 		Depth   int
 		visited bool
 	}
+
 	type TraceFuncptr *TraceFunc
 
 	TraceMap := make(map[string]TraceFuncptr)
@@ -1408,7 +1416,7 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
 		parent := queue[0]
 		queue = queue[1:]
 		if parent == nil {
-			panic("attempting to open file Delve cannot parse")
+			panic("queue has a nil node, cannot traverse!")
 		}
 		if parent.Depth > followCalls {
 			continue
@@ -1426,10 +1434,89 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
 		}
 		f := parent.Func
 		text, err := proc.Disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), f.Entry, f.End)
+
 		if err != nil {
 			return nil, fmt.Errorf("disassemble failed with error %w", err)
 		}
 		for _, instr := range text {
+			// Dynamic functions need to be handled specially as their destination location
+			// is not known statically, hence its required to put a breakpoint in order to
+			// acquire the address of the function at runtime and we do this via a
+			// call back mechanism
+			if instr.IsCall() && instr.DestLoc == nil {
+				dynbp, err := t.SetBreakpoint(0, instr.Loc.PC, proc.NextBreakpoint, nil)
+				if err != nil {
+					return nil, fmt.Errorf("error setting breakpoint inside deferreturn")
+				}
+				dynCallback := func(th proc.Thread, tgt *proc.Target) (bool, error) {
+					// TODO(optimization): Consider using an iterator to avoid materializing
+					// the full stack when we only need frames up to the root function
+					rawlocs, err := proc.ThreadStacktrace(tgt, tgt.CurrentThread(), followCalls+2)
+					if err != nil {
+						return false, fmt.Errorf("thread stack trace returned error")
+					}
+					// Since the dynamic function is known only at runtime, the depth is likewise
+					// calculated by referring to the stack and the mechanism is similar to that
+					// used in pkg/terminal/command.go:printTraceOutput
+					rootindex := -1
+					for i := len(rawlocs) - 1; i >= 0; i-- {
+						if rawlocs[i].Call.Fn.Name == rootstr {
+							if rootindex == -1 {
+								rootindex = i
+								break
+							}
+						}
+					}
+					sdepth := rootindex + 1
+
+					if sdepth+1 > followCalls {
+						return false, nil
+					}
+					regs, err := th.Registers()
+					if err != nil {
+						return false, fmt.Errorf("registers inside callback returned err")
+
+					}
+					// Disassemble the instruction at the current PC to get the call destination
+					pc := instr.Loc.PC
+					maxInstLen := uint64(tgt.BinInfo().Arch.MaxInstructionLength())
+					disasm, err := proc.Disassemble(t.Memory(), regs, t.Breakpoints(), tgt.BinInfo(), pc, pc+maxInstLen)
+					if err != nil {
+						return false, fmt.Errorf("failed to disassemble instruction: %w", err)
+					}
+
+					// Extract address from the decoded instruction's destination location
+					var addr uint64
+					if len(disasm) > 0 && disasm[0].DestLoc != nil {
+						addr = disasm[0].DestLoc.PC
+					} else {
+						return false, fmt.Errorf("failed to extract call destination from instruction at PC %#x", pc)
+					}
+					fn := tgt.BinInfo().PCToFunc(addr)
+					if fn == nil {
+						return false, fmt.Errorf("PCToFunc returned nil")
+					}
+					err = createFunctionTracepoints(d, fn.Name, rootstr, followCalls)
+					if err != nil {
+						return false, fmt.Errorf("error creating tracepoint in function %s", fn.Name)
+					}
+					dynchildren, err := d.traverse(t, fn, sdepth+1, followCalls, rootstr)
+					if err != nil {
+						return false, fmt.Errorf("error calling traverse on dynamic children")
+					}
+					for _, child := range dynchildren {
+						err := createFunctionTracepoints(d, child, rootstr, followCalls)
+						if err != nil {
+							return false, fmt.Errorf("error creating tracepoint in function %s", child)
+						}
+					}
+					return false, nil
+				}
+				for _, dynBrklet := range dynbp.Breaklets {
+					dynBrklet.SetCallback(dynCallback)
+				}
+			}
+
 			if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil {
 				cf := instr.DestLoc.Fn
 				if (strings.HasPrefix(cf.Name, "runtime.") || strings.HasPrefix(cf.Name, "runtime/internal")) && cf.Name != "runtime.deferreturn" && cf.Name != "runtime.gorecover" && cf.Name != "runtime.gopanic" {
@@ -1448,6 +1535,70 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
 	return funcs, nil
 }
 
+// createFunctionTracepoints is a way to create a trace point late in the cycle but just in time that
+// it can be included in the trace output as in the case of dynamic functions, we get to know the
+// functions that are being called from deferreturn quite late in execution. This might create multiple
+// tracepoints
+func createFunctionTracepoints(d *Debugger, fname string, rootstr string, followCalls int) error {
+
+	// Helper function to create breakpoints for tracepoints
+	// Ignore BreakpointExistsError since duplicate tracepoints
+	// are expected during dynamic function discovery
+	createBrkForTracepoint := func(lbp *proc.LogicalBreakpoint) error {
+		d.breakpointIDCounter++
+		lbp.LogicalID = d.breakpointIDCounter
+		lbp.HitCount = make(map[int64]uint64)
+		d.target.LogicalBreakpoints[lbp.LogicalID] = lbp
+		err := d.target.SetBreakpointEnabled(lbp, true)
+		if err != nil {
+			delete(d.target.LogicalBreakpoints, lbp.LogicalID)
+			// Silently ignore BreakpointExistsError - this is expected when
+			// creating tracepoints for functions that may already have them
+			if _, exists := err.(proc.BreakpointExistsError); exists {
+				return nil
+			}
+			return err
+		}
+		return nil
+	}
+
+	// Create tracepoint for function entry
+	lbp := &proc.LogicalBreakpoint{
+		Set: proc.SetBreakpoint{
+			FunctionName: fname,
+		},
+		Tracepoint:       true,
+		RootFuncName:     rootstr,
+		Stacktrace:       20,
+		TraceFollowCalls: followCalls,
+	}
+
+	err := createBrkForTracepoint(lbp)
+	if err != nil {
+		return fmt.Errorf("error creating breakpoint at function %s: %w", fname, err)
+	}
+
+	// Create tracepoints for function return locations
+	raddrs, _ := d.functionReturnLocationsInternal(fname)
+	for i := range raddrs {
+		retLbp := &proc.LogicalBreakpoint{
+			Set: proc.SetBreakpoint{
+				PidAddrs: []proc.PidAddr{{Pid: d.target.Selected.Pid(), Addr: raddrs[i]}},
+			},
+			TraceReturn:      true,
+			RootFuncName:     rootstr,
+			Stacktrace:       20,
+			TraceFollowCalls: followCalls,
+		}
+
+		err := createBrkForTracepoint(retLbp)
+		if err != nil {
+			return fmt.Errorf("error creating breakpoint at function return %s: %w", fname, err)
+		}
+	}
+	return nil
+}
+
 // Types returns all type information in the binary.
 func (d *Debugger) Types(filter string) ([]string, error) {
 	d.targetMutex.Lock()



Test patch:
diff --git a/_fixtures/testtracefns.go b/_fixtures/testtracefns.go
index 84c6f3b7..a40c396b 100644
--- a/_fixtures/testtracefns.go
+++ b/_fixtures/testtracefns.go
@@ -69,6 +69,75 @@ func F4() {
 	panic("blah")
 }
 
+var intc, intd int
+
+func swap() {
+	defer func() {
+		intc += 100
+	}()
+	temp := intc
+	intc = intd
+	intd = temp
+}
+
+func unnamedDefer() {
+	intc = -100
+	intd = 100
+	swap()
+	fmt.Println(intc, intd)
+}
+func formula(op string) func(int, int) int {
+	var calc func(int, int) int
+	if op == "add" {
+		calc = func(m int, n int) int {
+			res := m + n
+			return res
+		}
+	} else if op == "mul" {
+		calc = func(m int, n int) int {
+			res := m * n
+			return res
+		}
+	}
+	return calc
+}
+
+func op() int {
+	calc := formula("add")
+	res := calc(10, 20)
+	return res
+}
+
+func assign(bar func()) {
+	bar()
+}
+func testfunc() {
+	intc = 10
+	intd = 20
+}
+
+func dyn() {
+	intc = 0
+	intd = 0
+	assign(testfunc)
+}
+
+func outer() {
+	intc = 40
+        defer swap()
+}
+func nestDefer() {
+        defer outer()
+}
+
+func namedDeferLoop(n int) {
+        for i := 0; i < n; i++ {
+                defer testfunc()
+        }
+        temp := intc
+        intc = intd
+        intd = temp
+}
 func main() {
 	j := 0
 	j += A(2)
@@ -76,6 +145,12 @@ func main() {
 	j += first(6)
 	j += callme(2)
 	fmt.Println(j)
+	unnamedDefer()
+	nestDefer()
+	namedDeferLoop(2)
 	F0()
+	ans := op()
+	fmt.Println(ans)
+	dyn()
 
 }
diff --git a/_fixtures/testvariables2.go b/_fixtures/testvariables2.go
index d0d2d04a..cd905c0c 100644
--- a/_fixtures/testvariables2.go
+++ b/_fixtures/testvariables2.go
@@ -432,6 +432,7 @@ func main() {
 	var messageVar Message = Message{1, 2, 3, 4, 5}
 
 	var iface7 interface{} = OnlyUsedInInterface{"test"}
+	issue4072 := []byte{116, 121, 112, 101, 32, 84, 32, 115, 116, 114, 117, 99, 116, 32, 123, 12, 12, 9, 255, 102, 108, 100, 99, 111, 109, 255}
 
 	var amb1 = 1
 	runtime.Breakpoint()
@@ -443,5 +444,5 @@ func main() {
 	longslice := make([]int, 100, 100)
 
 	runtime.Breakpoint()
-	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4, mlarge, messageVar, iface7)
+	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4, mlarge, messageVar, iface7, issue4072)
 }
diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go
index 55d22b8b..8dea12ac 100644
--- a/cmd/dlv/dlv_test.go
+++ b/cmd/dlv/dlv_test.go
@@ -1467,9 +1467,6 @@ func TestCapsLock(t *testing.T) {
 	if goos == "linux" && goarch == "ppc64le" {
 		args = append([]string{"-buildtags", "exp.linuxppc64le"}, args...)
 	}
-	if goos == "windows" && goarch == "arm64" {
-		args = append([]string{"-buildtags", "exp.winarm64"}, args...)
-	}
 
 	cmd := exec.Command("capslock", args...)
 	cmd.Dir = protest.ProjectRoot()
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 5811a4fc..8532c6a1 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -977,6 +977,33 @@ func TestBreakpointOnFunctionEntry(t *testing.T) {
 	testseq2(t, "testprog", "main.main", []seqTest{{contContinue, 17}})
 }
 
+func TestFirstStmtOptimizedBinaries(t *testing.T) {
+	protest.AllowRecording(t)
+	withTestProcessArgs("multinamedreturns", t, ".", []string{}, protest.EnableOptimization, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
+		_ = setFunctionBreakpoint(p, t, "main.ManyArgsWithNamedReturns")
+
+		assertNoError(grp.Continue(), t, "Continue()")
+
+		pc := currentPC(p, t)
+
+		fn := p.BinInfo().PCToFunc(pc)
+		if fn == nil {
+			t.Fatal("Could not find function for current PC")
+		}
+		if fn.Name != "main.ManyArgsWithNamedReturns" {
+			t.Fatalf("Expected to be in main.ManyArgsWithNamedReturns, got %s", fn.Name)
+		}
+
+		_, f, l, _ := currentLocation(p, t)
+		if !strings.Contains(f, "multinamedreturns.go") || l != 7 {
+			t.Fatalf("Expected file name to be multinamedreturns.go and line number to be 7, got %s:%d", f, l)
+		}
+		if pc == fn.End-1 {
+			t.Fatalf("PC is at function end (0x%x), expected to be at function entry", pc)
+		}
+	})
+}
+
 func TestProcessReceivesSIGCHLD(t *testing.T) {
 	protest.AllowRecording(t)
 	withTestProcess("sigchldprog", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go
index 0def945d..18b50f78 100644
--- a/pkg/proc/test/support.go
+++ b/pkg/proc/test/support.go
@@ -450,9 +450,6 @@ func GetDlvBinary(t *testing.T) string {
 	t.Helper()
 
 	var tags []string
-	if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" {
-		tags = []string{"-tags=exp.winarm64"}
-	}
 	if runtime.GOOS == "linux" && runtime.GOARCH == "ppc64le" {
 		tags = []string{"-tags=exp.linuxppc64le"}
 	}
diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go
index 56b64694..9cbde28b 100644
--- a/pkg/terminal/command_test.go
+++ b/pkg/terminal/command_test.go
@@ -11,6 +11,7 @@ import (
 	"reflect"
 	"regexp"
 	"runtime"
+	"slices"
 	"strconv"
 	"strings"
 	"testing"
@@ -245,6 +246,54 @@ func TestIssue411(t *testing.T) {
 	})
 }
 
+func TestCustomCommandNestedBreakpoint(t *testing.T) {
+	withTestTerminal("nestedbp", t, func(term *FakeTerminal) {
+		term.MustExec("source " + findStarFile("test_custom_cmd_nested"))
+
+		term.MustExec("break nestedbp.go:13") // BP 1
+		term.MustExec("break nestedbp.go:18") // BP 2
+		term.MustExec("break nestedbp.go:19") // BP 3
+
+		// Add custom commands to BP1:
+		// 1. cmd before continue (should execute)
+		// 2. continue cmd (should execute and hit BP2)
+		// 3. cmd after continue (should NOT execute)
+		term.MustExec("on 1 test_bp1_before_continue")
+		term.MustExec("on 1 test_bp1_continue_cmd")
+		term.MustExec("on 1 test_bp1_after_continue")
+
+		// Add custom commands to BP2 which should execute due to BP 1 custom command continue:
+		term.MustExec(fmt.Sprintf("on %d test_bp2_cmd", 2))
+		term.MustExec("on 2 test_bp1_continue_cmd")
+		term.MustExec("on 2 test_bp1_after_continue")
+
+		// Add command on BP3 which should execute due to BP 2 custom command continue:
+		term.MustExec("on 3 test_bp3_cmd")
+
+		out := term.MustExec("continue")
+
+		if !strings.Contains(out, "BP1_BEFORE_CONTINUE") {
+			t.Errorf("expected BP1_BEFORE_CONTINUE to be printed, got: %q", out)
+		}
+
+		if !strings.Contains(out, "BP1_CONTINUE_CMD") {
+			t.Errorf("expected BP1_CONTINUE_CMD to be printed, got: %q", out)
+		}
+
+		// Verify BP1 command after continue did NOT execute
+		if strings.Contains(out, "BP1_AFTER_CONTINUE") {
+			t.Errorf("BP1_AFTER_CONTINUE was printed, which means the command ran after continue, got: %q", out)
+		}
+
+		if !strings.Contains(out, "BP2_CMD_EXECUTED") {
+			t.Errorf("BP2's custom commands not executed")
+		}
+		if !strings.Contains(out, "BP3_CMD_EXECUTED") {
+			t.Errorf("BP3's custom commands not executed")
+		}
+	})
+}
+
 func TestTrace(t *testing.T) {
 	test.AllowRecording(t)
 	withTestTerminal("issue573", t, func(term *FakeTerminal) {
@@ -1615,3 +1664,136 @@ func TestTraceRegexpReturn(t *testing.T) {
 		}
 	})
 }
+
+func TestStarlarkOnPrefix(t *testing.T) {
+	withTestTerminal("testvariables2", t, func(term *FakeTerminal) {
+		term.MustExec("source " + findStarFile("test_allow_on"))
+
+		var customCmd *command
+		for i := range term.cmds.cmds {
+			cmd := &term.cmds.cmds[i]
+			if cmd.match("test_on_allowed") {
+				customCmd = cmd
+				break
+			}
+		}
+
+		if customCmd == nil {
+			t.Fatal("test_on_allowed command not found")
+		}
+
+		// All custom starlark commands should have onPrefix set
+		if customCmd.allowedPrefixes&onPrefix == 0 {
+			t.Errorf("custom starlark command should have onPrefix set, got allowedPrefixes=%v", customCmd.allowedPrefixes)
+		}
+
+		term.MustExec("break main.main")
+
+		bps1, _ := term.client.ListBreakpoints(false)
+		var targetBpID int
+		for _, bp := range bps1 {
+			if bp.FunctionName == "main.main" {
+				targetBpID = bp.ID
+				break
+			}
+		}
+		if targetBpID == 0 {
+			t.Fatal("Could not find breakpoint on main.main")
+		}
+
+		term.MustExec(fmt.Sprintf("on %d test_on_allowed", targetBpID))
+
+		bpListOutput := term.MustExec("breakpoints")
+		if !strings.Contains(bpListOutput, "test_on_allowed") {
+			t.Errorf("expected 'test_on_allowed' to appear in breakpoints output, got: %q", bpListOutput)
+		}
+
+		breakpoints, err := term.client.ListBreakpoints(false)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(breakpoints) == 0 {
+			t.Fatal("no breakpoints found")
+		}
+
+		var bp *api.Breakpoint
+		for _, b := range breakpoints {
+			if b.ID == targetBpID {
+				bp = b
+				break
+			}
+		}
+		if bp == nil {
+			t.Fatalf("Could not find breakpoint with ID %d", targetBpID)
+		}
+
+		if len(bp.CustomCommands) == 0 {
+			t.Errorf("expected custom command to be registered on breakpoint, but CustomCommands is empty")
+		} else {
+			found := slices.Contains(bp.CustomCommands, "test_on_allowed")
+			if !found {
+				t.Errorf("expected 'test_on_allowed' to be in CustomCommands, got %v", bp.CustomCommands)
+			}
+		}
+
+		term.MustExec(fmt.Sprintf("on %d test_on_allowed hello world", targetBpID))
+
+		bpListOutput2 := term.MustExec("breakpoints")
+		if !strings.Contains(bpListOutput2, "test_on_allowed hello world") {
+			t.Errorf("expected 'test_on_allowed hello world' to appear in breakpoints output, got: %q", bpListOutput2)
+		}
+
+		out := term.MustExec("continue")
+		if !strings.Contains(out, "test_on_allowed called with:") {
+			t.Errorf("expected custom command to be executed when breakpoint is hit, but output was: %q", out)
+		}
+		if !strings.Contains(out, "test_on_allowed called with: hello world") {
+			t.Errorf("expected custom command with args to be executed, but output was: %q", out)
+		}
+	})
+}
+
+func TestCustomCommandStopsOnContinue(t *testing.T) {
+	// This test verifies that when a custom command triggers a runCmd (like continue),
+	// the execution of remaining custom commands is stopped because the process state
+	// has been invalidated.
+	withTestTerminal("break", t, func(term *FakeTerminal) {
+		term.MustExec("source " + findStarFile("test_custom_cmd_continue"))
+
+		term.MustExec("break break.go:7")
+
+		bps, _ := term.client.ListBreakpoints(false)
+		var targetBpID int
+		for _, bp := range bps {
+			if strings.Contains(bp.File, "break.go") && bp.Line == 7 {
+				targetBpID = bp.ID
+				break
+			}
+		}
+		if targetBpID == 0 {
+			t.Fatal("Could not find breakpoint on break.go:7")
+		}
+
+		// Add three custom commands to the breakpoint:
+		// 1. cmd before continue (should execute)
+		// 2. continue (should execute and invalidate state)
+		// 3. cmd after continue (should NOT execute because state is invalid)
+		term.MustExec(fmt.Sprintf("on %d test_cmd_before_continue", targetBpID))
+		term.MustExec(fmt.Sprintf("on %d test_continue_cmd", targetBpID))
+		term.MustExec(fmt.Sprintf("on %d test_cmd_after_continue", targetBpID))
+
+		out := term.MustExec("continue")
+
+		if !strings.Contains(out, "BEFORE_CONTINUE") {
+			t.Errorf("expected BEFORE_CONTINUE to be printed, got: %q", out)
+		}
+
+		if !strings.Contains(out, "CONTINUE_CMD") {
+			t.Errorf("expected CONTINUE_CMD to be printed, got: %q", out)
+		}
+
+		if strings.Contains(out, "AFTER_CONTINUE") {
+			t.Errorf("AFTER_CONTINUE was printed, which means the command ran after continue, got: %q", out)
+		}
+	})
+}
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index d9a457d0..a853cab5 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -213,6 +213,7 @@ func (c *Client) InitializeRequest() {
 		SupportsVariableType:         true,
 		SupportsVariablePaging:       true,
 		SupportsRunInTerminalRequest: true,
+		SupportsMemoryReferences:     true,
 		Locale:                       "en-us",
 	}
 	c.send(request)
@@ -550,8 +551,14 @@ func (c *Client) SetDataBreakpointsRequest() {
 }
 
 // ReadMemoryRequest sends a 'readMemory' request.
-func (c *Client) ReadMemoryRequest() {
-	c.send(&dap.ReadMemoryRequest{Request: *c.newRequest("readMemory")})
+func (c *Client) ReadMemoryRequest(ref string, offset, count int) {
+	c.send(&dap.ReadMemoryRequest{
+		Request: *c.newRequest("readMemory"),
+		Arguments: dap.ReadMemoryArguments{
+			MemoryReference: ref,
+			Offset:          offset,
+			Count:           count,
+		}})
 }
 
 // DisassembleRequest sends a 'disassemble' request.
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 240a407d..9aaa25c1 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -4,6 +4,9 @@ import (
 	"bufio"
 	"bytes"
 	"cmp"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/hex"
 	"flag"
 	"fmt"
 	"io"
@@ -6548,9 +6551,6 @@ func TestOptionalNotYetImplementedResponses(t *testing.T) {
 		client.LoadedSourcesRequest()
 		expectNotYetImplemented("loadedSources")
 
-		client.ReadMemoryRequest()
-		expectNotYetImplemented("readMemory")
-
 		client.CancelRequest()
 		expectNotYetImplemented("cancel")
 
@@ -8054,6 +8054,130 @@ func TestRedirects(t *testing.T) {
 	})
 }
 
+func TestReadMemory_StringPagination(t *testing.T) {
+	if runtime.GOOS == "freebsd" {
+		t.Skip("test skipped on freebsd")
+	}
+
+	runTest(t, "readmem_json", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Breakpoints are set within the program
+			fixture.Source, []int{},
+			[]onBreakpoint{
+				{
+					execute:    func() {},
+					disconnect: false,
+				},
+				{
+					execute: func() {
+						client.StackTraceRequest(1, 0, 20)
+						_ = client.ExpectStackTraceResponse(t)
+
+						client.ScopesRequest(1000)
+						_ = client.ExpectScopesResponse(t)
+
+						client.VariablesRequest(localsScope)
+						locals := client.ExpectVariablesResponse(t)
+						if locals == nil {
+							t.Fatal("wanted local variables, got 0")
+						}
+
+						mustGetByName := func(vars []dap.Variable, names ...string) (map[string]dap.Variable, error) {
+							res := make(map[string]dap.Variable)
+
+							for _, n := range names {
+								idx := slices.IndexFunc(vars, func(v dap.Variable) bool {
+									return v.Name == n
+								})
+
+								if idx == -1 {
+									return nil, fmt.Errorf("%s not found", n)
+								}
+
+								res[n] = vars[idx]
+							}
+
+							return res, nil
+						}
+
+						varIdx, err := mustGetByName(locals.Body.Variables,
+							"jsonString",
+							"jsonHash",
+							"jsonAddr",
+							"bytesString",
+							"nonprint",
+						)
+						if err != nil {
+							t.Fatal(err)
+						}
+
+						longString := varIdx["jsonString"]
+
+						if strings.Trim(varIdx["jsonAddr"].Value, `"`) != longString.MemoryReference {
+							t.Fatal("bad memory address")
+						}
+
+						got := readVarByChunk(t, client, longString, 64)
+						hashed := sha256.Sum256(got.Bytes())
+						hashString := hex.EncodeToString(hashed[:])
+
+						if strings.Trim(varIdx["jsonHash"].Value, `"`) != hashString {
+							t.Fatal("we got wrong values")
+						}
+
+						bytes := readVarByChunk(t, client, varIdx["bytesString"], 1)
+
+						if bytes.String() != "this\nis\nit" {
+							t.Fail()
+						}
+
+						nonp := readVarByChunk(t, client, varIdx["nonprint"], 1)
+						want := []byte{242, 243, 244, 245}
+
+						for i, b := range nonp.Bytes() {
+							if want[i] != b {
+								t.Fail()
+							}
+						}
+					},
+					disconnect: true,
+				}})
+	})
+}
+
+func readVarByChunk(t *testing.T, client *daptest.Client, v dap.Variable, chunk int) bytes.Buffer {
+	t.Helper()
+
+	var got bytes.Buffer
+
+	for off := 0; ; off += chunk {
+		count := chunk
+		client.ReadMemoryRequest(v.MemoryReference, off, count)
+		rm := client.ExpectReadMemoryResponse(t)
+
+		if rm.Body.Data == "" {
+			break
+		}
+
+		data, err := base64.StdEncoding.DecodeString(rm.Body.Data)
+		if err != nil {
+			t.Fatalf("base64 decode failed: %v", err)
+		}
+
+		got.Write(data)
+
+		if len(data) < count {
+			break
+		}
+	}
+
+	return got
+}
+
 type discard struct {
 }
 
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index ec5fdcd8..29efc6e5 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -806,28 +806,65 @@ func matchFunctions(t *testing.T, funcs []string, expected []string, depth int)
 func TestTraceFollowCallsCommand(t *testing.T) {
 	protest.AllowRecording(t)
 	withTestClient2("testtracefns", t, func(c service.Client) {
-		depth := 3
-		functions, err := c.ListFunctions("main.A", depth)
-		assertNoError(err, t, "ListFunctions()")
-		expected := []string{"main.A", "main.B", "main.C", "main.D"}
-		matchFunctions(t, functions, expected, depth)
-
-		functions, err = c.ListFunctions("main.first", depth)
-		assertNoError(err, t, "ListFunctions()")
-		expected = []string{"main.first", "main.second"}
-		matchFunctions(t, functions, expected, depth)
-
-		depth = 4
-		functions, err = c.ListFunctions("main.callme", depth)
-		assertNoError(err, t, "ListFunctions()")
-		expected = []string{"main.callme", "main.callme2", "main.callmed", "main.callmee"}
-		matchFunctions(t, functions, expected, depth)
+		testCases := []struct {
+			funcName string
+			depth    int
+			expected []string
+		}{
+			{
+				funcName: "main.A",
+				depth:    3,
+				expected: []string{"main.A", "main.B", "main.C", "main.D"},
+			},
+			{
+				funcName: "main.first",
+				depth:    3,
+				expected: []string{"main.first", "main.second"},
+			},
+			{
+				funcName: "main.callme",
+				depth:    4,
+				expected: []string{"main.callme", "main.callme2", "main.callmed", "main.callmee"},
+			},
+			{
+				funcName: "main.F0",
+				depth:    6,
+				expected: []string{"main.F0", "main.F0.func1", "main.F1", "main.F2", "main.F3", "main.F4", "runtime.deferreturn", "runtime.gopanic", "runtime.gorecover"},
+			},
+			{
+				funcName: "main.swap",
+				depth:    3,
+				expected: []string{"main.swap", "main.swap.func1", "runtime.deferreturn"},
+			},
+			{
+				funcName: "main.nestDefer",
+				depth:    7,
+				expected: []string{"main.nestDefer", "runtime.deferreturn", "main.outer", "main.swap", "main.swap.func1"},
+			},
+			{
+				funcName: "main.namedDeferLoop",
+				depth:    3,
+				expected: []string{"main.namedDeferLoop", "runtime.deferreturn", "main.testfunc"},
+			},
+			{
+				funcName: "main.op",
+				depth:    3,
+				expected: []string{"main.formula", "main.op", "main.formula.func1"},
+			},
+			{
+				funcName: "main.dyn",
+				depth:    3,
+				expected: []string{"main.assign", "main.dyn", "main.testfunc"},
+			},
+		}
 
-		depth = 6
-		functions, err = c.ListFunctions("main.F0", depth)
-		assertNoError(err, t, "ListFunctions()")
-		expected = []string{"main.F0", "main.F0.func1", "main.F1", "main.F2", "main.F3", "main.F4", "runtime.deferreturn", "runtime.gopanic", "runtime.gorecover"}
-		matchFunctions(t, functions, expected, depth)
+		for _, tc := range testCases {
+			t.Run(tc.funcName, func(t *testing.T) {
+				functions, err := c.ListFunctions(tc.funcName, tc.depth)
+				assertNoError(err, t, "ListFunctions()")
+				matchFunctions(t, functions, tc.expected, tc.depth)
+			})
+		}
 	})
 }
 
@@ -3213,10 +3250,6 @@ func TestGuessSubstitutePath(t *testing.T) {
 			t.Setenv("GOFLAGS", "-tags=exp.linuxriscv64")
 		case "loong64":
 			t.Setenv("GOFLAGS", "-tags=exp.linuxloong64")
-		case "arm64":
-			if runtime.GOOS == "windows" {
-				t.Setenv("GOFLAGS", "-tags=exp.winarm64")
-			}
 		}
 
 		gsp, err := client.GuessSubstitutePath()
@@ -3395,3 +3428,20 @@ func TestCancelDownload(t *testing.T) {
 		}
 	})
 }
+
+func TestEvalNonunicodeString(t *testing.T) {
+	// Non-unicode strings can not be sent through json encoding without being
+	// altered, check that our workaround works.
+	// See issue #4072.
+	withTestClient2("testvariables2", t, func(c service.Client) {
+		state := <-c.Continue()
+		assertNoError(state.Err, t, "Continue")
+		v, err := c.EvalVariable(api.EvalScope{GoroutineID: -1}, "string(issue4072)", normalLoadConfig)
+		assertNoError(err, t, "EvalVariable")
+		t.Logf("%s", v.MultilineString("", ""))
+		tgt := string([]byte{116, 121, 112, 101, 32, 84, 32, 115, 116, 114, 117, 99, 116, 32, 123, 12, 12, 9, 255, 102, 108, 100, 99, 111, 109, 255})
+		if v.Value != tgt {
+			t.Errorf("wrong value for string issue4072 expected %q, got %q", tgt, v.Value)
+		}
+	})
+}