From ce5238944d4ccb995dc577d23a2f4f5694bc90ae Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Thu, 14 Oct 2021 13:44:36 -0400 Subject: [PATCH] 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. --- service/dap/daptest/client.go | 14 +- service/dap/error_ids.go | 1 + service/dap/server.go | 205 +++++++++++++++++++++- service/dap/server_test.go | 320 +++++++++++++++++++++++++++++++++- 4 files changed, 531 insertions(+), 9 deletions(-) diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go index cb29f0b3..bf2143f6 100644 --- a/service/dap/daptest/client.go +++ b/service/dap/daptest/client.go @@ -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. diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go index 07a4d770..2f58956a 100644 --- a/service/dap/error_ids.go +++ b/service/dap/error_ids.go @@ -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 diff --git a/service/dap/server.go b/service/dap/server.go index 0fdd22be..ceed066c 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -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. diff --git a/service/dap/server_test.go b/service/dap/server_test.go index 203e9d0f..98925aaa 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -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) + } + }) + } +}