Repository: go-delve/delve
Pull request #2466 (2021-05-05 19:56:49)

service/dap: support pause request
Updates #1515
Updates https://github.com/golang/vscode-go/issues/1474

polinasok: I am having all sorts of issues with the test. Everything works locally, but as always TeamCity brings some surprises. The test uses [loopprog.go](https://github.com/go-delve/delve/blob/master/_fixtures/loopprog.go), which doesn't do anything fancy. Yet have seen the following failures. Could they be related to halting? E.g. I am not checking state and issue a halt command request even if things are not running. Should I be?  
```
### continue to breakpoint at line 16, then request next
2021-05-05T23:26:40Z debug layer=debugger switching to goroutine 1
2021-05-05T23:26:40Z debug layer=debugger nexting
2021-05-05T23:26:40Z debug layer=dap "next" command stopped - reason "next finished", location /delve/_fixtures/loopprog.go:17
### issue a halt request - which I expected to be a no-op because nothing was running
2021-05-05T23:26:40Z debug layer=dap [<- from client]{"seq":8,"type":"request","command":"pause","arguments":{"threadId":1}}
2021-05-05T23:26:40Z debug layer=debugger halting
2021-05-05T23:26:40Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":8,"success":true,"command":"pause"}
2021-05-05T23:26:40Z debug layer=dap [<- from client]{"seq":9,"type":"request","command":"stackTrace","arguments":{"threadId":1,"levels":20,"format":{}}}
2021-05-05T23:26:40Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":9,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1003,"name":"main.main","source":{"name":"loopprog.go","path":"/delve/_fixtures/loopprog.go"},"line":17,"column":0},{"id":1004,"name":"runtime.main","source":{"name":"proc.go","path":"/usr/local/go/go1.14.15/src/runtime/proc.go"},"line":203,"column":0},{"id":1005,"name":"runtime.goexit","source":{"name":"asm_amd64.s","path":"/usr/local/go/go1.14.15/src/runtime/asm_amd64.s"},"line":1373,"column":0}],"totalFrames":3}}
### then a continue request, which failed miserably - is the previous halt somehow to blame?
2021-05-05T23:26:40Z debug layer=dap [<- from client]{"seq":10,"type":"request","command":"continue","arguments":{"threadId":0}}
2021-05-05T23:26:40Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":10,"success":true,"command":"continue","body":{"allThreadsContinued":true}}
2021-05-05T23:26:40Z debug layer=debugger continuing
SIGILL: illegal instruction
PC=0x4ae0dc m=0 sigcode=2
```

```
# breakpoint, next, halt (at a next stop), continue, halt, continue, halt, continue
# eventually another continue hit a fatal error
2021-05-05T23:46:52Z debug layer=debugger continuing
fatal error: unexpected signal during runtime execution
2021-05-05T23:46:52Z debug layer=dap [<- from client]{"seq":15,"type":"request","command":"pause","arguments":{"threadId":-2}}
2021-05-05T23:46:52Z debug layer=debugger halting
2021-05-05T23:46:52Z debug layer=dap "continue" command stopped - reason "manual", location /usr/local/go/go1.15.11/src/runtime/panic.go:1162
2021-05-05T23:46:52Z debug layer=dap [-> to client]{"seq":0,"type":"event","event":"stopped","body":{"reason":"fatal error","allThreadsStopped":true}}
```
polinasok: cc @hyangah @suzmue 
aarzilli: The test has no reason to be more complicated than starting loopprog, waiting for a random timeout and sending a single pause request.
polinasok: > The test has no reason to be more complicated than starting loopprog, waiting for a random timeout and sending a single pause request.

I figured that this would be the price of a non-flaky test. However, I wanted to keep it as-is for now so we can have a discussion as to why there have been all the issues that I have observed. I experimented with quite a bit - adding and removing random time-outs and such and it did not make things better. I do think that the issue is the repeated continue/pause sequence and also calling pause when already at pause. I had the loop because I wanted to both get the case when the main goroutine pauses and there is no selected goroutine more or less reliably. I can take it out without reducing test coverage. But I would not want to take out the case of pausing while already in paused stated. Shouldn't that be working?
aarzilli: If you want to test something about the proc package you should write a test for the proc package, there's no reason to make things more complicated by going through a rpc layer.
polinasok: > If you want to test something about the proc package you should write a test for the proc package, there's no reason to make things more complicated by going through a rpc layer.

My goal was not to test the proc package. I wanted to see how the dap layer handled different outcomes of halt. Does it handle halt errors? Does it handle cases where nothing is running and so nothing is interrupted and no stopped event is sent? What do things look like when there is no selected goroutine when we stop? In doing so I came across proc behavior that surprised me, so I wanted to learn more about it, to understand better the inner-workings of the codebase I am working with.

In particular, should it be valid to call command halt when the debugger state is not running? Or should the client or the dap server detect that nothing is running and not call it? This would not only impact pause, but also disconnect.


aarzilli: > My goal was not to test the proc package. I wanted to see how the dap layer handled different outcomes of halt. Does it handle halt errors? Does it handle cases where nothing is running and so nothing is interrupted and no stopped event is sent? What do things look like when there is no selected goroutine when we stop? In doing so I came across proc behavior that surprised me, so I wanted to learn more about it, to understand better the inner-workings of the codebase and the runtime I am working with.

Got it. But none of this is being tested here, this is a test about what happens if pause and resume are used in rapid succession.

> In particular, should it be valid to call command halt when the debugger state is not running?

It should be but I don't think anyone has looked into it closely and it isn't something that's being tested in the wild either.
polinasok: > > My goal was not to test the proc package. I wanted to see how the dap layer handled different outcomes of halt. Does it handle halt errors? Does it handle cases where nothing is running and so nothing is interrupted and no stopped event is sent? What do things look like when there is no selected goroutine when we stop? In doing so I came across proc behavior that surprised me, so I wanted to learn more about it, to understand better the inner-workings of the codebase and the runtime I am working with.
> 
> Got it. But none of this is being tested here, this is a test about what happens if pause and resume are used in rapid succession.

Some of the test iterations that failed on TeamCity for me were sleeping between the successions, so they were not so rapid, but it did not help. Maybe the value I choose was still too small. I will try something more comparable to what a human would do. I just found the rapidly printing numbers polluting the output log very annoying. Any objections to changing this fixture to printing less often?
 
> > In particular, should it be valid to call command halt when the debugger state is not running?
> 
> It should be but I don't think anyone has looked into it closely and it isn't something that's being tested in the wild either.

It is used in the wild because both legacy vscode-go and dlv dap issue a halt as part of disconnect, without checking if the process is running. You can also run into double continue or double pause scenarios with multiple parallel clients or experimental and not fully fleshed out features like call, where the UI is not always correctly reflecting the running/stopped state, exposing the wrong controls. Obviously, that's not a big concern in reality.
aarzilli: The way manual stops work is pretty simple, we just send a SIGTRAP signal to the process, simulating what a software breakpoint would do. I don't know what the kernel does with it if the thread happens to be already in a trace-stopped state, I've never checked. I've also never checked what happens if the thread happens to have just exited trace-stop but hasn't been scheduled yet. It might be that something strange happens, it might be that there is a bug in our code.

The behavior is also very likely to be different between different OSes.

I'd be interested in looking into this more but I don't think this should be part of the test for this PR, because it's a problem in a different component, because it's unlikely to be fixed quickly given that it seems pretty low impact and because it's going to be easier to diagnose with fewer layers in between.

> It is used in the wild because both legacy vscode-go and dlv dap issue a halt as part of disconnect, without checking if the process is running

If that's a concern I guess it's easier to fix now that you can call IsRunning, but if this was a common problem I think we would have heard something from users.
polinasok: > If that's a concern I guess it's easier to fix now that you can call IsRunning, but if this was a common problem I think we would have heard something from users.

Likely so. Although if things are disconnecting and fail in the process, one might not even notice because either way all is gone in a fraction of a second. But yes, it's likely not a big deal because clearly in the common case where it matters, it hasn't bothered users yet.
aarzilli: I don't think this and the detach thing are related. I think the way we detach while killing the process has never been that great and sometimes it returns an error for stupid reasons.
polinasok: > I don't think this and the detach thing are related. I think the way we detach while killing the process has never been that great and sometimes it returns an error for stupid reasons.

Fair enough. I will rework the test, but I think you can look at the implementation already. Looking forward to your review and thank you for the discussion.
aarzilli: > Pause test is now passing.
> TestAttachStopOnEntry is failing. It has not been impacted by the pause change, but I have experienced it flake every once in a while in an unexpected way. I can adjust the test in a separate PR since it's not related.

I think this is the problem with how we do the `Detach(kill=true)`.
polinasok: @derekparker Is there anything else that needs to be addressed, so this can be merged? Thank you.
derekparker: @polinasok this LGTM, please just rebase and then I will merge today.

Patch:
diff --git a/pkg/dwarf/op/op.go b/pkg/dwarf/op/op.go
index 7410f7c1..1d19bc10 100644
--- a/pkg/dwarf/op/op.go
+++ b/pkg/dwarf/op/op.go
@@ -30,12 +30,20 @@ type context struct {
 
 // Piece is a piece of memory stored either at an address or in a register.
 type Piece struct {
-	Size       int
-	Addr       int64
-	RegNum     uint64
-	IsRegister bool
+	Size int
+	Kind PieceKind
+	Val  uint64
 }
 
+// PieceKind describes the kind of a piece.
+type PieceKind uint8
+
+const (
+	AddrPiece PieceKind = iota // The piece is stored in memory, Val is the address
+	RegPiece                   // The piece is stored in a register, Val is the register number
+	ImmPiece                   // The piece is an immediate value, Val is the value
+)
+
 // ExecuteStackProgram executes a DWARF location expression and returns
 // either an address (int64), or a slice of Pieces for location expressions
 // that don't evaluate to an address (such as register and composite expressions).
@@ -56,7 +64,7 @@ func ExecuteStackProgram(regs DwarfRegisters, instructions []byte, ptrSize int)
 		if ctxt.reg && opcode != DW_OP_piece {
 			// last opcode was DW_OP_regN and next one isn't DW_OP_piece so convert
 			// the register piece into a stack value.
-			ctxt.stack = append(ctxt.stack, int64(regs.Uint64Val(ctxt.pieces[len(ctxt.pieces)-1].RegNum)))
+			ctxt.stack = append(ctxt.stack, int64(regs.Uint64Val(ctxt.pieces[len(ctxt.pieces)-1].Val)))
 			ctxt.pieces = ctxt.pieces[:len(ctxt.pieces)-1]
 			ctxt.reg = false
 		}
@@ -72,8 +80,8 @@ func ExecuteStackProgram(regs DwarfRegisters, instructions []byte, ptrSize int)
 	}
 
 	if ctxt.pieces != nil {
-		if len(ctxt.pieces) == 1 && ctxt.pieces[0].IsRegister {
-			return int64(regs.Uint64Val(ctxt.pieces[0].RegNum)), ctxt.pieces, nil
+		if len(ctxt.pieces) == 1 && ctxt.pieces[0].Kind == RegPiece {
+			return int64(regs.Uint64Val(ctxt.pieces[0].Val)), ctxt.pieces, nil
 		}
 		return 0, ctxt.pieces, nil
 	}
@@ -187,9 +195,9 @@ func register(opcode Opcode, ctxt *context) error {
 	ctxt.reg = true
 	if opcode == DW_OP_regx {
 		n, _ := util.DecodeSLEB128(ctxt.buf)
-		ctxt.pieces = append(ctxt.pieces, Piece{IsRegister: true, RegNum: uint64(n)})
+		ctxt.pieces = append(ctxt.pieces, Piece{Kind: RegPiece, Val: uint64(n)})
 	} else {
-		ctxt.pieces = append(ctxt.pieces, Piece{IsRegister: true, RegNum: uint64(opcode - DW_OP_reg0)})
+		ctxt.pieces = append(ctxt.pieces, Piece{Kind: RegPiece, Val: uint64(opcode - DW_OP_reg0)})
 	}
 	return nil
 }
@@ -203,11 +211,14 @@ func piece(opcode Opcode, ctxt *context) error {
 	}
 
 	if len(ctxt.stack) == 0 {
-		return errors.New("empty OP stack")
+		// nothing on the stack means this piece is unavailable (padding,
+		// optimized away...), see DWARFv4 sec. 2.6.1.3 page 30.
+		ctxt.pieces = append(ctxt.pieces, Piece{Size: int(sz), Kind: ImmPiece, Val: 0})
+		return nil
 	}
 
 	addr := ctxt.stack[len(ctxt.stack)-1]
-	ctxt.pieces = append(ctxt.pieces, Piece{Size: int(sz), Addr: addr})
+	ctxt.pieces = append(ctxt.pieces, Piece{Size: int(sz), Kind: AddrPiece, Val: uint64(addr)})
 	ctxt.stack = ctxt.stack[:0]
 	return nil
 }
diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go
index 1313a514..fa35095d 100644
--- a/pkg/proc/eval.go
+++ b/pkg/proc/eval.go
@@ -3,7 +3,6 @@ package proc
 import (
 	"bytes"
 	"debug/dwarf"
-	"encoding/binary"
 	"errors"
 	"fmt"
 	"go/ast"
@@ -905,17 +904,14 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 }
 
 func convertInt(n uint64, signed bool, size int64) uint64 {
-	buf := make([]byte, 64/8)
-	binary.BigEndian.PutUint64(buf, n)
-	m := 64/8 - int(size)
-	s := byte(0)
-	if signed && (buf[m]&0x80 > 0) {
-		s = 0xff
+	bits := uint64(size) * 8
+	mask := uint64((1 << bits) - 1)
+	r := n & mask
+	if signed && (r>>(bits-1)) != 0 {
+		// sign extension
+		r |= ^uint64(0) &^ mask
 	}
-	for i := 0; i < m; i++ {
-		buf[i] = s
-	}
-	return uint64(binary.BigEndian.Uint64(buf))
+	return r
 }
 
 func (scope *EvalScope) evalBuiltinCall(node *ast.CallExpr) (*Variable, error) {
@@ -1636,8 +1632,15 @@ func (scope *EvalScope) evalBinary(node *ast.BinaryExpr) (*Variable, error) {
 
 		r := xv.newVariable("", 0, typ, scope.Mem)
 		r.Value = rc
-		if r.Kind == reflect.String {
+		switch r.Kind {
+		case reflect.String:
 			r.Len = xv.Len + yv.Len
+		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+			n, _ := constant.Int64Val(r.Value)
+			r.Value = constant.MakeInt64(int64(convertInt(uint64(n), true, typ.Size())))
+		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+			n, _ := constant.Uint64Val(r.Value)
+			r.Value = constant.MakeUint64(convertInt(n, false, typ.Size()))
 		}
 		return r, nil
 	}
diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go
index 176ccf9f..37743667 100644
--- a/pkg/proc/gdbserial/gdbserver.go
+++ b/pkg/proc/gdbserial/gdbserver.go
@@ -1556,7 +1556,8 @@ func (t *gdbThread) reloadRegisters() error {
 
 	if t.p.gcmdok {
 		if err := t.p.conn.readRegisters(t.strID, t.regs.buf); err != nil {
-			if isProtocolErrorUnsupported(err) {
+			gdberr, isProt := err.(*GdbProtocolError)
+			if isProtocolErrorUnsupported(err) || (t.p.conn.isDebugserver && isProt && gdberr.code == "E74") {
 				t.p.gcmdok = false
 			} else {
 				return err
diff --git a/pkg/proc/mem.go b/pkg/proc/mem.go
index 70ff9434..8b19fbfb 100644
--- a/pkg/proc/mem.go
+++ b/pkg/proc/mem.go
@@ -1,6 +1,7 @@
 package proc
 
 import (
+	"encoding/binary"
 	"errors"
 	"fmt"
 
@@ -99,22 +100,33 @@ func newCompositeMemory(mem MemoryReadWriter, arch *Arch, regs op.DwarfRegisters
 	cmem := &compositeMemory{realmem: mem, arch: arch, regs: regs, pieces: pieces, data: []byte{}}
 	for i := range pieces {
 		piece := &pieces[i]
-		if piece.IsRegister {
-			reg := regs.Bytes(piece.RegNum)
+		switch piece.Kind {
+		case op.RegPiece:
+			reg := regs.Bytes(piece.Val)
 			if piece.Size == 0 && len(pieces) == 1 {
 				piece.Size = len(reg)
 			}
 			if piece.Size > len(reg) {
 				if regs.FloatLoadError != nil {
-					return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d), also error loading floating point registers: %v", piece.Size, piece.RegNum, len(reg), regs.FloatLoadError)
+					return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d), also error loading floating point registers: %v", piece.Size, piece.Val, len(reg), regs.FloatLoadError)
 				}
-				return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d)", piece.Size, piece.RegNum, len(reg))
+				return nil, fmt.Errorf("could not read %d bytes from register %d (size: %d)", piece.Size, piece.Val, len(reg))
 			}
 			cmem.data = append(cmem.data, reg[:piece.Size]...)
-		} else {
+		case op.AddrPiece:
 			buf := make([]byte, piece.Size)
-			mem.ReadMemory(buf, uint64(piece.Addr))
+			mem.ReadMemory(buf, uint64(piece.Val))
 			cmem.data = append(cmem.data, buf...)
+		case op.ImmPiece:
+			sz := 8
+			if piece.Size > sz {
+				sz = piece.Size
+			}
+			buf := make([]byte, sz)
+			binary.LittleEndian.PutUint64(buf, piece.Val)
+			cmem.data = append(cmem.data, buf[:piece.Size]...)
+		default:
+			panic("unsupported piece kind")
 		}
 	}
 	return cmem, nil
@@ -147,17 +159,22 @@ func (mem *compositeMemory) WriteMemory(addr uint64, data []byte) (int, error) {
 			// changed memory interval overlaps current piece
 			pieceMem := mem.data[curAddr : curAddr+uint64(piece.Size)]
 
-			if piece.IsRegister {
-				err := mem.regs.ChangeFunc(piece.RegNum, op.DwarfRegisterFromBytes(pieceMem))
+			switch piece.Kind {
+			case op.RegPiece:
+				err := mem.regs.ChangeFunc(piece.Val, op.DwarfRegisterFromBytes(pieceMem))
 				if err != nil {
 					return donesz, err
 				}
-			} else {
-				n, err := mem.realmem.WriteMemory(uint64(piece.Addr), pieceMem)
+			case op.AddrPiece:
+				n, err := mem.realmem.WriteMemory(uint64(piece.Val), pieceMem)
 				if err != nil {
 					return donesz + n, err
 				}
-
+			case op.ImmPiece:
+				//TODO(aarzilli): maybe return an error if the user tried to change the value?
+				// nothing to do
+			default:
+				panic("unsupported piece kind")
 			}
 			donesz += piece.Size
 		}
diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go
index 1a5344df..2ae0f330 100644
--- a/service/dap/error_ids.go
+++ b/service/dap/error_ids.go
@@ -21,7 +21,8 @@ const (
 	UnableToListGlobals        = 2007
 	UnableToLookupVariable     = 2008
 	UnableToEvaluateExpression = 2009
-	UnableToGetExceptionInfo   = 2010
+	UnableToHalt               = 2010
+	UnableToGetExceptionInfo   = 2011
 	// Add more codes as we support more requests
 	DebuggeeIsRunning = 4000
 	DisconnectError   = 5000
diff --git a/service/dap/server.go b/service/dap/server.go
index 2cf7f5bf..a7802ea8 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -104,13 +104,10 @@ type Server struct {
 	variableHandles *variablesHandlesMap
 	// args tracks special settings for handling debug session requests.
 	args launchAttachArgs
-<<<<<<< HEAD
 	// exceptionErr tracks the runtime error that last occurred.
 	exceptionErr error
-=======
 	// clientCapabilities tracks special settings for handling debug session requests.
 	clientCapabilities dapClientCapabilites
->>>>>>> 1e9c5c3b07dc5f0f2b3b1fb17bde6444cbf7ca30
 
 	// mu synchronizes access to objects set on start-up (from run goroutine)
 	// and stopped on teardown (from main goroutine)
@@ -380,7 +377,7 @@ func (s *Server) handleRequest(request dap.Message) {
 		return
 	}
 
-	// These requests, can be handeled regardless of whether the targret is running
+	// These requests, can be handled regardless of whether the targret is running
 	switch request := request.(type) {
 	case *dap.DisconnectRequest:
 		// Required
@@ -388,7 +385,6 @@ func (s *Server) handleRequest(request dap.Message) {
 		return
 	case *dap.PauseRequest:
 		// Required
-		// TODO: implement this request in V0
 		s.onPauseRequest(request)
 		return
 	case *dap.TerminateRequest:
@@ -1278,10 +1274,20 @@ func (s *Server) doStepCommand(command string, threadId int, asyncSetupDone chan
 	s.doRunCommand(command, asyncSetupDone)
 }
 
-// onPauseRequest sends a not-yet-implemented error response.
+// onPauseRequest handles 'pause' request.
 // This is a mandatory request to support.
-func (s *Server) onPauseRequest(request *dap.PauseRequest) { // TODO V0
-	s.sendNotYetImplementedErrorResponse(request.Request)
+func (s *Server) onPauseRequest(request *dap.PauseRequest) {
+	_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
+	if err != nil {
+		s.sendErrorResponse(request.Request, UnableToHalt, "Unable to halt execution", err.Error())
+		return
+	}
+	s.send(&dap.PauseResponse{Response: *newResponse(request.Request)})
+	// No need to send any event here.
+	// If we received this request while stopped, there already was an event for the stop.
+	// If we received this while running, then doCommand will unblock and trigger the right
+	// event, using debugger.StopReason because manual stop reason always wins even if we
+	// simultaneously receive a manual stop request and hit a breakpoint.
 }
 
 // stackFrame represents the index of a frame within
@@ -2092,7 +2098,11 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
 	}
 
 	stopReason := s.debugger.StopReason()
-	s.log.Debugf("%q command stopped - reason %q", command, stopReason)
+	file, line := "?", -1
+	if state != nil && state.CurrentThread != nil {
+		file, line = state.CurrentThread.File, state.CurrentThread.Line
+	}
+	s.log.Debugf("%q command stopped - reason %q, location %s:%d", command, stopReason, file, line)
 
 	s.resetHandlesForStoppedEvent()
 	stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
@@ -2109,7 +2119,7 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
 			stopped.Body.Reason = "step"
 		case proc.StopManual: // triggered by halt
 			stopped.Body.Reason = "pause"
-		case proc.StopUnknown: // can happen while stopping
+		case proc.StopUnknown: // can happen while terminating
 			stopped.Body.Reason = "unknown"
 		case proc.StopWatchpoint:
 			stopped.Body.Reason = "data breakpoint"


Test patch:
diff --git a/pkg/proc/dwarf_expr_test.go b/pkg/proc/dwarf_expr_test.go
index ed514c64..51860728 100644
--- a/pkg/proc/dwarf_expr_test.go
+++ b/pkg/proc/dwarf_expr_test.go
@@ -152,9 +152,11 @@ func TestDwarfExprRegisters(t *testing.T) {
 
 func TestDwarfExprComposite(t *testing.T) {
 	testCases := map[string]uint16{
-		"pair.k": 0x8765,
-		"pair.v": 0x5678,
-		"n":      42,
+		"pair.k":  0x8765,
+		"pair.v":  0x5678,
+		"n":       42,
+		"pair2.k": 0x8765,
+		"pair2.v": 0,
 	}
 
 	const stringVal = "this is a string"
@@ -188,6 +190,9 @@ func TestDwarfExprComposite(t *testing.T) {
 		op.DW_OP_reg1, op.DW_OP_piece, uint(8),
 		op.DW_OP_reg0, op.DW_OP_piece, uint(8)))
 	dwb.AddVariable("n", intoff, dwarfbuilder.LocationBlock(op.DW_OP_reg3))
+	dwb.AddVariable("pair2", pairoff, dwarfbuilder.LocationBlock(
+		op.DW_OP_reg2, op.DW_OP_piece, uint(2),
+		op.DW_OP_piece, uint(2)))
 	dwb.TagClose()
 
 	bi, _ := fakeBinaryInfo(t, dwb)
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 99ecbca7..6241a1b9 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -5169,9 +5169,9 @@ func TestCompositeMemoryWrite(t *testing.T) {
 		oldPc, oldRax, oldXmm1 := getregs()
 		t.Logf("PC %#x AX %#x XMM1 %#x", oldPc, oldRax, oldXmm1)
 
-		memRax, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, RegNum: 0, IsRegister: true}})
+		memRax, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, Val: 0, Kind: op.RegPiece}})
 		assertNoError(err, t, "NewCompositeMemory (rax)")
-		memXmm1, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, RegNum: 18, IsRegister: true}})
+		memXmm1, err := proc.NewCompositeMemory(p, []op.Piece{{Size: 0, Val: 18, Kind: op.RegPiece}})
 		assertNoError(err, t, "NewCompositeMemory (xmm1)")
 
 		if memRax := getmem(memRax); memRax != oldRax {
diff --git a/pkg/proc/proc_unexported_test.go b/pkg/proc/proc_unexported_test.go
index 62327ea8..ce269475 100644
--- a/pkg/proc/proc_unexported_test.go
+++ b/pkg/proc/proc_unexported_test.go
@@ -73,3 +73,26 @@ func TestAlignAddr(t *testing.T) {
 		c(example.align, example.in+0x10000, example.tgt+0x10000)
 	}
 }
+
+func TestConvertInt(t *testing.T) {
+	var testCases = []struct {
+		in     uint64
+		signed bool
+		size   int64
+		tgt    uint64
+	}{
+		{1, false, 1, 1},
+		{uint64(0xf0), true, 1, 0xfffffffffffffff0},
+		{uint64(0xf0), false, 1, 0xf0},
+		{uint64(0x70), true, 1, 0x70},
+		{uint64(0x90f0), true, 2, 0xffffffffffff90f0},
+		{uint64(0x90f0), false, 2, 0x90f0},
+	}
+	for _, tc := range testCases {
+		out := convertInt(tc.in, tc.signed, tc.size)
+		t.Logf("in=%#016x signed=%v size=%d -> %#016x\n", tc.in, tc.signed, tc.size, out)
+		if out != tc.tgt {
+			t.Errorf("expected=%#016x got=%#016x\n", tc.tgt, out)
+		}
+	}
+}
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index b58bfad7..52221adc 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -285,9 +285,9 @@ func (c *Client) StepOutRequest(thread int) {
 }
 
 // PauseRequest sends a 'pause' request.
-func (c *Client) PauseRequest() {
-	request := &dap.NextRequest{Request: *c.newRequest("pause")}
-	// TODO(polina): arguments
+func (c *Client) PauseRequest(threadId int) {
+	request := &dap.PauseRequest{Request: *c.newRequest("pause")}
+	request.Arguments.ThreadId = threadId
 	c.send(request)
 }
 
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 64a2ed75..93637cb0 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -572,6 +572,20 @@ func TestPreSetBreakpoint(t *testing.T) {
 		// "Continue" is triggered after the response is sent
 
 		client.ExpectTerminatedEvent(t)
+
+		// Pause request after termination should result in an error.
+		// But in certain cases this request actually succeeds.
+		client.PauseRequest(1)
+		switch r := client.ExpectMessage(t).(type) {
+		case *dap.ErrorResponse:
+			if r.Message != "Unable to halt execution" {
+				t.Errorf("\ngot  %#v\nwant Message='Unable to halt execution'", r)
+			}
+		case *dap.PauseResponse:
+		default:
+			t.Fatalf("Unexpected response type: expect error or pause, got %#v", r)
+		}
+
 		client.DisconnectRequest()
 		client.ExpectOutputEventProcessExited(t, 0)
 		client.ExpectOutputEventDetaching(t)
@@ -2821,15 +2835,8 @@ func TestFatalThrowBreakpoint(t *testing.T) {
 	})
 }
 
-// handleStop covers the standard sequence of reqeusts issued by
-// a client at a breakpoint or another non-terminal stop event.
-// The details have been tested by other tests,
-// so this is just a sanity check.
-// Skips line check if line is -1.
-func handleStop(t *testing.T, client *daptest.Client, thread int, name string, line int) {
+func verifyStopLocation(t *testing.T, client *daptest.Client, thread int, name string, line int) {
 	t.Helper()
-	client.ThreadsRequest()
-	client.ExpectThreadsResponse(t)
 
 	client.StackTraceRequest(thread, 0, 20)
 	st := client.ExpectStackTraceResponse(t)
@@ -2843,6 +2850,19 @@ func handleStop(t *testing.T, client *daptest.Client, thread int, name string, l
 			t.Errorf("\ngot  %#v\nwant Name=%q", st, name)
 		}
 	}
+}
+
+// handleStop 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,
+// so this is just a sanity check.
+// Skips line check if line is -1.
+func handleStop(t *testing.T, client *daptest.Client, thread int, name string, line int) {
+	t.Helper()
+	client.ThreadsRequest()
+	client.ExpectThreadsResponse(t)
+
+	verifyStopLocation(t, client, thread, name, line)
 
 	client.ScopesRequest(1000)
 	client.ExpectScopesResponse(t)
@@ -3140,6 +3160,51 @@ func TestAttachRequest(t *testing.T) {
 	})
 }
 
+func TestPauseAndContinue(t *testing.T) {
+	runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Set breakpoints
+			fixture.Source, []int{6},
+			[]onBreakpoint{{
+				execute: func() {
+					verifyStopLocation(t, client, 1, "main.loop", 6)
+
+					// Continue resumes all goroutines, so thread id is ignored
+					client.ContinueRequest(12345)
+					client.ExpectContinueResponse(t)
+
+					time.Sleep(time.Second)
+
+					// Halt pauses all goroutines, so thread id is ignored
+					client.PauseRequest(56789)
+					// Since we are in async mode while running, we might receive next two messages in either order.
+					for i := 0; i < 2; i++ {
+						msg := client.ExpectMessage(t)
+						switch m := msg.(type) {
+						case *dap.StoppedEvent:
+							if m.Body.Reason != "pause" || m.Body.ThreadId != 0 && m.Body.ThreadId != 1 {
+								t.Errorf("\ngot %#v\nwant ThreadId=0/1 Reason='pause'", m)
+							}
+						case *dap.PauseResponse:
+						default:
+							t.Fatalf("got %#v, want StoppedEvent or PauseResponse", m)
+						}
+					}
+
+					// Pause will be a no-op at a pause: there will be no additional stopped events
+					client.PauseRequest(1)
+					client.ExpectPauseResponse(t)
+				},
+				// The program has an infinite loop, so we must kill it by disconnecting.
+				disconnect: true,
+			}})
+	})
+}
+
 func TestUnupportedCommandResponses(t *testing.T) {
 	var got *dap.ErrorResponse
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
@@ -3188,24 +3253,6 @@ func TestUnupportedCommandResponses(t *testing.T) {
 	})
 }
 
-func TestRequiredNotYetImplementedResponses(t *testing.T) {
-	var got *dap.ErrorResponse
-	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
-		seqCnt := 1
-		expectNotYetImplemented := func(cmd string) {
-			t.Helper()
-			got = client.ExpectNotYetImplementedErrorResponse(t)
-			if got.RequestSeq != seqCnt || got.Command != cmd {
-				t.Errorf("\ngot  %#v\nwant RequestSeq=%d Command=%s", got, seqCnt, cmd)
-			}
-			seqCnt++
-		}
-
-		client.PauseRequest()
-		expectNotYetImplemented("pause")
-	})
-}
-
 func TestOptionalNotYetImplementedResponses(t *testing.T) {
 	var got *dap.ErrorResponse
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
diff --git a/service/test/variables_test.go b/service/test/variables_test.go
index 4bab82a9..a40e9710 100644
--- a/service/test/variables_test.go
+++ b/service/test/variables_test.go
@@ -836,6 +836,10 @@ func TestEvalExpression(t *testing.T) {
 		{`iface2map.(data)`, false, "…", "…", "map[string]interface {}", nil},
 
 		{"issue1578", false, "main.Block {cache: *main.Cache nil}", "main.Block {cache: *main.Cache nil}", "main.Block", nil},
+		{"ni8 << 2", false, "-20", "-20", "int8", nil},
+		{"ni8 << 8", false, "0", "0", "int8", nil},
+		{"ni8 >> 1", false, "-3", "-3", "int8", nil},
+		{"bytearray[0] * bytearray[0]", false, "144", "144", "uint8", nil},
 	}
 
 	ver, _ := goversion.Parse(runtime.Version())