mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-08-08 07:40:39 -09:00
GP-7009: from review
GP-7009: restoring build.gradle (again?) GP-7009: from Eclipse testing GP-7009: hmmm, not so much GP-7009: from review GP-7009: help revisions GP-7009: struggling with help GP-7009: struggling with help GP-7009: stubs GP-7009: local trash GP-7009: reset build.gradle GP-7009: NPE checks GP-7009: minor edits GP-7009: minor edits GP-7009: better connections story GP-7009: suggestions GP-7009: more cmds GP-7009: options GP-7009: mostly functional GP-7009: better breakpoints GP-7009: events GP-7009: towards events GP-7009: more methods GP-7009: mostly working methods GP-7009: jdk25 test GP-7009: test GP-7009: oops GP-7009: functional death star GP-7009: minimal working server GP-7009: untested GP-7009: fleshing things out GP-7009: one working method GP-7009: first pass GP-7009: first pass
This commit is contained in:
0
Ghidra/Debug/Debugger-dap/Module.manifest
Normal file
0
Ghidra/Debug/Debugger-dap/Module.manifest
Normal file
1
Ghidra/Debug/Debugger-dap/README.md
Normal file
1
Ghidra/Debug/Debugger-dap/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Debugger-DAP: provides server-side DAP access to the debugger agents
|
||||
30
Ghidra/Debug/Debugger-dap/build.gradle
Normal file
30
Ghidra/Debug/Debugger-dap/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
apply from: "${rootProject.projectDir}/gradle/javaProject.gradle"
|
||||
apply from: "${rootProject.projectDir}/gradle/helpProject.gradle"
|
||||
apply from: "${rootProject.projectDir}/gradle/distributableGhidraModule.gradle"
|
||||
|
||||
apply plugin: 'eclipse'
|
||||
eclipse.project.name = 'Debug Debugger-dap'
|
||||
|
||||
dependencies {
|
||||
api project(':Debugger-rmi-trace')
|
||||
api "org.eclipse.lsp4j:org.eclipse.lsp4j:1.0.0"
|
||||
api "org.eclipse.lsp4j:org.eclipse.lsp4j.debug:1.0.0"
|
||||
api "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:1.0.0"
|
||||
api "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc.debug:1.0.0"
|
||||
}
|
||||
5
Ghidra/Debug/Debugger-dap/certification.manifest
Normal file
5
Ghidra/Debug/Debugger-dap/certification.manifest
Normal file
@@ -0,0 +1,5 @@
|
||||
##VERSION: 2.0
|
||||
Module.manifest||GHIDRA||||END|
|
||||
README.md||GHIDRA||||END|
|
||||
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
|
||||
src/main/help/help/topics/dap/dap.html||GHIDRA||||END|
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version='1.0' encoding='ISO-8859-1'?>
|
||||
<!-- See Base's TOC_Source.xml for help -->
|
||||
<tocroot>
|
||||
<tocref id="Debugger">
|
||||
<tocdef id="dap" text="Debugger Adapter Protocol (DAP)"
|
||||
target="help/topics/dap/dap.html">
|
||||
</tocdef>
|
||||
</tocref>
|
||||
</tocroot>
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
|
||||
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META name="generator" content=
|
||||
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
|
||||
|
||||
<TITLE>Debugger Adapter Protocol (DAP)</TITLE>
|
||||
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
|
||||
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
|
||||
</HEAD>
|
||||
|
||||
<BODY lang="EN-US">
|
||||
<H1>DAP Server</H1>
|
||||
|
||||
<P>The DebuggerDapServerPlugin provides a very simple adapter allowing access to the Ghidra
|
||||
debugger by standard DAP clients. The plugin provides two actions, <B>Start server</B> and
|
||||
<B>Stop server</B>, and two options, <B>IP Address</B> and <B>Port</B>. The server will remain
|
||||
running until the stop command is issued. Ghidra debugger agents may launch and kill sessions
|
||||
without restarting the server, and DAP clients may connect, disconnect, and reconnect. Running
|
||||
multiple sessions simultaneously has not been tested extensively and requires, at a minimum,
|
||||
changing port values.</P>
|
||||
|
||||
<H1>DAP Clients</H1>
|
||||
|
||||
<P>Exact configuration and use of the DAP client will be largely client-dependent, particularly
|
||||
with regard to supported commands. <B>Attach</B> and <B>launch</B> are again largely untested,
|
||||
as the server assumes an active agent session. As a starting example, the DAP server may be
|
||||
accessed from <B><TT>VSCode</TT></B> by writing a <TT>config.yaml</TT> file, and pointing a new
|
||||
or existing debuggger configuration at it. For example, pointing <TT>"Python Debugger: Remote
|
||||
Attach"</TT> to the following <TT>config.yaml</TT> will allow <B>F5</B> to establish a local
|
||||
connection to a DAP server running at port <TT>54321</TT>:</P>
|
||||
|
||||
<UL style="list-style-type: none">
|
||||
<LI>
|
||||
<PRE>
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Remote Attach",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "localhost",
|
||||
"port": 54321
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</PRE>
|
||||
</LI>
|
||||
</UL>
|
||||
</BODY>
|
||||
</HTML>
|
||||
885
Ghidra/Debug/Debugger-dap/src/main/java/dap/DapDebugAdapter.java
Normal file
885
Ghidra/Debug/Debugger-dap/src/main/java/dap/DapDebugAdapter.java
Normal file
@@ -0,0 +1,885 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dap;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.lsp4j.debug.*;
|
||||
import org.eclipse.lsp4j.debug.StackFrame;
|
||||
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
|
||||
import org.eclipse.lsp4j.debug.services.IDebugProtocolServer;
|
||||
|
||||
import docking.ActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.action.BasicAutoReadMemorySpec;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.debug.api.model.DebuggerSingleObjectPathActionContext;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.debug.api.target.Target.ActionEntry;
|
||||
import ghidra.debug.api.target.Target.ObjectArgumentPolicy;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.debug.api.tracermi.*;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.database.sourcemap.SourceFile;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.lang.RegisterValue;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.mem.MemoryAccessException;
|
||||
import ghidra.program.model.sourcemap.SourceFileManager;
|
||||
import ghidra.program.model.sourcemap.SourceMapEntry;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolIterator;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointKind.CommonSet;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointLocation;
|
||||
import ghidra.trace.model.guest.TracePlatform;
|
||||
import ghidra.trace.model.modules.TraceModule;
|
||||
import ghidra.trace.model.modules.TraceModuleManager;
|
||||
import ghidra.trace.model.stack.TraceStack;
|
||||
import ghidra.trace.model.stack.TraceStackFrame;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.trace.model.target.iface.TraceObjectInterface;
|
||||
import ghidra.trace.model.thread.*;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.NumericUtilities;
|
||||
|
||||
public class DapDebugAdapter implements IDebugProtocolServer {
|
||||
|
||||
private DapPlugin plugin;
|
||||
private DebuggerTraceManagerService manager;
|
||||
private TraceRmiService rmi;
|
||||
private DebuggerStaticMappingService mappings;
|
||||
|
||||
private Trace trace;
|
||||
private TraceRmiTarget target;
|
||||
private TraceRmiConnection conn;
|
||||
private RemoteMethodRegistry registry;
|
||||
|
||||
private Map<String, DataBreakpointInfo> dataBreakpoints = new HashMap<>();
|
||||
private AddressSpace space;
|
||||
private IDebugProtocolClient client;
|
||||
|
||||
public DapDebugAdapter(DapPlugin plugin, PluginTool tool) {
|
||||
this.plugin = plugin;
|
||||
this.manager = tool.getService(DebuggerTraceManagerService.class);
|
||||
this.rmi = tool.getService(TraceRmiService.class);
|
||||
this.mappings = tool.getService(DebuggerStaticMappingService.class);
|
||||
setCoordinates(manager.getCurrentFor(manager.getCurrentTrace()));
|
||||
}
|
||||
|
||||
public void setCoordinates(DebuggerCoordinates coordinates) {
|
||||
Trace traceFromCoord = coordinates.getTrace();
|
||||
if (traceFromCoord == null || traceFromCoord.equals(this.trace)) {
|
||||
return;
|
||||
}
|
||||
this.trace = traceFromCoord;
|
||||
this.target = (TraceRmiTarget) coordinates.getTarget();
|
||||
|
||||
space = trace.getBaseAddressFactory().getDefaultAddressSpace();
|
||||
for (TraceRmiConnection c : rmi.getAllConnections()) {
|
||||
if (c.getTargets().contains(target)) {
|
||||
this.conn = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.registry = conn.getMethods();
|
||||
plugin.addListener(trace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Capabilities> initialize(InitializeRequestArguments args) {
|
||||
Capabilities capabilities = new Capabilities();
|
||||
capabilities.setSupportsConfigurationDoneRequest(true);
|
||||
//capabilities.setSupportsConditionalBreakpoints(true);
|
||||
capabilities.setSupportsDataBreakpoints(true);
|
||||
capabilities.setSupportsDataBreakpointBytes(true);
|
||||
capabilities.setSupportsFunctionBreakpoints(true);
|
||||
capabilities.setSupportsInstructionBreakpoints(true);
|
||||
capabilities.setSupportsModulesRequest(true);
|
||||
capabilities.setSupportsReadMemoryRequest(true);
|
||||
capabilities.setSupportsWriteMemoryRequest(true);
|
||||
capabilities.setSupportsTerminateRequest(true);
|
||||
capabilities.setSupportsDisassembleRequest(true);
|
||||
client.initialized();
|
||||
return CompletableFuture.completedFuture(capabilities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> disconnect(DisconnectArguments args) {
|
||||
plugin.removeListener(trace);
|
||||
this.trace = null;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ContinueResponse> continue_(ContinueArguments args) {
|
||||
TraceThread thread = getThread(args.getThreadId());
|
||||
if (thread == null || thread.getObject() == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("No active thread context found."));
|
||||
}
|
||||
|
||||
ActionContext context =
|
||||
new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
|
||||
return invoke(ActionName.RESUME, context).thenApply(_ -> {
|
||||
ContinueResponse response = new ContinueResponse();
|
||||
response.setAllThreadsContinued(true);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> reverseContinue(ReverseContinueArguments args) {
|
||||
// TODO: We should standardize this
|
||||
RemoteMethod method0 = registry.get("go_back");
|
||||
RemoteMethod method1 = registry.get("resume_back");
|
||||
if (method0 == null && method1 == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new UnsupportedOperationException(
|
||||
"The connected backend does not support the 'reverse continue' method."));
|
||||
}
|
||||
|
||||
RemoteMethod method = method0 == null ? method1 : method0;
|
||||
return method.invokeAsync(new HashMap<>()).toCompletableFuture().thenApply(_ -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> next(NextArguments args) {
|
||||
TraceThread thread = getThread(args.getThreadId());
|
||||
if (thread == null || thread.getObject() == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("No active thread context found."));
|
||||
}
|
||||
|
||||
ActionContext context =
|
||||
new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
|
||||
return invoke(ActionName.STEP_OVER, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> stepIn(StepInArguments args) {
|
||||
TraceThread thread = getThread(args.getThreadId());
|
||||
if (thread == null || thread.getObject() == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("No active thread context found."));
|
||||
}
|
||||
|
||||
ActionContext context =
|
||||
new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
|
||||
return invoke(ActionName.STEP_INTO, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> stepOut(StepOutArguments args) {
|
||||
TraceThread thread = getThread(args.getThreadId());
|
||||
if (thread == null || thread.getObject() == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("No active thread context found."));
|
||||
}
|
||||
|
||||
ActionContext context =
|
||||
new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
|
||||
return invoke(ActionName.STEP_OUT, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> stepBack(StepBackArguments args) {
|
||||
TraceThread thread = getThread(args.getThreadId());
|
||||
if (thread == null || thread.getObject() == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("No active thread context found."));
|
||||
}
|
||||
|
||||
ActionContext context =
|
||||
new DebuggerSingleObjectPathActionContext(thread.getObject().getCanonicalPath());
|
||||
return invoke(ActionName.STEP_BACK, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> pause(PauseArguments args) {
|
||||
TraceProcess proc = getProcess();
|
||||
ActionContext context =
|
||||
new DebuggerSingleObjectPathActionContext(proc.getObject().getCanonicalPath());
|
||||
return invoke(ActionName.INTERRUPT, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SetInstructionBreakpointsResponse> setInstructionBreakpoints(
|
||||
SetInstructionBreakpointsArguments args) {
|
||||
return clearExistingBreakpointsOfType(false)
|
||||
.thenCompose(_ -> {
|
||||
List<CompletableFuture<Void>> futuresList = new ArrayList<>();
|
||||
InstructionBreakpoint[] breakpoints = args.getBreakpoints();
|
||||
for (InstructionBreakpoint bpt : breakpoints) {
|
||||
String ref = bpt.getInstructionReference();
|
||||
TraceBreakpointKindSet kind = kind(bpt.getMode());
|
||||
String condition = bpt.getCondition();
|
||||
try {
|
||||
Address address = space.getAddress(ref).add(bpt.getOffset());
|
||||
CompletableFuture<Void> future = target.placeBreakpointAsync(
|
||||
new AddressRangeImpl(address, 1), kind, condition, null);
|
||||
futuresList.add(future);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Drop malformed entries (excluded entries in the response
|
||||
// should be apparent to the client)
|
||||
}
|
||||
}
|
||||
return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
|
||||
})
|
||||
.thenApply(_ -> {
|
||||
List<Breakpoint> rbpts = getBreakpointList();
|
||||
SetInstructionBreakpointsResponse response =
|
||||
new SetInstructionBreakpointsResponse();
|
||||
response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
public record DataBreakpointInfo(String key, String mode, int size) {}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<DataBreakpointInfoResponse> dataBreakpointInfo(
|
||||
DataBreakpointInfoArguments args) {
|
||||
Boolean asAddress = args.getAsAddress();
|
||||
Integer size = args.getBytes();
|
||||
String mode = args.getMode();
|
||||
String name = args.getName();
|
||||
|
||||
if (asAddress == null || !asAddress) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
dataBreakpoints.put(name, new DataBreakpointInfo(name, mode, size != null ? size : 1));
|
||||
|
||||
DataBreakpointInfoResponse response = new DataBreakpointInfoResponse();
|
||||
response.setDescription(name);
|
||||
response.setDataId(name);
|
||||
response.setAccessTypes(DataBreakpointAccessType.values());
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SetBreakpointsResponse> setBreakpoints(SetBreakpointsArguments args) {
|
||||
Program program = plugin.getCurrentProgram();
|
||||
if (program == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("No active program source file trace map."));
|
||||
}
|
||||
return clearExistingBreakpointsOfType(false)
|
||||
.thenCompose(_ -> {
|
||||
List<CompletableFuture<Void>> futuresList = new ArrayList<>();
|
||||
convertAndPlaceBreakpoints(args, program, futuresList);
|
||||
return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
|
||||
})
|
||||
.thenApply(_ -> {
|
||||
List<Breakpoint> rbpts = getBreakpointList();
|
||||
SetBreakpointsResponse response = new SetBreakpointsResponse();
|
||||
response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private void convertAndPlaceBreakpoints(SetBreakpointsArguments args, Program program,
|
||||
List<CompletableFuture<Void>> futuresList) {
|
||||
SourceFileManager sourceManager = program.getSourceFileManager();
|
||||
SourceBreakpoint[] breakpoints = args.getBreakpoints();
|
||||
for (SourceBreakpoint bpt : breakpoints) {
|
||||
int ref = bpt.getLine();
|
||||
List<SourceFile> files = sourceManager.getMappedSourceFiles();
|
||||
for (SourceFile f : files) {
|
||||
List<SourceMapEntry> entries =
|
||||
sourceManager.getSourceMapEntries(f, ref);
|
||||
for (SourceMapEntry entry : entries) {
|
||||
convertAndPlaceBreakpoint(program, futuresList, bpt, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void convertAndPlaceBreakpoint(Program program,
|
||||
List<CompletableFuture<Void>> futuresList,
|
||||
SourceBreakpoint bpt, SourceMapEntry entry) {
|
||||
try {
|
||||
Address baseAddress = entry.getBaseAddress();
|
||||
Address targetAddress = dynamicForStatic(program, baseAddress);
|
||||
if (targetAddress != null) {
|
||||
CompletableFuture<Void> future = target.placeBreakpointAsync(
|
||||
new AddressRangeImpl(targetAddress, 1),
|
||||
kind(bpt.getMode()),
|
||||
bpt.getCondition(), null);
|
||||
futuresList.add(future);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Drop malformed entries
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SetDataBreakpointsResponse> setDataBreakpoints(
|
||||
SetDataBreakpointsArguments args) {
|
||||
return clearExistingBreakpointsOfType(true)
|
||||
.thenCompose(_ -> {
|
||||
List<CompletableFuture<Void>> futuresList = new ArrayList<>();
|
||||
DataBreakpoint[] breakpoints = args.getBreakpoints();
|
||||
for (DataBreakpoint bpt : breakpoints) {
|
||||
String ref = bpt.getDataId();
|
||||
DataBreakpointInfo info = dataBreakpoints.get(ref);
|
||||
DataBreakpointAccessType accessType = bpt.getAccessType();
|
||||
String condition = bpt.getCondition();
|
||||
|
||||
try {
|
||||
Address address = space.getAddress(ref);
|
||||
CompletableFuture<Void> future = target.placeBreakpointAsync(
|
||||
new AddressRangeImpl(address, info.size()), kinds(accessType),
|
||||
condition, null);
|
||||
futuresList.add(future);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Drop malformed entries
|
||||
}
|
||||
}
|
||||
return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
|
||||
})
|
||||
.thenApply(_ -> {
|
||||
List<Breakpoint> rbpts = getBreakpointList();
|
||||
SetDataBreakpointsResponse response = new SetDataBreakpointsResponse();
|
||||
response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SetFunctionBreakpointsResponse> setFunctionBreakpoints(
|
||||
SetFunctionBreakpointsArguments args) {
|
||||
return clearExistingBreakpointsOfType(false)
|
||||
.thenCompose(_ -> {
|
||||
List<CompletableFuture<Void>> futuresList = new ArrayList<>();
|
||||
FunctionBreakpoint[] breakpoints = args.getBreakpoints();
|
||||
for (FunctionBreakpoint bpt : breakpoints) {
|
||||
Function f = getFunctionByName(bpt.getName());
|
||||
if (f == null) {
|
||||
continue;
|
||||
}
|
||||
Address targetAddress = dynamicForStatic(f.getProgram(), f.getEntryPoint());
|
||||
if (targetAddress == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
CompletableFuture<Void> future = target.placeBreakpointAsync(
|
||||
new AddressRangeImpl(targetAddress, 1),
|
||||
CommonSet.SWX.kinds(),
|
||||
bpt.getCondition(), null);
|
||||
futuresList.add(future);
|
||||
break;
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Drop malformed entries
|
||||
}
|
||||
}
|
||||
return CompletableFuture.allOf(futuresList.toArray(new CompletableFuture[0]));
|
||||
})
|
||||
.thenApply(_ -> {
|
||||
List<Breakpoint> rbpts = getBreakpointList();
|
||||
SetFunctionBreakpointsResponse response =
|
||||
new SetFunctionBreakpointsResponse();
|
||||
response.setBreakpoints(rbpts.toArray(new Breakpoint[0]));
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> clearExistingBreakpointsOfType(boolean dataBreakpoints) {
|
||||
// TODO: Do we really want to do this?
|
||||
// Right now, it's not obvious that the DAP client knows about all breakpoints, i.e.
|
||||
// unclear whether it receives/filters client.breakpoint messages. Until it does, seems
|
||||
// unwise to clear breakpoints possible only know to Ghidra
|
||||
List<CompletableFuture<Void>> deletionFutures = new ArrayList<>();
|
||||
return CompletableFuture.allOf(deletionFutures.toArray(new CompletableFuture[0]));
|
||||
}
|
||||
|
||||
private Function getFunctionByName(String name) {
|
||||
Program program = plugin.getCurrentProgram();
|
||||
if (program == null || name == null) {
|
||||
return null;
|
||||
}
|
||||
SymbolIterator symbols = program.getSymbolTable().getSymbols(name);
|
||||
while (symbols.hasNext()) {
|
||||
Symbol sym = symbols.next();
|
||||
if (sym.getObject() instanceof Function func) {
|
||||
return func;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Address dynamicForStatic(Program program, Address staticAddress) {
|
||||
ProgramLocation dynamicLocation = mappings.getDynamicLocationFromStatic(
|
||||
trace.getProgramView(), new ProgramLocation(program, staticAddress));
|
||||
return dynamicLocation == null ? null : dynamicLocation.getAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StackTraceResponse> stackTrace(StackTraceArguments args) {
|
||||
Integer startFrame = args.getStartFrame();
|
||||
Integer levels = args.getLevels();
|
||||
int tid = args.getThreadId();
|
||||
// TOOO: StackFrameFormat format = args.getFormat();
|
||||
|
||||
TraceThread thread = getThread(tid);
|
||||
if (thread == null || thread.getObject() == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("No active thread context found."));
|
||||
}
|
||||
|
||||
long currentSnap = manager.getCurrentSnap();
|
||||
TraceStack stack = trace.getStackManager().getLatestStack(thread, currentSnap);
|
||||
if (stack == null) {
|
||||
return CompletableFuture.completedFuture(new StackTraceResponse());
|
||||
}
|
||||
|
||||
StackTraceResponse response = new StackTraceResponse();
|
||||
List<StackFrame> rframes = new ArrayList<>();
|
||||
int start = (startFrame != null) ? startFrame : 0;
|
||||
int maxLevels = (levels != null && levels > 0) ? levels : stack.getDepth(currentSnap);
|
||||
|
||||
for (TraceStackFrame f : stack.getFrames(currentSnap)) {
|
||||
if (f.getLevel() >= start && rframes.size() < maxLevels) {
|
||||
StackFrame rf = new StackFrame();
|
||||
rf.setId(f.getLevel());
|
||||
|
||||
String desc = getDisplay(f.getObject());
|
||||
if (desc != null) {
|
||||
rf.setName(desc);
|
||||
}
|
||||
|
||||
rf.setInstructionPointerReference(
|
||||
f.getProgramCounter(currentSnap).toString());
|
||||
rframes.add(rf);
|
||||
}
|
||||
}
|
||||
response.setStackFrames(rframes.toArray(new StackFrame[0]));
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ThreadsResponse> threads() {
|
||||
if (trace == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
TraceThreadManager threadManager = trace.getThreadManager();
|
||||
Collection<? extends TraceThread> threads =
|
||||
threadManager.getLiveThreads(manager.getCurrentSnap());
|
||||
|
||||
ThreadsResponse response = new ThreadsResponse();
|
||||
List<org.eclipse.lsp4j.debug.Thread> rthreads = new ArrayList<>();
|
||||
|
||||
for (TraceThread t : threads) {
|
||||
org.eclipse.lsp4j.debug.Thread rt = new org.eclipse.lsp4j.debug.Thread();
|
||||
rt.setId(getThreadId(t));
|
||||
rt.setName(t.getName(manager.getCurrentSnap()));
|
||||
rthreads.add(rt);
|
||||
}
|
||||
response.setThreads(rthreads.toArray(new org.eclipse.lsp4j.debug.Thread[0]));
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ModulesResponse> modules(ModulesArguments args) {
|
||||
if (trace == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
TraceModuleManager modManager = trace.getModuleManager();
|
||||
Collection<? extends TraceModule> modules =
|
||||
modManager.getLoadedModules(manager.getCurrentSnap());
|
||||
|
||||
ModulesResponse response = new ModulesResponse();
|
||||
List<org.eclipse.lsp4j.debug.Module> rmods = new ArrayList<>();
|
||||
|
||||
for (TraceModule m : modules) {
|
||||
org.eclipse.lsp4j.debug.Module rm = new org.eclipse.lsp4j.debug.Module();
|
||||
rm.setId((int) m.getObject().getKey());
|
||||
rm.setName(m.getName(manager.getCurrentSnap()));
|
||||
rm.setAddressRange(m.getRange(manager.getCurrentSnap()).toString());
|
||||
rmods.add(rm);
|
||||
}
|
||||
response.setModules(rmods.toArray(new org.eclipse.lsp4j.debug.Module[0]));
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<EvaluateResponse> evaluate(EvaluateArguments args) {
|
||||
return target.executeAsync(args.getExpression(), true)
|
||||
.toCompletableFuture()
|
||||
.thenApply(result -> {
|
||||
EvaluateResponse response = new EvaluateResponse();
|
||||
response.setResult(result != null ? result.toString() : "null");
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> terminate(TerminateArguments args) {
|
||||
return target.disconnectAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ReadMemoryResponse> readMemory(ReadMemoryArguments args) {
|
||||
Address baseAddress = getAddress(args.getMemoryReference());
|
||||
int requestedCount = args.getCount();
|
||||
long offsetAdjustment = (args.getOffset() != null) ? args.getOffset() : 0L;
|
||||
|
||||
final Address address;
|
||||
try {
|
||||
address = baseAddress.add(offsetAdjustment);
|
||||
}
|
||||
catch (AddressOutOfBoundsException e) {
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
|
||||
AddressSetView view;
|
||||
try {
|
||||
view = new AddressSet(address, address.add(requestedCount - 1));
|
||||
}
|
||||
catch (AddressOutOfBoundsException e) {
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
|
||||
return BasicAutoReadMemorySpec.VISIBLE
|
||||
.readMemory(plugin.getTool(), manager.getCurrentFor(trace), view)
|
||||
.thenApply(_ -> {
|
||||
ByteBuffer buf = ByteBuffer.allocate(args.getCount());
|
||||
int bytesRead =
|
||||
trace.getMemoryManager()
|
||||
.getViewBytes(manager.getCurrentSnap(), address, buf);
|
||||
buf.flip();
|
||||
|
||||
ReadMemoryResponse response = new ReadMemoryResponse();
|
||||
response.setAddress(address.toString());
|
||||
String data =
|
||||
new String(Base64.getEncoder().encode(buf).limit(bytesRead).array());
|
||||
response.setData(data);
|
||||
response.setUnreadableBytes(args.getCount() - bytesRead);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<WriteMemoryResponse> writeMemory(WriteMemoryArguments args) {
|
||||
Address baseAddress = getAddress(args.getMemoryReference());
|
||||
long offsetAdjustment = (args.getOffset() != null) ? args.getOffset() : 0L;
|
||||
|
||||
final Address address;
|
||||
try {
|
||||
address = baseAddress.add(offsetAdjustment);
|
||||
}
|
||||
catch (AddressOutOfBoundsException e) {
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
|
||||
byte[] bytes = Base64.getDecoder().decode(args.getData());
|
||||
return target.writeMemoryAsync(address, bytes).thenApply(_ -> {
|
||||
WriteMemoryResponse response = new WriteMemoryResponse();
|
||||
response.setOffset(args.getOffset());
|
||||
response.setBytesWritten(bytes.length);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<LoadedSourcesResponse> loadedSources(LoadedSourcesArguments args) {
|
||||
SourceFileManager sourceManager = plugin.getCurrentProgram().getSourceFileManager();
|
||||
List<SourceFile> mappedSourceFiles = sourceManager.getMappedSourceFiles();
|
||||
LoadedSourcesResponse response = new LoadedSourcesResponse();
|
||||
List<Source> sources = new ArrayList<>();
|
||||
|
||||
for (SourceFile f : mappedSourceFiles) {
|
||||
Source src = new Source();
|
||||
src.setName(f.getFilename());
|
||||
src.setPath(f.getPath());
|
||||
sources.add(src);
|
||||
}
|
||||
response.setSources(sources.toArray(new Source[0]));
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<DisassembleResponse> disassemble(DisassembleArguments args) {
|
||||
Listing listing = trace.getProgramView().getListing();
|
||||
String memoryReference = args.getMemoryReference();
|
||||
Address start = getAddress(memoryReference);
|
||||
int count = args.getInstructionCount();
|
||||
InstructionIterator iter = listing.getInstructions(start, true);
|
||||
List<Instruction> instructions = new ArrayList<>();
|
||||
while (iter.hasNext() && instructions.size() < count) {
|
||||
instructions.add(iter.next());
|
||||
}
|
||||
|
||||
DisassembleResponse response = new DisassembleResponse();
|
||||
DisassembledInstruction[] rinstructions = new DisassembledInstruction[instructions.size()];
|
||||
int n = 0;
|
||||
for (Instruction i : instructions) {
|
||||
DisassembledInstruction ri = new DisassembledInstruction();
|
||||
ri.setAddress(i.getAddress().toString());
|
||||
ri.setInstruction(i.toString());
|
||||
try {
|
||||
String rep = NumericUtilities.convertBytesToString(i.getBytes(), ":");
|
||||
ri.setInstructionBytes(rep);
|
||||
}
|
||||
catch (MemoryAccessException e) {
|
||||
Msg.error(this, e.getMessage());
|
||||
}
|
||||
rinstructions[n++] = ri;
|
||||
}
|
||||
response.setInstructions(rinstructions);
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> goto_(GotoArguments args) {
|
||||
TracePlatform platform = trace.getPlatformManager().getHostPlatform();
|
||||
TraceThread thread = getThread(args.getThreadId());
|
||||
if (thread == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("Thread missing during context update."));
|
||||
}
|
||||
Register pc = trace.getBaseLanguage().getProgramCounter();
|
||||
RegisterValue rval = new RegisterValue(pc, BigInteger.valueOf(args.getTargetId()));
|
||||
return target.writeRegisterAsync(platform, thread, 0, rval);
|
||||
}
|
||||
|
||||
// NOT CURRENTLY IMPLEMENTED
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> launch(Map<String, Object> args) {
|
||||
RemoteMethod method = registry.get("launch");
|
||||
if (method == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new UnsupportedOperationException(
|
||||
"The connected backend does not support the 'launch' method."));
|
||||
}
|
||||
|
||||
return method.invokeAsync(args).toCompletableFuture().thenApply(_ -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> attach(Map<String, Object> args) {
|
||||
RemoteMethod method = registry.get("attach");
|
||||
if (method == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
return method.invokeAsync(args).toCompletableFuture().thenApply(_ -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> restart(RestartArguments args) {
|
||||
RemoteMethod method = registry.get("launch");
|
||||
if (method == null) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new UnsupportedOperationException(
|
||||
"The connected backend does not support the 'restart' method."));
|
||||
}
|
||||
|
||||
return method.invokeAsync(new HashMap<>()).toCompletableFuture().thenApply(_ -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ExceptionInfoResponse> exceptionInfo(ExceptionInfoArguments args) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SetExceptionBreakpointsResponse> setExceptionBreakpoints(
|
||||
SetExceptionBreakpointsArguments args) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
// private methods
|
||||
|
||||
private CompletableFuture<Void> invoke(ActionName action, ActionContext context) {
|
||||
ActionEntry entry = target
|
||||
.collectActions(action, context,
|
||||
ObjectArgumentPolicy.CURRENT_AND_RELATED)
|
||||
.values()
|
||||
.stream()
|
||||
.filter(e -> !e.requiresPrompt())
|
||||
.sorted(Comparator.comparing(e -> -e.specificity()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (entry != null) {
|
||||
return entry.invokeAsyncWithoutTimeout(false).thenApply(_ -> null);
|
||||
}
|
||||
return CompletableFuture.failedFuture(new RuntimeException("No such method"));
|
||||
}
|
||||
|
||||
public int getThreadId(TraceThread thread) {
|
||||
var tidAttr = thread.getObject().getValue(manager.getCurrentSnap(), TraceThread.KEY_TID);
|
||||
if (tidAttr == null || tidAttr.getValue() == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
Long tid = (Long) tidAttr.getValue();
|
||||
return tid.intValue();
|
||||
}
|
||||
|
||||
TraceProcess getProcess() {
|
||||
TraceThread currentThread = manager.getCurrentThread();
|
||||
if (currentThread == null) {
|
||||
return null;
|
||||
}
|
||||
return currentThread.getObject()
|
||||
.queryAncestorsInterface(Lifespan.ALL, TraceProcess.class)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private TraceThread getThread(int threadId) {
|
||||
for (TraceThread t : trace.getThreadManager().getAllThreads()) {
|
||||
int tid = getThreadId(t);
|
||||
if (tid == threadId) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return manager.getCurrentThread();
|
||||
}
|
||||
|
||||
private TraceBreakpointKindSet kind(String mode) {
|
||||
return mode.equals("hardware") ? CommonSet.HWX.kinds() : CommonSet.SWX.kinds();
|
||||
}
|
||||
|
||||
private TraceBreakpointKindSet kinds(DataBreakpointAccessType accessType) {
|
||||
return switch (accessType) {
|
||||
case DataBreakpointAccessType.READ -> CommonSet.READ.kinds();
|
||||
case DataBreakpointAccessType.WRITE -> CommonSet.WRITE.kinds();
|
||||
case DataBreakpointAccessType.READ_WRITE -> CommonSet.ACCESS.kinds();
|
||||
case null -> CommonSet.SWX.kinds();
|
||||
default -> CommonSet.SWX.kinds();
|
||||
};
|
||||
}
|
||||
|
||||
List<Breakpoint> getBreakpointList() {
|
||||
List<Breakpoint> rbpts = new ArrayList<>();
|
||||
if (trace == null) {
|
||||
return rbpts;
|
||||
}
|
||||
|
||||
Collection<? extends TraceBreakpointLocation> bpts =
|
||||
trace.getBreakpointManager().getAllBreakpointLocations();
|
||||
for (TraceBreakpointLocation loc : bpts) {
|
||||
AddressRange range = loc.getRange(manager.getCurrentSnap());
|
||||
if (range == null) {
|
||||
continue;
|
||||
}
|
||||
String key = loc.getName(manager.getCurrentSnap());
|
||||
key = key.substring(1, key.indexOf("]"));
|
||||
|
||||
Breakpoint rbpt = new Breakpoint();
|
||||
rbpt.setId(Integer.parseInt(key));
|
||||
rbpt.setInstructionReference(range.getMinAddress().toString(false));
|
||||
//rbpt.setOffset(0);
|
||||
rbpt.setVerified(true);
|
||||
rbpts.add(rbpt);
|
||||
}
|
||||
return rbpts;
|
||||
}
|
||||
|
||||
private Address getAddress(String offset) {
|
||||
if (offset == null || space == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return space.getAddress(offset);
|
||||
}
|
||||
catch (AddressFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getDisplay(TraceObject obj) {
|
||||
if (obj == null) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
var displayAttr = obj.getValue(manager.getCurrentSnap(), TraceObjectInterface.KEY_DISPLAY);
|
||||
if (displayAttr == null || displayAttr.getValue() == null) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
Object display = displayAttr.getValue();
|
||||
if (display instanceof String desc) {
|
||||
return desc;
|
||||
}
|
||||
return display.toString();
|
||||
}
|
||||
|
||||
// private int getLine(Address address) {
|
||||
// SourceFileManager sourceManager = plugin.getCurrentProgram().getSourceFileManager();
|
||||
// List<SourceMapEntry> sourceMapEntries = sourceManager.getSourceMapEntries(address);
|
||||
// for (SourceMapEntry entry : sourceMapEntries) {
|
||||
// return entry.getLineNumber();
|
||||
// }
|
||||
// return -1;
|
||||
// }
|
||||
//
|
||||
// private String getFile(Address address) {
|
||||
// SourceFileManager sourceManager = plugin.getCurrentProgram().getSourceFileManager();
|
||||
// List<SourceMapEntry> sourceMapEntries = sourceManager.getSourceMapEntries(address);
|
||||
// for (SourceMapEntry entry : sourceMapEntries) {
|
||||
// return entry.getSourceFile().getFilename();
|
||||
// }
|
||||
// return "";
|
||||
// }
|
||||
|
||||
public void setClient(IDebugProtocolClient client) {
|
||||
this.client = client;
|
||||
|
||||
StoppedEventArguments args = new StoppedEventArguments();
|
||||
args.setAllThreadsStopped(true);
|
||||
args.setReason("initialization");
|
||||
client.stopped(args);
|
||||
}
|
||||
|
||||
public IDebugProtocolClient getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
// UNSUPPORTED but called enough we'd rather not throw an exception
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> configurationDone(ConfigurationDoneArguments args) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ScopesResponse> scopes(ScopesArguments args) {
|
||||
ScopesResponse response = new ScopesResponse();
|
||||
return CompletableFuture.completedFuture(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<VariablesResponse> variables(VariablesArguments args) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
377
Ghidra/Debug/Debugger-dap/src/main/java/dap/DapPlugin.java
Normal file
377
Ghidra/Debug/Debugger-dap/src/main/java/dap/DapPlugin.java
Normal file
@@ -0,0 +1,377 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dap;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.*;
|
||||
|
||||
import org.eclipse.lsp4j.debug.*;
|
||||
import org.eclipse.lsp4j.debug.Module;
|
||||
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
|
||||
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.CorePluginPackage;
|
||||
import ghidra.app.events.ProgramActivatedPluginEvent;
|
||||
import ghidra.app.events.ProgramClosedPluginEvent;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.ProgramPlugin;
|
||||
import ghidra.app.plugin.core.debug.event.*;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
import ghidra.debug.api.target.Target;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.framework.model.DomainObjectChangeRecord;
|
||||
import ghidra.framework.model.DomainObjectEvent;
|
||||
import ghidra.framework.options.OptionType;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.program.database.SpecExtension;
|
||||
import ghidra.program.model.address.AddressRange;
|
||||
import ghidra.program.model.address.AddressSpace;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.breakpoint.TraceBreakpointLocation;
|
||||
import ghidra.trace.model.modules.TraceModule;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.trace.model.thread.TraceProcess;
|
||||
import ghidra.trace.model.thread.TraceThread;
|
||||
import ghidra.trace.model.time.TraceSnapshot;
|
||||
import ghidra.trace.util.TraceEvents;
|
||||
import ghidra.util.HelpLocation;
|
||||
|
||||
//@formatter:off
|
||||
@PluginInfo(
|
||||
status = PluginStatus.UNSTABLE,
|
||||
packageName = CorePluginPackage.NAME,
|
||||
category = PluginCategoryNames.ANALYSIS,
|
||||
shortDescription = "DapDebuggerSupport",
|
||||
description = "Plugin implements server-side DAP access to the debugger",
|
||||
eventsConsumed = {
|
||||
ProgramActivatedPluginEvent.class, ProgramClosedPluginEvent.class,
|
||||
TraceActivatedPluginEvent.class, TraceOpenedPluginEvent.class,
|
||||
TraceClosedPluginEvent.class
|
||||
})
|
||||
//@formatter:on
|
||||
|
||||
public class DapPlugin extends ProgramPlugin {
|
||||
|
||||
public final static String HELP_LOCATION = "dap";
|
||||
private static final String OPTIONS_TITLE = "DAP Server";
|
||||
|
||||
private DapServer server;
|
||||
private final Map<Trace, ListenerForChanges> listeners = new HashMap<>();
|
||||
private IDebugProtocolClient client;
|
||||
|
||||
public DapPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
createActions();
|
||||
ToolOptions opt = tool.getOptions(OPTIONS_TITLE);
|
||||
opt.registerOption("Port", OptionType.INT_TYPE, 54321, null, "Port to connect to");
|
||||
opt.registerOption("IP address", OptionType.STRING_TYPE,
|
||||
InetAddress.getLoopbackAddress().getHostAddress(), null,
|
||||
"IP address to connect to");
|
||||
}
|
||||
|
||||
private void createActions() {
|
||||
|
||||
new ActionBuilder("Start DAP Server", getName())
|
||||
.menuPath("Debugger", "DAP", "Start server")
|
||||
.helpLocation(new HelpLocation(getName(),HELP_LOCATION))
|
||||
.enabledWhen(_ -> currentProgram != null)
|
||||
.onAction(_ -> doStartServer())
|
||||
.buildAndInstall(tool);
|
||||
|
||||
|
||||
new ActionBuilder("Stop DAP Server", getName())
|
||||
.menuPath("Debugger", "DAP", "Stop server")
|
||||
.helpLocation(new HelpLocation(getName(), HELP_LOCATION))
|
||||
.enabledWhen(_ -> currentProgram != null)
|
||||
.onAction(_ -> doStopServer())
|
||||
.buildAndInstall(tool);
|
||||
}
|
||||
|
||||
private void doStartServer() {
|
||||
DapDebugAdapter adapter = new DapDebugAdapter(DapPlugin.this, tool);
|
||||
server = new DapServer(DapPlugin.this, adapter);
|
||||
server.startServer();
|
||||
}
|
||||
|
||||
private void doStopServer() {
|
||||
if (server != null) {
|
||||
server.stopServer();
|
||||
server = null;
|
||||
}
|
||||
|
||||
var traceIterator = listeners.keySet().iterator();
|
||||
while (traceIterator.hasNext()) {
|
||||
Trace trace = traceIterator.next();
|
||||
ListenerForChanges listener = listeners.get(trace);
|
||||
if (listener != null) {
|
||||
trace.removeListener(listener);
|
||||
}
|
||||
traceIterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processEvent(PluginEvent event) {
|
||||
super.processEvent(event);
|
||||
|
||||
if (event instanceof ProgramClosedPluginEvent closedProgEvent) {
|
||||
Program program = closedProgEvent.getProgram();
|
||||
if (currentProgram != null && currentProgram.equals(program)) {
|
||||
currentProgram = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event instanceof ProgramActivatedPluginEvent activeProgEvent) {
|
||||
currentProgram = activeProgEvent.getActiveProgram();
|
||||
if (currentProgram != null) {
|
||||
SpecExtension.registerOptions(currentProgram);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event instanceof TraceClosedPluginEvent closedEvent) {
|
||||
removeListener(closedEvent.getTrace());
|
||||
return;
|
||||
}
|
||||
|
||||
if (event instanceof TraceActivatedPluginEvent activeTraceEvent) {
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
server.updateAdapter(activeTraceEvent.getActiveCoordinates());
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void addListener(Trace trace) {
|
||||
if (trace == null || listeners.containsKey(trace)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ListenerForChanges listener = new ListenerForChanges(trace);
|
||||
trace.addListener(listener);
|
||||
listeners.put(trace, listener);
|
||||
}
|
||||
|
||||
void removeListener(Trace trace) {
|
||||
if (trace == null) {
|
||||
return;
|
||||
}
|
||||
ListenerForChanges listener = listeners.remove(trace);
|
||||
if (listener != null) {
|
||||
trace.removeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
protected class ListenerForChanges extends TraceDomainObjectListener {
|
||||
|
||||
private final Trace trace;
|
||||
private DebuggerTraceManagerService manager;
|
||||
|
||||
public ListenerForChanges(Trace trace) {
|
||||
this.trace = trace;
|
||||
this.manager = tool.getService(DebuggerTraceManagerService.class);
|
||||
listenForUntyped(DomainObjectEvent.RESTORED, this::objectRestored);
|
||||
listenForUntyped(DomainObjectEvent.CLOSED, this::objectClosed);
|
||||
listenFor(TraceEvents.VALUE_CREATED, this::valueModified);
|
||||
listenFor(TraceEvents.VALUE_DELETED, this::valueModified);
|
||||
listenFor(TraceEvents.VALUE_LIFESPAN_CHANGED, this::valueModified);
|
||||
listenFor(TraceEvents.THREAD_ADDED, this::threadChanged);
|
||||
listenFor(TraceEvents.THREAD_DELETED, this::threadChanged);
|
||||
listenFor(TraceEvents.THREAD_LIFESPAN_CHANGED, this::threadChanged);
|
||||
listenFor(TraceEvents.BREAKPOINT_ADDED, this::breakpointChanged);
|
||||
listenFor(TraceEvents.BREAKPOINT_CHANGED, this::breakpointChanged);
|
||||
listenFor(TraceEvents.BREAKPOINT_DELETED, this::breakpointChanged);
|
||||
listenFor(TraceEvents.BREAKPOINT_LIFESPAN_CHANGED, this::breakpointChanged);
|
||||
listenFor(TraceEvents.MODULE_ADDED, this::moduleAdded);
|
||||
listenFor(TraceEvents.MODULE_CHANGED, this::moduleChanged);
|
||||
listenFor(TraceEvents.MODULE_DELETED, this::moduleDeleted);
|
||||
listenFor(TraceEvents.MODULE_LIFESPAN_CHANGED, this::moduleChanged);
|
||||
listenFor(TraceEvents.BYTES_CHANGED, this::bytesChanged);
|
||||
listenFor(TraceEvents.PLATFORM_DELETED, this::objectClosed);
|
||||
}
|
||||
|
||||
private void objectRestored(DomainObjectChangeRecord record) {
|
||||
contextChanged();
|
||||
}
|
||||
|
||||
private void objectClosed(DomainObjectChangeRecord record) {
|
||||
ExitedEventArguments args = new ExitedEventArguments();
|
||||
TraceProcess process = server.getAdapter().getProcess();
|
||||
|
||||
var exitAttr = process.getObject().getAttribute(manager.getCurrentSnap(), "Exit Code");
|
||||
if (exitAttr != null && exitAttr.getValue() != null) {
|
||||
Long rc = (Long) exitAttr.getValue();
|
||||
args.setExitCode(rc.intValue());
|
||||
}
|
||||
else {
|
||||
args.setExitCode(-1);
|
||||
}
|
||||
client.exited(args);
|
||||
}
|
||||
|
||||
protected void valueModified(TraceObjectValue value) {
|
||||
contextChanged();
|
||||
}
|
||||
|
||||
private void threadChanged(TraceThread thread) {
|
||||
contextChanged();
|
||||
}
|
||||
|
||||
private void breakpointChanged(TraceBreakpointLocation location) {
|
||||
contextChanged();
|
||||
}
|
||||
|
||||
private void moduleAdded(TraceModule module) {
|
||||
moduleUpdated(module, ModuleEventArgumentsReason.NEW);
|
||||
}
|
||||
|
||||
private void moduleDeleted(TraceModule module) {
|
||||
moduleUpdated(module, ModuleEventArgumentsReason.REMOVED);
|
||||
}
|
||||
|
||||
private void moduleChanged(TraceModule module) {
|
||||
moduleUpdated(module, ModuleEventArgumentsReason.CHANGED);
|
||||
}
|
||||
|
||||
private void bytesChanged(AddressSpace space, TraceAddressSnapRange range) {
|
||||
MemoryEventArguments args = new MemoryEventArguments();
|
||||
AddressRange addrRange = range.getRange();
|
||||
String ref = addrRange.getMinAddress().toString(false);
|
||||
args.setCount((int) addrRange.getLength());
|
||||
args.setOffset(0);
|
||||
args.setMemoryReference(ref);
|
||||
client.memory(args);
|
||||
}
|
||||
|
||||
private void contextChanged() {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
DebuggerCoordinates coordinates = manager.getCurrentFor(trace);
|
||||
Target target = coordinates.getTarget();
|
||||
long currentSnap = manager.getCurrentSnap();
|
||||
|
||||
for (TraceThread t : trace.getThreadManager().getLiveThreads(currentSnap)) {
|
||||
int tid = server.getAdapter().getThreadId(t);
|
||||
TraceExecutionState state = target.getThreadExecutionState(t);
|
||||
switch (state) {
|
||||
case STOPPED -> {
|
||||
StoppedEventArguments args = new StoppedEventArguments();
|
||||
args.setThreadId(tid);
|
||||
TraceSnapshot snapshot =
|
||||
trace.getTimeManager().getSnapshot(currentSnap, false);
|
||||
args.setDescription(
|
||||
snapshot == null ? "STOPPED" : snapshot.getDescription());
|
||||
args.setReason(ThreadEventArgumentsReason.STARTED);
|
||||
client.stopped(args);
|
||||
}
|
||||
case RUNNING -> {
|
||||
ContinuedEventArguments args = new ContinuedEventArguments();
|
||||
args.setThreadId(tid);
|
||||
client.continued(args);
|
||||
}
|
||||
case ALIVE, INACTIVE -> {
|
||||
ThreadEventArguments args = new ThreadEventArguments();
|
||||
args.setThreadId(tid);
|
||||
args.setReason(ThreadEventArgumentsReason.STARTED);
|
||||
client.thread(args);
|
||||
}
|
||||
case TERMINATED -> {
|
||||
ThreadEventArguments args = new ThreadEventArguments();
|
||||
args.setThreadId(tid);
|
||||
args.setReason(ThreadEventArgumentsReason.EXITED);
|
||||
client.thread(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Yes, this is a lot of probably-mostly-extranous work, but...
|
||||
// right now, the alternatives seem grim.
|
||||
List<Breakpoint> breakpointList = server.getAdapter().getBreakpointList();
|
||||
for (Breakpoint bpt : breakpointList) {
|
||||
BreakpointEventArguments args = new BreakpointEventArguments();
|
||||
args.setBreakpoint(bpt);
|
||||
client.breakpoint(args);
|
||||
}
|
||||
|
||||
for (TraceModule mod : trace.getModuleManager()
|
||||
.getLoadedModules(manager.getCurrentSnap())) {
|
||||
moduleUpdated(mod, ModuleEventArgumentsReason.CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
private void moduleUpdated(TraceModule module, ModuleEventArgumentsReason reason) {
|
||||
ModuleEventArguments args = new ModuleEventArguments();
|
||||
Module rmod = new Module();
|
||||
long currentSnap = manager.getCurrentSnap();
|
||||
|
||||
rmod.setId((int) module.getObject().getKey());
|
||||
rmod.setName(module.getName(currentSnap));
|
||||
|
||||
var range = module.getRange(currentSnap);
|
||||
rmod.setAddressRange(range != null ? range.toString() : "Unknown");
|
||||
|
||||
args.setModule(rmod);
|
||||
args.setReason(reason);
|
||||
client.module(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setClient(IDebugProtocolClient client) {
|
||||
this.client = client;
|
||||
DebuggerTraceManagerService manager =
|
||||
tool.getService(DebuggerTraceManagerService.class);
|
||||
if (manager != null) {
|
||||
Trace currentTrace = manager.getCurrentTrace();
|
||||
if (currentTrace != null) {
|
||||
addListener(currentTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IDebugProtocolClient getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public Integer getPort() {
|
||||
ToolOptions opt = tool.getOptions(OPTIONS_TITLE);
|
||||
return opt.getInt("Port", 54321);
|
||||
}
|
||||
|
||||
public InetAddress getInetAddress() {
|
||||
ToolOptions opt = tool.getOptions(OPTIONS_TITLE);
|
||||
String addr =
|
||||
opt.getString("IP address", InetAddress.getLoopbackAddress().getHostAddress());
|
||||
if (addr == null || addr.isEmpty() || addr.equals("localhost")) {
|
||||
return InetAddress.getLoopbackAddress();
|
||||
}
|
||||
try {
|
||||
return InetAddress.getByName(addr);
|
||||
}
|
||||
catch (UnknownHostException e) {
|
||||
return InetAddress.getLoopbackAddress();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
155
Ghidra/Debug/Debugger-dap/src/main/java/dap/DapServer.java
Normal file
155
Ghidra/Debug/Debugger-dap/src/main/java/dap/DapServer.java
Normal file
@@ -0,0 +1,155 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dap;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
|
||||
import org.eclipse.lsp4j.jsonrpc.Launcher;
|
||||
import org.eclipse.lsp4j.jsonrpc.debug.DebugLauncher;
|
||||
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class DapServer implements Runnable {
|
||||
|
||||
private DapPlugin plugin;
|
||||
private DapDebugAdapter adapter;
|
||||
private InetAddress address;
|
||||
private int port;
|
||||
|
||||
private ServerSocket server;
|
||||
private Thread listenerThread;
|
||||
private ExecutorService sessionThreadPool = Executors.newCachedThreadPool();
|
||||
private volatile boolean running = false;
|
||||
|
||||
public DapServer(DapPlugin plugin, DapDebugAdapter adapter) {
|
||||
this.plugin = plugin;
|
||||
this.adapter = adapter;
|
||||
this.address = plugin.getInetAddress();
|
||||
this.port = plugin.getPort();
|
||||
}
|
||||
|
||||
public void startServer() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (server != null && !server.isClosed()) {
|
||||
server.close();
|
||||
}
|
||||
server = new ServerSocket(port, 50, address);
|
||||
this.running = true;
|
||||
|
||||
this.listenerThread = new Thread(this, "DAP-Server-Listener");
|
||||
this.listenerThread.start();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("Could not start server" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void stopServer() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
running = false;
|
||||
|
||||
if (server != null) {
|
||||
try {
|
||||
if (!server.isClosed()) {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.warn(this, "Error closing server socket: " + e.getMessage());
|
||||
}
|
||||
server = null;
|
||||
}
|
||||
|
||||
if (listenerThread != null) {
|
||||
listenerThread.interrupt();
|
||||
listenerThread = null;
|
||||
}
|
||||
|
||||
if (sessionThreadPool != null) {
|
||||
sessionThreadPool.shutdownNow();
|
||||
try {
|
||||
if (!sessionThreadPool.awaitTermination(2, TimeUnit.SECONDS)) {
|
||||
Msg.warn(this, "Session thread pool did not terminate cleanly.");
|
||||
}
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
sessionThreadPool = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ServerSocket localServer = this.server;
|
||||
while (running && localServer != null && !localServer.isClosed()) {
|
||||
try {
|
||||
Socket clientSocket = localServer.accept();
|
||||
if (running && sessionThreadPool != null) {
|
||||
sessionThreadPool.submit(() -> handleClientSession(clientSocket));
|
||||
}
|
||||
else {
|
||||
clientSocket.close(); // Safeguard against trailing boundary race connections
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (running) {
|
||||
Msg.error(this, "Error accepting connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClientSession(Socket socket) {
|
||||
try (Socket s = socket;
|
||||
InputStream in = s.getInputStream();
|
||||
OutputStream out = s.getOutputStream()) {
|
||||
|
||||
Launcher<IDebugProtocolClient> launcher = DebugLauncher.createLauncher(
|
||||
adapter,
|
||||
IDebugProtocolClient.class,
|
||||
in,
|
||||
out);
|
||||
IDebugProtocolClient client = launcher.getRemoteProxy();
|
||||
|
||||
adapter.setClient(client);
|
||||
plugin.setClient(client);
|
||||
launcher.startListening().get();
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Session error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public DapDebugAdapter getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
public void updateAdapter(DebuggerCoordinates debuggerCoordinates) {
|
||||
adapter.setCoordinates(debuggerCoordinates);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user