Skip to content

Commit

Permalink
GP-869: Implement ConPTY-based GDB pty
Browse files Browse the repository at this point in the history
  • Loading branch information
d-millar authored and nsadeveloper789 committed Jan 26, 2022
1 parent 073c726 commit f2a34dc
Show file tree
Hide file tree
Showing 33 changed files with 1,922 additions and 208 deletions.
2 changes: 2 additions & 0 deletions Ghidra/Debug/Debugger-agent-gdb/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ dependencies {
api project(':Debugger-gadp')
api project(':Python')
api 'com.jcraft:jsch:0.1.55'
api "net.java.dev.jna:jna:5.4.0"
api "net.java.dev.jna:jna-platform:5.4.0"

testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import agent.gdb.manager.GdbManager;
import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.linux.LinuxPtyFactory;
import agent.gdb.pty.PtyFactory;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
Expand Down Expand Up @@ -50,9 +50,8 @@ public class GdbInJvmDebuggerModelFactory implements DebuggerModelFactory {

@Override
public CompletableFuture<? extends DebuggerObjectModel> build() {
// TODO: Choose Linux or Windows pty based on host OS
List<String> gdbCmdLine = ShellUtils.parseArgs(gdbCmd);
GdbModelImpl model = new GdbModelImpl(new LinuxPtyFactory());
GdbModelImpl model = new GdbModelImpl(PtyFactory.local());
return model
.startGDB(existing ? null : gdbCmdLine.get(0),
gdbCmdLine.subList(1, gdbCmdLine.size()).toArray(String[]::new))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import agent.gdb.gadp.GdbGadpServer;
import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.linux.LinuxPtyFactory;
import agent.gdb.pty.PtyFactory;
import ghidra.dbg.gadp.server.AbstractGadpServer;

public class GdbGadpServerImpl implements GdbGadpServer {
Expand All @@ -36,8 +36,7 @@ public GadpSide(GdbModelImpl model, SocketAddress addr) throws IOException {

public GdbGadpServerImpl(SocketAddress addr) throws IOException {
super();
// TODO: Select Linux or Windows factory based on host OS
this.model = new GdbModelImpl(new LinuxPtyFactory());
this.model = new GdbModelImpl(PtyFactory.local());
this.server = new GadpSide(model, addr);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import agent.gdb.manager.impl.GdbManagerImpl;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPty;
import agent.gdb.pty.linux.LinuxPtyFactory;

/**
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal
Expand Down Expand Up @@ -86,8 +85,7 @@ public String toString() {
*/
public static void main(String[] args)
throws InterruptedException, ExecutionException, IOException {
// TODO: Choose factory by host OS
try (GdbManager mgr = newInstance(new LinuxPtyFactory())) {
try (GdbManager mgr = newInstance(PtyFactory.local())) {
mgr.start(DEFAULT_GDB_CMD, args);
mgr.runRC().get();
mgr.consoleLoop();
Expand Down Expand Up @@ -434,23 +432,6 @@ default void close() {
*/
CompletableFuture<Void> removeInferior(GdbInferior inferior);

/**
* Interrupt the GDB session
*
* <p>
* The manager may employ a variety of mechanisms depending on the current configuration. If
* multiple interpreters are available, it will issue an "interrupt" command on whichever
* interpreter it believes is responsive -- usually the opposite of the one issuing the last
* run, continue, step, etc. command. Otherwise, it sends Ctrl-C to GDB's TTY, which
* unfortunately is notoriously unreliable. The manager will send Ctrl-C to the TTY up to three
* times, waiting about 10ms between each, until GDB issues a stopped event and presents a new
* prompt. If that fails, it is up to the user to find an alternative means to interrupt the
* target, e.g., issuing {@code kill [pid]} from the a terminal on the target's host.
*
* @return a future that completes when GDB has entered the stopped state
*/
CompletableFuture<Void> interrupt();

/**
* List GDB's inferiors
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package agent.gdb.manager.evt;

import agent.gdb.manager.GdbCause;
import agent.gdb.manager.GdbCause.Causes;
import agent.gdb.manager.GdbState;
import agent.gdb.manager.impl.GdbEvent;
import agent.gdb.manager.impl.GdbPendingCommand;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
/**
* An "event" corresponding with GDB/MI commands
*
* <p>
* If using a PTY configured with local echo, the manager needs to recognize and ignore the commands
* it issued. GDB/MI makes them easy to distinguish, because they start with "-".
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* ###
* 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 agent.gdb.manager.evt;

import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;

/**
* An "event" corresponding with the {@code -exec-interrupt} GDB/MI command
*
* <p>
* If issued, the {@code -exec-interrupt} command is always issued "out of band". It skips the queue
* and is printed straight to GDB's pty, usually preceded by a Ctrl-C (char 3). As a result, GDB is
* going to print {@code ^done}, which will get mistaken for the completion of a command in the
* queue. By recognizing the command being echoed back, we can identify the done event that does
* with it, and ignore it.
*/
public class GdbCommandEchoInterruptEvent extends AbstractGdbEvent<String> {

/**
* Construct a new "event", passing the tail through as information
*
* @param tail the text following the event type in the GDB/MI event record
* @throws GdbParseError if the tail cannot be parsed
*/
public GdbCommandEchoInterruptEvent(CharSequence tail) throws GdbParseError {
super(tail);
}

@Override
protected String parseInfo(CharSequence tail) throws GdbParseError {
return tail.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
*/
package agent.gdb.manager.impl;

import static ghidra.async.AsyncUtils.loop;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.JDialog;
import javax.swing.JOptionPane;
Expand All @@ -40,12 +39,14 @@
import agent.gdb.manager.parsing.GdbMiParser;
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
import agent.gdb.pty.*;
import agent.gdb.pty.windows.AnsiBufferedInputStream;
import ghidra.GhidraApplicationLayout;
import ghidra.async.*;
import ghidra.async.AsyncLock.Hold;
import ghidra.dbg.error.DebuggerModelTerminatingException;
import ghidra.dbg.util.HandlerMap;
import ghidra.dbg.util.PrefixMap;
import ghidra.framework.OperatingSystem;
import ghidra.lifecycle.Internal;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
Expand Down Expand Up @@ -107,8 +108,13 @@ class PtyThread extends Thread {
PtyThread(Pty pty, Channel channel, Interpreter interpreter) {
this.pty = pty;
this.channel = channel;
this.reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
InputStream inputStream = pty.getParent().getInputStream();
// TODO: This should really only be applied to the MI2 console
// But, we don't know what we have until we read it....
if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) {
inputStream = new AnsiBufferedInputStream(inputStream);
}
this.reader = new BufferedReader(new InputStreamReader(inputStream));
this.interpreter = interpreter;
hasWriter = new CompletableFuture<>();
}
Expand Down Expand Up @@ -202,9 +208,9 @@ public void run() {
private final AsyncLock cmdLock = new AsyncLock();
private final AtomicReference<AsyncLock.Hold> cmdLockHold = new AtomicReference<>(null);
private ExecutorService executor;
private final AsyncTimer timer = AsyncTimer.DEFAULT_TIMER;

private GdbPendingCommand<?> curCmd = null;
private int interruptCount = 0;

private final Map<Integer, GdbInferiorImpl> inferiors = new LinkedHashMap<>();
private GdbInferiorImpl curInferior = null;
Expand Down Expand Up @@ -248,6 +254,7 @@ private void initLog() {
File userSettings = layout.getUserSettingsDir();
File logFile = new File(userSettings, "GDB.log");
try {
logFile.getParentFile().mkdirs();
logFile.createNewFile();
}
catch (Exception e) {
Expand Down Expand Up @@ -287,6 +294,7 @@ private void trackRunningInterpreter(GdbState oldSt, GdbState st, GdbCause cause
}

private void defaultPrefixes() {
mi2PrefixMap.put("-exec-interrupt", GdbCommandEchoInterruptEvent::new);
mi2PrefixMap.put("-", GdbCommandEchoEvent::new);
mi2PrefixMap.put("~", GdbConsoleOutputEvent::fromMi2);
mi2PrefixMap.put("@", GdbTargetOutputEvent::new);
Expand Down Expand Up @@ -320,6 +328,7 @@ private void defaultPrefixes() {
}

private void defaultHandlers() {
handlerMap.putVoid(GdbCommandEchoInterruptEvent.class, this::pushCmdInterrupt);
handlerMap.putVoid(GdbCommandEchoEvent.class, this::ignoreCmdEcho);
handlerMap.putVoid(GdbConsoleOutputEvent.class, this::processStdOut);
handlerMap.putVoid(GdbTargetOutputEvent.class, this::processTargetOut);
Expand Down Expand Up @@ -637,7 +646,8 @@ public void start(String gdbCmd, String... args) throws IOException {
.get(10, TimeUnit.SECONDS);
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException("Could not detect GDB's interpreter mode");
throw new IOException(
"Could not detect GDB's interpreter mode. Try " + gdbCmd + " -i mi2");
}
if (state.get() == GdbState.EXIT) {
throw new IOException("GDB terminated before first prompt");
Expand All @@ -651,15 +661,24 @@ public void start(String gdbCmd, String... args) throws IOException {
// Looks terrible, but we're already in this world
cliThread.writer.print("set confirm off" + newLine);
cliThread.writer.print("set pagination off" + newLine);
cliThread.writer
.print("new-ui mi2 " + mi2Pty.getChild().nullSession() + newLine);
String ptyName;
try {
ptyName = Objects.requireNonNull(mi2Pty.getChild().nullSession());
}
catch (UnsupportedOperationException e) {
throw new IOException(
"Pty implementation does not support null sessions. Try " + gdbCmd +
" i mi2",
e);
}
cliThread.writer.print("new-ui mi2 " + ptyName + newLine);
cliThread.writer.flush();

mi2Thread = new PtyThread(mi2Pty, Channel.STDOUT, Interpreter.MI2);
mi2Thread.setName("GDB Read MI2");
mi2Thread.start();
try {
mi2Thread.hasWriter.get(2, TimeUnit.SECONDS);
mi2Thread.hasWriter.get(10, TimeUnit.SECONDS);
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(
Expand Down Expand Up @@ -720,10 +739,13 @@ protected CompletableFuture<Void> rc() {
// NB. confirm and pagination are already disabled here
return AsyncUtils.NIL;
}
else {
// NB. Don't disable pagination here. MI2 is not paginated.
return console("set confirm off", CompletesWithRunning.CANNOT);
}
// NB. Don't disable pagination here. MI2 is not paginated.
return CompletableFuture.allOf(
console("set confirm off", CompletesWithRunning.CANNOT),
console("set new-console on", CompletesWithRunning.CANNOT).exceptionally(e -> {
// not Windows. So what?
return null;
}));
}

protected void resync() {
Expand Down Expand Up @@ -918,6 +940,12 @@ protected void checkImpliedFocusChange() {
}

protected synchronized void processEvent(GdbEvent<?> evt) {
if (evt instanceof AbstractGdbCompletedCommandEvent && interruptCount > 0) {
interruptCount--;
Msg.debug(this, "Ignoring " + evt +
" from -exec-interrupt. new count = " + interruptCount);
return;
}
/**
* NOTE: I've forgotten why, but the the state update needs to happen between handle and
* finish.
Expand Down Expand Up @@ -1006,6 +1034,10 @@ protected void processGdbExited(int exitcode) {
Msg.info(this, "GDB exited with code " + exitcode);
}

protected void pushCmdInterrupt(GdbCommandEchoInterruptEvent evt, Void v) {
interruptCount++;
}

/**
* Called for lines starting with "-", which are just commands echoed back by the PTY
*
Expand Down Expand Up @@ -1376,6 +1408,7 @@ protected void processParamChanged(GdbParamChangedEvent evt, Void v) {
/**
* Check that a command completion event was claimed
*
* <p>
* Except under certain error conditions, GDB should never issue a command completed event that
* is not associated with a command. A command implementation in the manager must claim the
* completion event. This is an assertion to ensure no implementation forgets to do that.
Expand Down Expand Up @@ -1738,33 +1771,6 @@ public CompletableFuture<String> consoleCapture(String command, CompletesWithRun
GdbConsoleExecCommand.Output.CAPTURE, cwr));
}

@Override
public CompletableFuture<Void> interrupt() {
AtomicInteger retryCount = new AtomicInteger();
return loop(TypeSpec.VOID, loop -> {
GdbCommand<Void> interrupt = new GdbInterruptCommand(this);
execute(interrupt).thenApply(e -> (Throwable) null)
.exceptionally(e -> e)
.handle(loop::consume);
}, TypeSpec.cls(Throwable.class), (exc, loop) -> {
Msg.debug(this, "Executed an interrupt");
if (exc == null) {
loop.exit();
}
else if (state.get() == GdbState.STOPPED) {
// Not the cleanest, but as long as we're stopped, why not call it good?
loop.exit();
}
else if (retryCount.getAndAdd(1) >= INTERRUPT_MAX_RETRIES) {
loop.exit(exc);
}
else {
Msg.error(this, "Error executing interrupt: " + exc);
timer.mark().after(INTERRUPT_RETRY_PERIOD_MILLIS).handle(loop::repeat);
}
});
}

@Override
public CompletableFuture<Map<Integer, GdbInferior>> listInferiors() {
return execute(new GdbListInferiorsCommand(this));
Expand Down
Loading

0 comments on commit f2a34dc

Please sign in to comment.