service/dap: support disassemble request (#2728)

This adds support for disassembling a region of code using the instructionPointerReference value returned in the stack trace request.
This commit is contained in:
Suzy Mueller 2021-10-14 13:44:36 -04:00 committed by GitHub
parent 6cf7a7149d
commit ce5238944d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 531 additions and 9 deletions

@ -114,6 +114,7 @@ func (c *Client) ExpectInitializeResponseAndCapabilities(t *testing.T) *dap.Init
SupportsClipboardContext: true,
SupportsSteppingGranularity: true,
SupportsLogPoints: true,
SupportsDisassembleRequest: true,
}
if !reflect.DeepEqual(initResp.Body, wantCapabilities) {
t.Errorf("capabilities in initializeResponse: got %+v, want %v", pretty(initResp.Body), pretty(wantCapabilities))
@ -518,8 +519,17 @@ func (c *Client) ReadMemoryRequest() {
}
// DisassembleRequest sends a 'disassemble' request.
func (c *Client) DisassembleRequest() {
c.send(&dap.DisassembleRequest{Request: *c.newRequest("disassemble")})
func (c *Client) DisassembleRequest(memoryReference string, instructionOffset, inctructionCount int) {
c.send(&dap.DisassembleRequest{
Request: *c.newRequest("disassemble"),
Arguments: dap.DisassembleArguments{
MemoryReference: memoryReference,
Offset: 0,
InstructionOffset: instructionOffset,
InstructionCount: inctructionCount,
ResolveSymbols: false,
},
})
}
// CancelRequest sends a 'cancel' request.

@ -24,6 +24,7 @@ const (
UnableToHalt = 2010
UnableToGetExceptionInfo = 2011
UnableToSetVariable = 2012
UnableToDisassemble = 2013
// Add more codes as we support more requests
NoDebugIsRunning = 3000
DebuggeeIsRunning = 4000

@ -26,6 +26,7 @@ import (
"regexp"
"runtime"
"runtime/debug"
"sort"
"strconv"
"strings"
"sync"
@ -830,6 +831,7 @@ func (s *Session) onInitializeRequest(request *dap.InitializeRequest) {
response.Body.SupportsClipboardContext = true
response.Body.SupportsSteppingGranularity = true
response.Body.SupportsLogPoints = true
response.Body.SupportsDisassembleRequest = true
// TODO(polina): support these requests in addition to vscode-go feature parity
response.Body.SupportsTerminateRequest = false
response.Body.SupportsRestartRequest = false
@ -837,7 +839,6 @@ func (s *Session) onInitializeRequest(request *dap.InitializeRequest) {
response.Body.SupportsSetExpression = false
response.Body.SupportsLoadedSourcesRequest = false
response.Body.SupportsReadMemoryRequest = false
response.Body.SupportsDisassembleRequest = false
response.Body.SupportsCancelRequest = false
s.send(response)
}
@ -2833,10 +2834,206 @@ func (s *Session) onReadMemoryRequest(request *dap.ReadMemoryRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
// onDisassembleRequest sends a not-yet-implemented error response.
// Capability 'supportsDisassembleRequest' is not set 'initialize' response.
var invalidInstruction = dap.DisassembledInstruction{
Address: "out of bounds",
Instruction: "invalid instruction",
}
// onDisassembleRequest handles 'disassemble' requests.
// Capability 'supportsDisassembleRequest' is set in 'initialize' response.
func (s *Session) onDisassembleRequest(request *dap.DisassembleRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
// If the requested memory address is an invalid location, return all invalid instructions.
// TODO(suzmue): consider adding fake addresses that would allow us to receive out of bounds
// requests that include valid instructions designated by InstructionOffset or InstructionCount.
if request.Arguments.MemoryReference == invalidInstruction.Address {
instructions := make([]dap.DisassembledInstruction, request.Arguments.InstructionCount)
for i := range instructions {
instructions[i] = invalidInstruction
}
response := &dap.DisassembleResponse{
Response: *newResponse(request.Request),
Body: dap.DisassembleResponseBody{
Instructions: instructions,
},
}
s.send(response)
return
}
// TODO(suzmue): microsoft/vscode#129655 is discussing the difference between
// memory reference and instructionPointerReference, which are currently
// being used interchangeably by vscode.
addr, err := strconv.ParseInt(request.Arguments.MemoryReference, 0, 64)
if err != nil {
s.sendErrorResponse(request.Request, UnableToDisassemble, "Unable to disassemble", err.Error())
return
}
start := uint64(addr)
maxInstructionLength := s.debugger.Target().BinInfo().Arch.MaxInstructionLength()
byteOffset := request.Arguments.InstructionOffset * maxInstructionLength
// Adjust the offset to include instructions before the requested address.
if byteOffset < 0 {
start = uint64(addr + int64(byteOffset))
}
// Adjust the number of instructions to include enough instructions after
// the requested address.
count := request.Arguments.InstructionCount
if byteOffset > 0 {
count += byteOffset
}
end := uint64(addr + int64(count*maxInstructionLength))
// Make sure the PCs are lined up with instructions.
start, end = alignPCs(s.debugger.Target().BinInfo(), start, end)
// Disassemble the instructions
procInstructions, err := s.debugger.Disassemble(-1, start, end)
if err != nil {
s.sendErrorResponse(request.Request, UnableToDisassemble, "Unable to disassemble", err.Error())
return
}
// Find the section of instructions that were requested.
procInstructions, offset, err := findInstructions(procInstructions, addr, request.Arguments.InstructionOffset, request.Arguments.InstructionCount)
if err != nil {
s.sendErrorResponse(request.Request, UnableToDisassemble, "Unable to disassemble", err.Error())
return
}
// Turn the given range of instructions into dap instructions.
instructions := make([]dap.DisassembledInstruction, request.Arguments.InstructionCount)
lastFile, lastLine := "", -1
for i := range instructions {
if i < offset || (i-offset) >= len(procInstructions) {
// i is not in a valid range.
instructions[i] = invalidInstruction
continue
}
instruction := api.ConvertAsmInstruction(procInstructions[i-offset], s.debugger.AsmInstructionText(&procInstructions[i-offset], proc.GoFlavour))
instructions[i] = dap.DisassembledInstruction{
Address: fmt.Sprintf("%#x", instruction.Loc.PC),
InstructionBytes: fmt.Sprintf("%x", instruction.Bytes),
Instruction: instruction.Text,
}
// Only set the location on the first instruction for a given line.
if instruction.Loc.File != lastFile || instruction.Loc.Line != lastLine {
instructions[i].Location = dap.Source{Path: instruction.Loc.File}
instructions[i].Line = instruction.Loc.Line
lastFile, lastLine = instruction.Loc.File, instruction.Loc.Line
}
}
response := &dap.DisassembleResponse{
Response: *newResponse(request.Request),
Body: dap.DisassembleResponseBody{
Instructions: instructions,
},
}
s.send(response)
}
func findInstructions(procInstructions []proc.AsmInstruction, addr int64, instructionOffset, count int) ([]proc.AsmInstruction, int, error) {
ref := sort.Search(len(procInstructions), func(i int) bool {
return procInstructions[i].Loc.PC >= uint64(addr)
})
if ref == len(procInstructions) || procInstructions[ref].Loc.PC != uint64(addr) {
return nil, -1, fmt.Errorf("could not find memory reference")
}
// offset is the number of instructions that should appear before the first instruction
// returned by findInstructions.
offset := 0
if ref+instructionOffset < 0 {
offset = -(ref + instructionOffset)
}
// Figure out the index to slice at.
startIdx := ref + instructionOffset
endIdx := ref + instructionOffset + count
if endIdx <= 0 || startIdx >= len(procInstructions) {
return []proc.AsmInstruction{}, 0, nil
}
// Adjust start and end to be inbounds.
if startIdx < 0 {
offset = -startIdx
startIdx = 0
}
if endIdx > len(procInstructions) {
endIdx = len(procInstructions)
}
return procInstructions[startIdx:endIdx], offset, nil
}
func alignPCs(bi *proc.BinaryInfo, start, end uint64) (uint64, uint64) {
// We want to find the function locations position that would enclose
// the range from start to end.
//
// Example:
//
// 0x0000 instruction (func1)
// 0x0004 instruction (func1)
// 0x0008 instruction (func1)
// 0x000c nop
// 0x000e nop
// 0x0000 nop
// 0x0002 nop
// 0x0004 instruction (func2)
// 0x0008 instruction (func2)
// 0x000c instruction (func2)
//
// start values:
// < 0x0000 at func1.Entry = 0x0000
// 0x0000-0x000b at func1.Entry = 0x0000
// 0x000c-0x0003 at func1.End = 0x000c
// 0x0004-0x000f at func2.Entry = 0x0004
// > 0x000f at func2.End = 0x0010
//
// end values:
// < 0x0000 at func1.Entry = 0x0000
// 0x0000-0x000b at func1.End = 0x0000
// 0x000c-0x0003 at func2.Entry = 0x000c
// 0x0004-0x000f at func2.End = 0x0004
// > 0x000f at func2.End = 0x0004
// Handle start values:
fn := bi.PCToFunc(start)
if fn != nil {
// start is in a funcition.
start = fn.Entry
} else if b, pc := checkOutOfAddressSpace(start, bi); b {
start = pc
} else {
// Otherwise it must come after some function.
i := sort.Search(len(bi.Functions), func(i int) bool {
fn := bi.Functions[len(bi.Functions)-(i+1)]
return start >= fn.End
})
start = bi.Functions[len(bi.Functions)-(i+1)].Entry
}
// Handle end values:
if fn := bi.PCToFunc(end); fn != nil {
// end is in a funcition.
end = fn.End
} else if b, pc := checkOutOfAddressSpace(end, bi); b {
end = pc
} else {
// Otherwise it must come before some function.
i := sort.Search(len(bi.Functions), func(i int) bool {
fn := bi.Functions[i]
return end < fn.Entry
})
end = bi.Functions[i].Entry
}
return start, end
}
func checkOutOfAddressSpace(pc uint64, bi *proc.BinaryInfo) (bool, uint64) {
if pc < bi.Functions[0].Entry {
return true, bi.Functions[0].Entry
}
if pc >= bi.Functions[len(bi.Functions)-1].End {
return true, bi.Functions[len(bi.Functions)-1].End
}
return false, pc
}
// onCancelRequest sends a not-yet-implemented error response.

@ -11,6 +11,7 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
@ -20,6 +21,7 @@ import (
"github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc"
protest "github.com/go-delve/delve/pkg/proc/test"
"github.com/go-delve/delve/service"
"github.com/go-delve/delve/service/api"
@ -5436,9 +5438,6 @@ func TestOptionalNotYetImplementedResponses(t *testing.T) {
client.ReadMemoryRequest()
expectNotYetImplemented("readMemory")
client.DisassembleRequest()
expectNotYetImplemented("disassemble")
client.CancelRequest()
expectNotYetImplemented("cancel")
@ -6002,3 +6001,318 @@ func TestBadlyFormattedMessageToServer(t *testing.T) {
client.ExpectDisconnectResponse(t)
})
}
func TestDisassemble(t *testing.T) {
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
// Set breakpoints
fixture.Source, []int{17},
[]onBreakpoint{{
// Stop at line 17
execute: func() {
checkStop(t, client, 1, "main.main", 17)
client.StackTraceRequest(1, 0, 1)
st := client.ExpectStackTraceResponse(t)
if len(st.Body.StackFrames) < 1 {
t.Fatalf("\ngot %#v\nwant len(stackframes) => 1", st)
}
// Request the single instruction that the program is stopped at.
pc := st.Body.StackFrames[0].InstructionPointerReference
client.DisassembleRequest(pc, 0, 1)
dr := client.ExpectDisassembleResponse(t)
if len(dr.Body.Instructions) != 1 {
t.Errorf("\ngot %#v\nwant len(instructions) = 1", dr)
} else if dr.Body.Instructions[0].Address != pc {
t.Errorf("\ngot %#v\nwant instructions[0].Address = %s", dr, pc)
}
// Request the instruction that the program is stopped at, and the two
// surrounding it.
client.DisassembleRequest(pc, -1, 3)
dr = client.ExpectDisassembleResponse(t)
if len(dr.Body.Instructions) != 3 {
t.Errorf("\ngot %#v\nwant len(instructions) = 3", dr)
} else if dr.Body.Instructions[1].Address != pc {
t.Errorf("\ngot %#v\nwant instructions[1].Address = %s", dr, pc)
}
// Request zero instrutions.
client.DisassembleRequest(pc, 0, 0)
dr = client.ExpectDisassembleResponse(t)
if len(dr.Body.Instructions) != 0 {
t.Errorf("\ngot %#v\nwant len(instructions) = 0", dr)
}
// Request invalid instructions.
client.DisassembleRequest(invalidInstruction.Address, 0, 10)
dr = client.ExpectDisassembleResponse(t)
if len(dr.Body.Instructions) != 10 {
t.Errorf("\ngot %#v\nwant len(instructions) = 10", dr)
}
for i, got := range dr.Body.Instructions {
if !reflect.DeepEqual(got, invalidInstruction) {
t.Errorf("\ngot [%d]=%#v\nwant = %#v", i, got, invalidInstruction)
}
}
// Bad request, not a number.
client.DisassembleRequest("hello, world!", 0, 1)
client.ExpectErrorResponse(t)
// Bad request, not an address in program.
client.DisassembleRequest("0x5", 0, 100)
client.ExpectErrorResponse(t)
},
disconnect: true,
}},
)
})
}
func TestAlignPCs(t *testing.T) {
NUM_FUNCS := 10
// Create fake functions to test align PCs.
funcs := make([]proc.Function, NUM_FUNCS)
for i := 0; i < len(funcs); i++ {
funcs[i] = proc.Function{
Entry: uint64(100 + i*10),
End: uint64(100 + i*10 + 5),
}
}
bi := &proc.BinaryInfo{
Functions: funcs,
}
type args struct {
start uint64
end uint64
}
tests := []struct {
name string
args args
wantStart uint64
wantEnd uint64
}{
{
name: "out of bounds",
args: args{
start: funcs[0].Entry - 5,
end: funcs[NUM_FUNCS-1].End + 5,
},
wantStart: funcs[0].Entry, // start of first function
wantEnd: funcs[NUM_FUNCS-1].End, // end of last function
},
{
name: "same function",
args: args{
start: funcs[1].Entry + 1,
end: funcs[1].Entry + 2,
},
wantStart: funcs[1].Entry, // start of containing function
wantEnd: funcs[1].End, // end of containing function
},
{
name: "between functions",
args: args{
start: funcs[1].End + 1,
end: funcs[1].End + 2,
},
wantStart: funcs[1].Entry, // start of function before
wantEnd: funcs[2].Entry, // start of function after
},
{
name: "start of function",
args: args{
start: funcs[2].Entry,
end: funcs[5].Entry,
},
wantStart: funcs[2].Entry, // start of current function
wantEnd: funcs[5].End, // end of current function
},
{
name: "end of function",
args: args{
start: funcs[4].End,
end: funcs[8].End,
},
wantStart: funcs[4].Entry, // start of current function
wantEnd: funcs[9].Entry, // start of next function
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotStart, gotEnd := alignPCs(bi, tt.args.start, tt.args.end)
if gotStart != tt.wantStart {
t.Errorf("alignPCs() got start = %v, want %v", gotStart, tt.wantStart)
}
if gotEnd != tt.wantEnd {
t.Errorf("alignPCs() got end = %v, want %v", gotEnd, tt.wantEnd)
}
})
}
}
func TestFindInstructions(t *testing.T) {
numInstructions := 100
startPC := 0x1000
procInstructions := make([]proc.AsmInstruction, numInstructions)
for i := 0; i < len(procInstructions); i++ {
procInstructions[i] = proc.AsmInstruction{
Loc: proc.Location{
PC: uint64(startPC + 2*i),
},
}
}
type args struct {
addr int64
offset int
count int
}
tests := []struct {
name string
args args
wantInstructions []proc.AsmInstruction
wantOffset int
wantErr bool
}{
{
name: "request all",
args: args{
addr: int64(startPC),
offset: 0,
count: 100,
},
wantInstructions: procInstructions,
wantOffset: 0,
wantErr: false,
},
{
name: "request all (with offset)",
args: args{
addr: int64(startPC + numInstructions), // the instruction addr at numInstructions/2
offset: -numInstructions / 2,
count: numInstructions,
},
wantInstructions: procInstructions,
wantOffset: 0,
wantErr: false,
},
{
name: "request half (with offset)",
args: args{
addr: int64(startPC),
offset: 0,
count: numInstructions / 2,
},
wantInstructions: procInstructions[:numInstructions/2],
wantOffset: 0,
wantErr: false,
},
{
name: "request half (with offset)",
args: args{
addr: int64(startPC),
offset: numInstructions / 2,
count: numInstructions / 2,
},
wantInstructions: procInstructions[numInstructions/2:],
wantOffset: 0,
wantErr: false,
},
{
name: "request too many",
args: args{
addr: int64(startPC),
offset: 0,
count: numInstructions * 2,
},
wantInstructions: procInstructions,
wantOffset: 0,
wantErr: false,
},
{
name: "request too many with offset",
args: args{
addr: int64(startPC),
offset: -numInstructions,
count: numInstructions * 2,
},
wantInstructions: procInstructions,
wantOffset: numInstructions,
wantErr: false,
},
{
name: "request out of bounds",
args: args{
addr: int64(startPC),
offset: -numInstructions,
count: numInstructions,
},
wantInstructions: []proc.AsmInstruction{},
wantOffset: 0,
wantErr: false,
},
{
name: "request out of bounds",
args: args{
addr: int64(uint64(startPC + 2*(numInstructions-1))),
offset: 1,
count: numInstructions,
},
wantInstructions: []proc.AsmInstruction{},
wantOffset: 0,
wantErr: false,
},
{
name: "addr out of bounds (low)",
args: args{
addr: 0,
offset: 0,
count: 100,
},
wantInstructions: nil,
wantOffset: -1,
wantErr: true,
},
{
name: "addr out of bounds (high)",
args: args{
addr: int64(startPC + 2*(numInstructions+1)),
offset: -10,
count: 20,
},
wantInstructions: nil,
wantOffset: -1,
wantErr: true,
},
{
name: "addr not aligned",
args: args{
addr: int64(startPC + 1),
offset: 0,
count: 20,
},
wantInstructions: nil,
wantOffset: -1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotInstructions, gotOffset, err := findInstructions(procInstructions, tt.args.addr, tt.args.offset, tt.args.count)
if (err != nil) != tt.wantErr {
t.Errorf("findInstructions() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotInstructions, tt.wantInstructions) {
t.Errorf("findInstructions() got instructions = %v, want %v", gotInstructions, tt.wantInstructions)
}
if gotOffset != tt.wantOffset {
t.Errorf("findInstructions() got offset = %v, want %v", gotOffset, tt.wantOffset)
}
})
}
}