diff --git a/src/DebugEngineHost.VSCode/HostMarshal.cs b/src/DebugEngineHost.VSCode/HostMarshal.cs index c032b4503..f6a243124 100644 --- a/src/DebugEngineHost.VSCode/HostMarshal.cs +++ b/src/DebugEngineHost.VSCode/HostMarshal.cs @@ -75,20 +75,6 @@ public static void ReleaseCodeContextId(IntPtr codeContextId) } } - public static IDebugCodeContext2 GetCodeContextForIntPtr(IntPtr codeContextId) - { - lock (s_codeContexts) - { - IDebugCodeContext2 codeContext; - if (!s_codeContexts.TryGet(codeContextId.ToInt32(), out codeContext)) - { - throw new ArgumentOutOfRangeException(nameof(codeContextId)); - } - - return codeContext; - } - } - public static IDebugDocumentPosition2 GetDocumentPositionForIntPtr(IntPtr documentPositionId) { lock (s_documentPositions) @@ -145,7 +131,16 @@ public static IntPtr GetIntPtrForDataBreakpointAddress(string address) /// code context object public static IDebugCodeContext2 GetDebugCodeContextForIntPtr(IntPtr contextId) { - throw new NotImplementedException(); + lock (s_codeContexts) + { + IDebugCodeContext2 codeContext; + if (!s_codeContexts.TryGet(contextId.ToInt32(), out codeContext)) + { + throw new ArgumentOutOfRangeException(nameof(contextId)); + } + + return codeContext; + } } public static IDebugEventCallback2 GetThreadSafeEventCallback(IDebugEventCallback2 ad7Callback) diff --git a/src/OpenDebugAD7/AD7DebugSession.cs b/src/OpenDebugAD7/AD7DebugSession.cs index 648b90807..92bdb195b 100644 --- a/src/OpenDebugAD7/AD7DebugSession.cs +++ b/src/OpenDebugAD7/AD7DebugSession.cs @@ -50,6 +50,7 @@ internal sealed class AD7DebugSession : DebugAdapterBase, IDebugPortNotify2, IDe private int m_nextContextId = 1; private Dictionary m_functionBreakpoints; + private Dictionary m_instructionBreakpoints; private readonly HandleCollection m_frameHandles; private IDebugProgram2 m_program; @@ -119,6 +120,7 @@ public AD7DebugSession(Stream debugAdapterStdIn, Stream debugAdapterStdOut, List m_frameHandles = new HandleCollection(); m_breakpoints = new Dictionary>(); m_functionBreakpoints = new Dictionary(); + m_instructionBreakpoints = new Dictionary(); m_variableManager = new VariableManager(); } @@ -307,6 +309,49 @@ ppBPRequest is AD7BreakPointRequest ad7BreakpointRequest && return tracepoints; } + public StoppedEvent.ReasonValue GetStoppedEventReason(IDebugBreakpointEvent2 breakpointEvent) + { + StoppedEvent.ReasonValue reason = StoppedEvent.ReasonValue.Breakpoint; + + if (breakpointEvent != null) + { + if (breakpointEvent.EnumBreakpoints(out IEnumDebugBoundBreakpoints2 enumBreakpoints) == HRConstants.S_OK && + enumBreakpoints.GetCount(out uint bpCount) == HRConstants.S_OK && + bpCount > 0) + { + + bool allInstructionBreakpoints = true; + + IDebugBoundBreakpoint2[] boundBp = new IDebugBoundBreakpoint2[1]; + uint fetched = 0; + while (enumBreakpoints.Next(1, boundBp, ref fetched) == HRConstants.S_OK) + { + + if (boundBp[0].GetPendingBreakpoint(out IDebugPendingBreakpoint2 pendingBreakpoint) == HRConstants.S_OK) + { + if (pendingBreakpoint.GetBreakpointRequest(out IDebugBreakpointRequest2 breakpointRequest) == HRConstants.S_OK) + { + AD7BreakPointRequest request = breakpointRequest as AD7BreakPointRequest; + + if (breakpointRequest != null && request.MemoryContext == null) + { + allInstructionBreakpoints = false; + break; + } + } + } + } + + if (allInstructionBreakpoints) + { + reason = StoppedEvent.ReasonValue.InstructionBreakpoint; + } + } + } + + return reason; + } + private static long FileTimeToPosix(FILETIME ft) { long date = ((long)ft.dwHighDateTime << 32) + ft.dwLowDateTime; @@ -318,9 +363,9 @@ private static long FileTimeToPosix(FILETIME ft) return date / 10000000; } - private int GetMemoryContext(string memoryReference, int? offset, out IDebugMemoryContext2 memoryContext, out ulong address) + private ulong ResolveInstructionReference(string memoryReference, int? offset) { - memoryContext = null; + ulong address; if (memoryReference.StartsWith("0x", StringComparison.Ordinal)) { @@ -343,6 +388,15 @@ private int GetMemoryContext(string memoryReference, int? offset, out IDebugMemo } } + return address; + } + + private int GetMemoryContext(string memoryReference, int? offset, out IDebugMemoryContext2 memoryContext, out ulong address) + { + memoryContext = null; + + address = ResolveInstructionReference(memoryReference, offset); + int hr = HRConstants.E_NOTIMPL; // Engine does not support IDebugMemoryBytesDAP if (m_engine is IDebugMemoryBytesDAP debugMemoryBytesDAPEngine) @@ -413,11 +467,12 @@ internal void FireStoppedEvent(IDebugThread2 thread, StoppedEvent.ReasonValue re Protocol.SendEvent(new OpenDebugStoppedEvent() { Reason = reason, + Text = text, + ThreadId = thread.Id(), + // Additional Breakpoint Information for Testing/Logging Source = textPosition.Source, Line = textPosition.Line, Column = textPosition.Column, - Text = text, - ThreadId = thread.Id() }); }); @@ -492,7 +547,7 @@ private static IEnumerable GetBoundBreakpoints(IDebugBre IDebugCodeContext2 codeContext; try { - codeContext = HostMarshal.GetCodeContextForIntPtr(location.unionmember1); + codeContext = HostMarshal.GetDebugCodeContextForIntPtr(location.unionmember1); HostMarshal.ReleaseCodeContextId(location.unionmember1); location.unionmember1 = IntPtr.Zero; } @@ -825,6 +880,7 @@ protected override void HandleInitializeRequestAsync(IRequestResponder responder) + { + if (responder.Arguments.Breakpoints == null) + { + responder.SetError(new ProtocolException("HandleSetInstructionBreakpointsRequest failed: Missing 'breakpoints'.")); + return; + } + + ErrorBuilder eb = new ErrorBuilder(() => AD7Resources.Error_UnableToSetInstructionBreakpoint); + + SetInstructionBreakpointsResponse response = new SetInstructionBreakpointsResponse(); + + List breakpoints = responder.Arguments.Breakpoints; + Dictionary newBreakpoints = new Dictionary(); + try + { + HashSet requestAddresses = responder.Arguments.Breakpoints.Select(x => ResolveInstructionReference(x.InstructionReference, x.Offset)).ToHashSet(); + + foreach (KeyValuePair b in m_instructionBreakpoints) + { + if (requestAddresses.Contains(b.Key)) + { + newBreakpoints[b.Key] = b.Value; // breakpoint still in new list + } + else + { + IDebugPendingBreakpoint2 pendingBp = b.Value; + if (pendingBp != null && + pendingBp.GetBreakpointRequest(out IDebugBreakpointRequest2 request) == HRConstants.S_OK && + request is AD7BreakPointRequest ad7Request) + { + HostMarshal.ReleaseCodeContextId(ad7Request.MemoryContextIntPtr); + } + else + { + Debug.Fail("Why can't we retrieve the MemoryContextIntPtr?"); + } + b.Value.Delete(); // not in new list so delete it + } + } + + foreach (var instructionBp in responder.Arguments.Breakpoints) + { + eb.CheckHR(GetMemoryContext(instructionBp.InstructionReference, instructionBp.Offset, out IDebugMemoryContext2 memoryContext, out ulong address)); + + if (m_instructionBreakpoints.ContainsKey(address)) + { + IDebugBreakpointRequest2 breakpointRequest; + if (m_instructionBreakpoints[address].GetBreakpointRequest(out breakpointRequest) == 0 && + breakpointRequest is AD7BreakPointRequest ad7BPRequest) + { + // Check to see if this breakpoint has a condition that has changed. + if (!StringComparer.Ordinal.Equals(ad7BPRequest.Condition, instructionBp.Condition)) + { + // Condition has been modified. Delete breakpoint so it will be recreated with the updated condition. + var toRemove = m_instructionBreakpoints[address]; + toRemove.Delete(); + m_instructionBreakpoints.Remove(address); + } + else + { + if (ad7BPRequest.BindResult != null) + { + response.Breakpoints.Add(ad7BPRequest.BindResult); + } + else + { + response.Breakpoints.Add(new Breakpoint() + { + Id = (int)ad7BPRequest.Id, + Verified = true, + Line = 0 + }); + + } + continue; + } + } + } + else + { + IDebugPendingBreakpoint2 pendingBp; + AD7BreakPointRequest pBPRequest = new AD7BreakPointRequest(memoryContext); + + eb.CheckHR(m_engine.CreatePendingBreakpoint(pBPRequest, out pendingBp)); + + if (pendingBp != null && pendingBp.Bind() == HRConstants.S_OK) + { + newBreakpoints[address] = pendingBp; + response.Breakpoints.Add(new Breakpoint() + { + Id = (int)pBPRequest.Id, + Verified = true, + Line = 0 + }); // success + } + else + { + response.Breakpoints.Add(new Breakpoint() + { + Id = (int)pBPRequest.Id, + Verified = false, + Line = 0, + Message = string.Format(CultureInfo.CurrentCulture, AD7Resources.Error_UnableToSetInstructionBreakpoint, address) + }); // couldn't create and/or bind + } + } + } + + m_instructionBreakpoints = newBreakpoints; + + responder.SetResponse(response); + } + catch (Exception e) + { + responder.SetError(new ProtocolException(e.Message)); + } + } + + #endregion -#region IDebugPortNotify2 + #region IDebugPortNotify2 int IDebugPortNotify2.AddProgramNode(IDebugProgramNode2 programNode) { @@ -2693,7 +2868,9 @@ public void HandleIDebugEntryPointEvent2(IDebugEngine2 pEngine, IDebugProcess2 p public void HandleIDebugBreakpointEvent2(IDebugEngine2 pEngine, IDebugProcess2 pProcess, IDebugProgram2 pProgram, IDebugThread2 pThread, IDebugEvent2 pEvent) { - IList tracepoints = GetTracepoints(pEvent as IDebugBreakpointEvent2); + IDebugBreakpointEvent2 breakpointEvent = pEvent as IDebugBreakpointEvent2; + StoppedEvent.ReasonValue reason = GetStoppedEventReason(breakpointEvent); + IList tracepoints = GetTracepoints(breakpointEvent); if (tracepoints.Any()) { ThreadPool.QueueUserWorkItem((o) => @@ -2724,13 +2901,13 @@ public void HandleIDebugBreakpointEvent2(IDebugEngine2 pEngine, IDebugProcess2 p } else { - FireStoppedEvent(pThread, StoppedEvent.ReasonValue.Breakpoint); + FireStoppedEvent(pThread, reason); } }); } else { - FireStoppedEvent(pThread, StoppedEvent.ReasonValue.Breakpoint); + FireStoppedEvent(pThread, reason); } } diff --git a/src/OpenDebugAD7/AD7Impl/AD7BreakPointRequest.cs b/src/OpenDebugAD7/AD7Impl/AD7BreakPointRequest.cs index 96d36f1d7..63f70afa0 100644 --- a/src/OpenDebugAD7/AD7Impl/AD7BreakPointRequest.cs +++ b/src/OpenDebugAD7/AD7Impl/AD7BreakPointRequest.cs @@ -23,6 +23,13 @@ static public uint GetNextBreakpointId() public AD7FunctionPosition FunctionPosition { get; private set; } + public IDebugMemoryContext2 MemoryContext { get; private set; } + + // Used for Releasing the MemoryContext. + // Caller of AD7BreakPointRequest(MemoryContext) is required to + // release it with HostMarshal.ReleaseCodeContextId + public IntPtr MemoryContextIntPtr { get; private set; } + // Unique identifier for breakpoint when communicating with VSCode public uint Id { get; private set; } @@ -41,6 +48,11 @@ public AD7BreakPointRequest(string functionName) FunctionPosition = new AD7FunctionPosition(functionName); } + public AD7BreakPointRequest(IDebugMemoryContext2 memoryContext) + { + MemoryContext = memoryContext; + } + public int GetLocationType(enum_BP_LOCATION_TYPE[] pBPLocationType) { if (DocumentPosition != null) @@ -51,6 +63,10 @@ public int GetLocationType(enum_BP_LOCATION_TYPE[] pBPLocationType) { pBPLocationType[0] = enum_BP_LOCATION_TYPE.BPLT_CODE_FUNC_OFFSET; } + else if (MemoryContext != null) + { + pBPLocationType[0] = enum_BP_LOCATION_TYPE.BPLT_CODE_CONTEXT; + } return 0; } @@ -71,6 +87,13 @@ public int GetRequestInfo(enum_BPREQI_FIELDS dwFields, BP_REQUEST_INFO[] pBPRequ pBPRequestInfo[0].bpLocation.bpLocationType = (uint)enum_BP_LOCATION_TYPE.BPLT_CODE_FUNC_OFFSET; pBPRequestInfo[0].bpLocation.unionmember2 = HostMarshal.RegisterFunctionPosition(FunctionPosition); } + else if (MemoryContext != null) + { + pBPRequestInfo[0].bpLocation.bpLocationType = (uint)enum_BP_LOCATION_TYPE.BPLT_CODE_CONTEXT; + MemoryContextIntPtr = HostMarshal.RegisterCodeContext(MemoryContext as IDebugCodeContext2); + pBPRequestInfo[0].bpLocation.unionmember1 = MemoryContextIntPtr; + + } } if ((dwFields & enum_BPREQI_FIELDS.BPREQI_CONDITION) != 0 && !string.IsNullOrWhiteSpace(Condition)) { diff --git a/src/OpenDebugAD7/AD7Resources.Designer.cs b/src/OpenDebugAD7/AD7Resources.Designer.cs index b029c99c5..0fe3d5d0d 100644 --- a/src/OpenDebugAD7/AD7Resources.Designer.cs +++ b/src/OpenDebugAD7/AD7Resources.Designer.cs @@ -19,7 +19,7 @@ namespace OpenDebugAD7 { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class AD7Resources { @@ -406,6 +406,15 @@ internal static string Error_UnableToSetBreakpoint { } } + /// + /// Looks up a localized string similar to Error setting instruction breakpoint: {0}. + /// + internal static string Error_UnableToSetInstructionBreakpoint { + get { + return ResourceManager.GetString("Error_UnableToSetInstructionBreakpoint", resourceCulture); + } + } + /// /// Looks up a localized string similar to '{0}' cannot be assigned to. /// diff --git a/src/OpenDebugAD7/AD7Resources.resx b/src/OpenDebugAD7/AD7Resources.resx index bc916064d..aa0a29773 100644 --- a/src/OpenDebugAD7/AD7Resources.resx +++ b/src/OpenDebugAD7/AD7Resources.resx @@ -301,4 +301,7 @@ Set next statement is not supported by the current debugger. + + Error setting instruction breakpoint: {0} + \ No newline at end of file diff --git a/test/CppTests/Tests/MemoryTests.cs b/test/CppTests/Tests/MemoryTests.cs new file mode 100644 index 000000000..62269d5b4 --- /dev/null +++ b/test/CppTests/Tests/MemoryTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using DebuggerTesting; +using DebuggerTesting.Compilation; +using DebuggerTesting.OpenDebug; +using DebuggerTesting.OpenDebug.Commands; +using DebuggerTesting.OpenDebug.CrossPlatCpp; +using DebuggerTesting.OpenDebug.Events; +using DebuggerTesting.OpenDebug.Extensions; +using DebuggerTesting.Ordering; +using DebuggerTesting.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace CppTests.Tests +{ + [TestCaseOrderer(DependencyTestOrderer.TypeName, DependencyTestOrderer.AssemblyName)] + public class MemoryTests : TestBase + { + #region Constructor + + public MemoryTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + #endregion + + #region Methods + + [Theory] + [RequiresTestSettings] + public void CompileKitchenSinkForBreakpointTests(ITestSettings settings) + { + this.TestPurpose("Compiles the kitchen sink debuggee."); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.OpenAndCompile(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void InstructionBreakpointsBasic(ITestSettings settings) + { + this.TestPurpose("Tests basic operation of instruction breakpoints"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + SourceBreakpoints mainBreakpoints = debuggee.Breakpoints(SinkHelper.Main, 33); + + this.Comment("Set initial breakpoints"); + runner.SetBreakpoints(mainBreakpoints); + + this.Comment("Launch and run until first breakpoint"); + runner.Expects.HitBreakpointEvent(SinkHelper.Main, 33) + .AfterConfigurationDone(); + + string ip = string.Empty; + + this.Comment("Inspect the stack and try evaluation."); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + this.Comment("Get the stack trace"); + IFrameInspector mainFrame = inspector.Stack.First(); + inspector.AssertStackFrameNames(true, "main.*"); + + this.WriteLine("Main frame: {0}", mainFrame); + ip = mainFrame?.InstructionPointerReference; + } + + Assert.False(string.IsNullOrEmpty(ip)); + + // Send Disassemble Request to get the current instruction and next one. + this.WriteLine("Disassemble to get current and next instruction."); + IEnumerable instructions = runner.Disassemble(ip, 2); + + // Validate that we got two instructions. + Assert.Equal(2, instructions.Count()); + + // Get the next instruction's address + string nextIPAddress = instructions.Last().Address; + Assert.False(string.IsNullOrEmpty(nextIPAddress)); + + // Set an instruction breakpoint + this.Comment("Set Instruction Breakpoint"); + InstructionBreakpoints instruction = new InstructionBreakpoints(new string[] { nextIPAddress }); + runner.SetInstructionBreakpoints(instruction); + + // Expect it to be hit. + runner.Expects.HitInstructionBreakpointEvent(nextIPAddress).AfterContinue(); + + // Get the Stack Trace to validate the current frame's ipReference is the one set from the InstructionBp + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + this.Comment("Get the instruction bp's stack trace"); + IFrameInspector mainFrame = inspector.Stack.First(); + ip = mainFrame?.InstructionPointerReference; + + Assert.False(string.IsNullOrEmpty(ip)); + + Assert.Equal(nextIPAddress, ip); + } + + this.Comment("Continue until end"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + #endregion + } +} diff --git a/test/DebuggerTesting/OpenDebug/Commands/DisassembleCommand.cs b/test/DebuggerTesting/OpenDebug/Commands/DisassembleCommand.cs new file mode 100644 index 000000000..c8684f692 --- /dev/null +++ b/test/DebuggerTesting/OpenDebug/Commands/DisassembleCommand.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DebuggerTesting.OpenDebug.Commands.Responses; +using Newtonsoft.Json; +using System; + +namespace DebuggerTesting.OpenDebug.Commands +{ + public sealed class DisassembleArgs : JsonValue + { + public string memoryReference; + + public int? offset; + + public int? instructionOffset; + + public int instructionCount; + + public bool? resolveSymbols; + } + + public class DisassembleCommand : CommandWithResponse + { + public DisassembleCommand(string memoryReference, int? offset, int? instructionOffset, int instructionCount, bool? resolveSymbols) : base("disassemble") + { + this.Args.memoryReference = memoryReference; + this.Args.offset = offset; + this.Args.instructionOffset = instructionOffset; + this.Args.instructionCount = instructionCount; + this.Args.resolveSymbols = resolveSymbols; + } + } +} diff --git a/test/DebuggerTesting/OpenDebug/Commands/ReadMemoryCommand.cs b/test/DebuggerTesting/OpenDebug/Commands/ReadMemoryCommand.cs new file mode 100644 index 000000000..3dd9140ad --- /dev/null +++ b/test/DebuggerTesting/OpenDebug/Commands/ReadMemoryCommand.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DebuggerTesting.OpenDebug.Commands.Responses; +using Newtonsoft.Json; +using System; + +namespace DebuggerTesting.OpenDebug.Commands +{ + public sealed class ReadMemoryArgs : JsonValue + { + public string memoryReference; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? offset; + + public int count; + } + + public class ReadMemoryCommand : CommandWithResponse + { + public ReadMemoryCommand(string reference, int? offset, int count) : base("readMemory") + { + this.Args.memoryReference = reference; + this.Args.offset = offset; + this.Args.count = count; + } + } +} diff --git a/test/DebuggerTesting/OpenDebug/Commands/Responses/DisassembleResponseValue.cs b/test/DebuggerTesting/OpenDebug/Commands/Responses/DisassembleResponseValue.cs new file mode 100644 index 000000000..9b9b8e8ec --- /dev/null +++ b/test/DebuggerTesting/OpenDebug/Commands/Responses/DisassembleResponseValue.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace DebuggerTesting.OpenDebug.Commands.Responses +{ + public sealed class DisassembleResponseValue : CommandResponseValue + { + public sealed class Body + { + public sealed class DisassembledInstruction + { + public string address; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string instructionBytes; + + public string instruction; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string symbol; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public Source location; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? line; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? column; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? endLine; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? endColumn; + } + + public DisassembledInstruction[] instructions; + } + + public Body body = new Body(); + } +} diff --git a/test/DebuggerTesting/OpenDebug/Commands/Responses/ReadMemoryResponseValue.cs b/test/DebuggerTesting/OpenDebug/Commands/Responses/ReadMemoryResponseValue.cs new file mode 100644 index 000000000..0f1878df5 --- /dev/null +++ b/test/DebuggerTesting/OpenDebug/Commands/Responses/ReadMemoryResponseValue.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace DebuggerTesting.OpenDebug.Commands.Responses +{ + public sealed class ReadMemoryResponseValue : CommandResponseValue + { + public sealed class Body + { + public string address; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int unreadableBytes; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string data; + } + + public Body body = new Body(); + } +} diff --git a/test/DebuggerTesting/OpenDebug/Commands/Responses/StackTraceResponseValue.cs b/test/DebuggerTesting/OpenDebug/Commands/Responses/StackTraceResponseValue.cs index 64b4611ce..65e2a95b8 100644 --- a/test/DebuggerTesting/OpenDebug/Commands/Responses/StackTraceResponseValue.cs +++ b/test/DebuggerTesting/OpenDebug/Commands/Responses/StackTraceResponseValue.cs @@ -24,6 +24,9 @@ public sealed class StackFrame [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public Source source; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string instructionPointerReference; } public StackFrame[] stackFrames; } diff --git a/test/DebuggerTesting/OpenDebug/Commands/SetInstructionBreakpointCommand.cs b/test/DebuggerTesting/OpenDebug/Commands/SetInstructionBreakpointCommand.cs new file mode 100644 index 000000000..82872460c --- /dev/null +++ b/test/DebuggerTesting/OpenDebug/Commands/SetInstructionBreakpointCommand.cs @@ -0,0 +1,98 @@ +// // Copyright (c) Microsoft. All rights reserved. +// // Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DebuggerTesting.OpenDebug.Commands.Responses; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DebuggerTesting.OpenDebug.Commands +{ + + #region SetInstructionBreakpointCommandArgs + + public sealed class SetInstructionBreakpointCommandArgs : JsonValue + { + public sealed class InstructionBreakpoint + { + public InstructionBreakpoint(string instructionReference, string condition = null) + { + this.instructionReference = instructionReference; + this.condition = condition; + } + + public string instructionReference; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? offset; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string condition; + } + + public InstructionBreakpoint[] breakpoints; + } + + #endregion + + #region InstructionBreakpoints + + public sealed class InstructionBreakpoints + { + private List breakpoints; + + public InstructionBreakpoints() + { + this.breakpoints = new List(); + } + + public InstructionBreakpoints(params string[] addresses) + : this() + { + foreach (string address in addresses) + { + this.Add(address); + } + } + + public InstructionBreakpoints Add(string address, string condition = null) + { + this.breakpoints.Add(new SetInstructionBreakpointCommandArgs.InstructionBreakpoint(address, condition)); + return this; + } + + public InstructionBreakpoints Remove(string address) + { + this.breakpoints.RemoveAll(bp => String.Equals(bp.instructionReference, address, StringComparison.Ordinal)); + return this; + } + + internal IList Breakpoints + { + get { return this.breakpoints; } + } + } + + #endregion + + public class SetInstructionBreakpointsCommand : CommandWithResponse + { + public SetInstructionBreakpointsCommand() : base("setInstructionBreakpoints") + { + } + + public SetInstructionBreakpointsCommand(InstructionBreakpoints breakpoints) : + this() + { + this.Args.breakpoints = breakpoints.Breakpoints.ToArray(); + } + + public override string ToString() + { + return "{0} ({1})".FormatInvariantWithArgs(base.ToString(), String.Join(", ", this.Args.breakpoints.Select(bp => bp.instructionReference))); + } + } +} \ No newline at end of file diff --git a/test/DebuggerTesting/OpenDebug/Events/StoppedEvent.cs b/test/DebuggerTesting/OpenDebug/Events/StoppedEvent.cs index 6be870949..547fa007e 100644 --- a/test/DebuggerTesting/OpenDebug/Events/StoppedEvent.cs +++ b/test/DebuggerTesting/OpenDebug/Events/StoppedEvent.cs @@ -16,7 +16,8 @@ public enum StoppedReason Breakpoint, Pause, Exception, - Entry + Entry, + InstructionBreakpoint } #region StoppedEventValue @@ -63,6 +64,18 @@ public class StoppedEvent : Event private bool verifyLineRange; private int startLine; private int endLine; + private ulong address; + + public StoppedEvent(ulong address) + : base("stopped") + { + + this.address = address; + + this.ExpectedResponse.body.reason = FromReason(StoppedReason.InstructionBreakpoint); + + this.verifyLineRange = false; + } public StoppedEvent(StoppedReason? reason = null, string fileName = null, int? lineNumber = null, string text = null) : base("stopped") @@ -106,6 +119,12 @@ private static string FromReason(StoppedReason? reason) return null; Parameter.ThrowIfIsInvalid(reason.Value, StoppedReason.Unknown, nameof(reason)); + + if (reason == StoppedReason.InstructionBreakpoint) + { + return "instruction breakpoint"; + } + return Enum.GetName(typeof(StoppedReason), reason.Value).ToLowerInvariant(); } diff --git a/test/DebuggerTesting/OpenDebug/Extensions/DebuggerRunnerExtensions.cs b/test/DebuggerTesting/OpenDebug/Extensions/DebuggerRunnerExtensions.cs index a732fc94c..71373b9b2 100644 --- a/test/DebuggerTesting/OpenDebug/Extensions/DebuggerRunnerExtensions.cs +++ b/test/DebuggerTesting/OpenDebug/Extensions/DebuggerRunnerExtensions.cs @@ -7,7 +7,9 @@ using DebuggerTesting.OpenDebug.Commands; using DebuggerTesting.OpenDebug.Commands.Responses; using DebuggerTesting.OpenDebug.Events; +using Newtonsoft.Json; using Xunit; +using static DebuggerTesting.OpenDebug.Commands.Responses.DisassembleResponseValue.Body; namespace DebuggerTesting.OpenDebug.Extensions { @@ -18,6 +20,19 @@ public interface IThreadInfo IThreadInspector GetThreadInspector(); } + public interface IDisassemblyInstruction + { + public string Address { get; } + public string InstructionBytes { get; } + public string Instruction { get; } + public string Symbol { get; } + public Source Location { get; } + public int? Line { get; } + public int? Column { get; } + public int? EndLine { get; } + public int? EndColumn { get; } + } + public static class DebuggerRunnerExtensions { /// @@ -141,6 +156,36 @@ public IThreadInspector GetThreadInspector() #endregion + #region DisassembleInstruction + + internal class DisassemblyInstruction: IDisassemblyInstruction + { + public string Address { get; private set; } + public string InstructionBytes { get; private set; } + public string Instruction { get; private set; } + public string Symbol { get; private set; } + public Source Location { get; private set; } + public int? Line { get; private set; } + public int? Column { get; private set; } + public int? EndLine { get; private set; } + public int? EndColumn { get; private set; } + + public DisassemblyInstruction(DisassembledInstruction disassembledInstruction) + { + this.Address = disassembledInstruction.address; + this.InstructionBytes = disassembledInstruction.instructionBytes; + this.Instruction = disassembledInstruction.instruction; + this.Symbol = disassembledInstruction.symbol; + this.Location = disassembledInstruction.location; + this.Line = disassembledInstruction.line; + this.Column = disassembledInstruction.column; + this.EndLine = disassembledInstruction.endLine; + this.EndColumn = disassembledInstruction.endColumn; + } + } + + #endregion + public static void RunCommandExpectFailure(this IDebuggerRunner runner, ICommand command) { command.ExpectsSuccess = false; @@ -162,6 +207,11 @@ public static SetBreakpointsResponseValue SetFunctionBreakpoints(this IDebuggerR return runner.RunCommand(new SetFunctionBreakpointsCommand(breakpoints)); } + public static SetBreakpointsResponseValue SetInstructionBreakpoints(this IDebuggerRunner runner, InstructionBreakpoints breakpoints) + { + return runner.RunCommand(new SetInstructionBreakpointsCommand(breakpoints)); + } + public static void Continue(this IDebuggerRunner runner) { runner.RunCommand(new ContinueCommand(runner.StoppedThreadId)); @@ -172,6 +222,18 @@ public static void ConfigurationDone(this IDebuggerRunner runner) runner.RunCommand(new ConfigurationDoneCommand()); } + public static ReadMemoryResponseValue ReadMemory(this IDebuggerRunner runner, string reference, int? offset, int count) + { + ReadMemoryResponseValue response = runner.RunCommand(new ReadMemoryCommand(reference, offset, count)); + return response; + } + + public static IEnumerable Disassemble(this IDebuggerRunner runner, string memoryReference, int instructionCount) + { + DisassembleResponseValue response = runner.RunCommand(new DisassembleCommand(memoryReference, 0, 0, instructionCount, false)); + return response?.body?.instructions.Select(i => new DisassemblyInstruction(i)); + } + /// /// Adds an expected stop event within a range of line numbers. If the stop does not occur on , then perform a step over /// and verify that the debuggee breaks at the . diff --git a/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs b/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs index 05e549077..ce1e5b06b 100644 --- a/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs +++ b/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs @@ -20,10 +20,11 @@ internal class FrameInspector : DisposableObject, IFrameInspector private int? line; private int? column; private int? sourceReference; + private string instructionPointerReference; #region Constructor/Dispose - public FrameInspector(IDebuggerRunner runner, string name, int id, string sourceName, string sourcePath, int? sourceReference, int? line, int? column) + public FrameInspector(IDebuggerRunner runner, string name, int id, string sourceName, string sourcePath, int? sourceReference, int? line, int? column, string instructionPointerReference) { Parameter.ThrowIfNull(runner, nameof(runner)); this.DebuggerRunner = runner; @@ -34,6 +35,7 @@ public FrameInspector(IDebuggerRunner runner, string name, int id, string source this.sourceReference = sourceReference; this.line = line; this.column = column; + this.instructionPointerReference = instructionPointerReference; } protected override void Dispose(bool isDisposing) @@ -116,6 +118,15 @@ public int? Column } } + public string InstructionPointerReference + { + get + { + this.VerifyNotDisposed(); + return this.instructionPointerReference; + } + } + #endregion /// diff --git a/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs b/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs index 593d82feb..caf6b2aff 100644 --- a/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs +++ b/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs @@ -67,8 +67,11 @@ public interface IFrameInspector : IVariableExpander, IInspector int? SourceReference { get; } int? Line { get; } + int? Column { get; } + string InstructionPointerReference { get; } + /// /// Evaluates an expression on this frame /// diff --git a/test/DebuggerTesting/OpenDebug/Extensions/ThreadInspector.cs b/test/DebuggerTesting/OpenDebug/Extensions/ThreadInspector.cs index 6b2510937..ba614f451 100644 --- a/test/DebuggerTesting/OpenDebug/Extensions/ThreadInspector.cs +++ b/test/DebuggerTesting/OpenDebug/Extensions/ThreadInspector.cs @@ -65,8 +65,9 @@ public IEnumerable Stack int? sourceReference = stackFrame.source?.sourceReference; int? line = stackFrame.line; int? column = stackFrame.column; + string instructionPointerReference = stackFrame.instructionPointerReference; - FrameInspector frame = new FrameInspector(this.DebuggerRunner, name, id, sourceName, sourcePath, sourceReference, line, column); + FrameInspector frame = new FrameInspector(this.DebuggerRunner, name, id, sourceName, sourcePath, sourceReference, line, column, instructionPointerReference); this.frameInspectors.Add(frame); yield return frame; } diff --git a/test/DebuggerTesting/OpenDebug/RunBuilder.cs b/test/DebuggerTesting/OpenDebug/RunBuilder.cs index 217e673de..6e0df5945 100644 --- a/test/DebuggerTesting/OpenDebug/RunBuilder.cs +++ b/test/DebuggerTesting/OpenDebug/RunBuilder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Net; using DebuggerTesting.OpenDebug.Commands; using DebuggerTesting.OpenDebug.Events; @@ -45,6 +46,22 @@ public static IRunBuilder StoppedEvent(this IRunBuilder runBuilder, StoppedReaso return runBuilder.Event(new StoppedEvent(reason, fileName, lineNumber, text)); } + public static IRunBuilder HitInstructionBreakpointEvent(this IRunBuilder runBuilder, string address) + { + ulong nextAddress; + + if (address.StartsWith("0x", StringComparison.Ordinal)) + { + nextAddress = Convert.ToUInt64(address.Substring(2), 16); + } + else + { + nextAddress = Convert.ToUInt64(address, 10); + } + + return runBuilder.Event(new StoppedEvent(nextAddress)); + } + public static IRunBuilder HitBreakpointEvent(this IRunBuilder runBuilder, string fileName = null, int? lineNumber = null, string text = null) { return runBuilder.StoppedEvent(StoppedReason.Breakpoint, fileName, lineNumber, text);