Merge remote-tracking branch 'origin/GP-869_d-millar_ConPTY--REBASED-1'

(Closes #2908)
This commit is contained in:
ghidra1 2022-01-26 17:09:49 -05:00
commit 7ab2f5d38f
33 changed files with 1922 additions and 208 deletions

View file

@ -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')

View file

@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture;
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;
@ -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))

View file

@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture;
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 {
@ -36,8 +36,7 @@ public class GdbGadpServerImpl implements GdbGadpServer {
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);
}

View file

@ -26,7 +26,6 @@ import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
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
@ -86,8 +85,7 @@ public interface GdbManager extends AutoCloseable, GdbConsoleOperations, GdbBrea
*/
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();
@ -434,23 +432,6 @@ public interface GdbManager extends AutoCloseable, GdbConsoleOperations, GdbBrea
*/
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
*

View file

@ -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;

View file

@ -20,6 +20,7 @@ import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
/**
* 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 "-".
*/

View file

@ -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();
}
}

View file

@ -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;
@ -40,12 +39,14 @@ import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.CompletesWithRunning;
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;
@ -107,8 +108,13 @@ public class GdbManagerImpl implements GdbManager {
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<>();
}
@ -202,9 +208,9 @@ public class GdbManagerImpl implements GdbManager {
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;
@ -248,6 +254,7 @@ public class GdbManagerImpl implements GdbManager {
File userSettings = layout.getUserSettingsDir();
File logFile = new File(userSettings, "GDB.log");
try {
logFile.getParentFile().mkdirs();
logFile.createNewFile();
}
catch (Exception e) {
@ -287,6 +294,7 @@ public class GdbManagerImpl implements GdbManager {
}
private void defaultPrefixes() {
mi2PrefixMap.put("-exec-interrupt", GdbCommandEchoInterruptEvent::new);
mi2PrefixMap.put("-", GdbCommandEchoEvent::new);
mi2PrefixMap.put("~", GdbConsoleOutputEvent::fromMi2);
mi2PrefixMap.put("@", GdbTargetOutputEvent::new);
@ -320,6 +328,7 @@ public class GdbManagerImpl implements GdbManager {
}
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);
@ -637,7 +646,8 @@ public class GdbManagerImpl implements GdbManager {
.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");
@ -651,15 +661,24 @@ public class GdbManagerImpl implements GdbManager {
// 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(
@ -720,10 +739,13 @@ public class GdbManagerImpl implements GdbManager {
// 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() {
@ -918,6 +940,12 @@ public class GdbManagerImpl implements GdbManager {
}
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.
@ -1006,6 +1034,10 @@ public class GdbManagerImpl implements GdbManager {
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
*
@ -1376,6 +1408,7 @@ public class GdbManagerImpl implements GdbManager {
/**
* 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.
@ -1738,33 +1771,6 @@ public class GdbManagerImpl implements GdbManager {
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));

View file

@ -1,84 +0,0 @@
/* ###
* 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.impl.cmd;
import agent.gdb.manager.GdbManager;
import agent.gdb.manager.GdbState;
import agent.gdb.manager.evt.GdbStoppedEvent;
import agent.gdb.manager.impl.*;
import agent.gdb.manager.impl.GdbManagerImpl.Interpreter;
import ghidra.util.Msg;
/**
* Implementation of {@link GdbManager#interrupt()} when we start GDB
*/
public class GdbInterruptCommand extends AbstractGdbCommand<Void> {
public GdbInterruptCommand(GdbManagerImpl manager) {
super(manager);
}
@Override
public boolean validInState(GdbState state) {
//return state == GdbState.RUNNING;
return true;
}
@Override
public String encode() {
Interpreter i = getInterpreter();
if (i == manager.getRunningInterpreter()) {
Msg.debug(this, "Using ^C to interrupt via " + i);
return "\u0003";
}
switch (i) {
case CLI:
Msg.debug(this, "Interrupting via CLI");
return "interrupt";
case MI2:
Msg.debug(this, "Interrupting via MI2");
return "-exec-interrupt";
default:
throw new AssertionError();
}
}
@Override
public boolean handle(GdbEvent<?> evt, GdbPendingCommand<?> pending) {
if (super.handle(evt, pending)) {
return true;
}
else if (evt instanceof GdbStoppedEvent) {
pending.claim(evt);
return true;
}
return false;
}
@Override
public Void complete(GdbPendingCommand<?> pending) {
// When using -exec-interrupt, ^done will come before *stopped
//pending.findSingleOf(GdbStoppedEvent.class);
return null;
}
@Override
public Interpreter getInterpreter() {
if (manager.hasCli() && manager.getRunningInterpreter() == Interpreter.MI2) {
return Interpreter.CLI;
}
return Interpreter.MI2;
}
}

View file

@ -144,7 +144,8 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
gdb.start(gdbCmd, args);
}
catch (IOException e) {
throw new DebuggerModelTerminatingException("Error while starting GDB", e);
throw new DebuggerModelTerminatingException(
"Error while starting GDB: " + e.getMessage(), e);
}
}).thenCompose(__ -> {
return gdb.runRC();

View file

@ -17,11 +17,31 @@ package agent.gdb.pty;
import java.io.IOException;
import agent.gdb.pty.linux.LinuxPtyFactory;
import agent.gdb.pty.windows.ConPtyFactory;
import ghidra.framework.OperatingSystem;
/**
* A mechanism for opening pseudo-terminals
*/
public interface PtyFactory {
/**
* Choose a factory of local pty's for the host operating system
*
* @return the factory
*/
static PtyFactory local() {
switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
case LINUX:
return new LinuxPtyFactory();
case WINDOWS:
return new ConPtyFactory();
default:
throw new UnsupportedOperationException();
}
}
/**
* Open a new pseudo-terminal
*

View file

@ -28,6 +28,6 @@ public class LinuxPtyFactory implements PtyFactory {
@Override
public String getDescription() {
return "local";
return "local (Linux)";
}
}

View file

@ -0,0 +1,77 @@
/* ###
* 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.pty.local;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.ptr.IntByReference;
import agent.gdb.pty.PtySession;
import agent.gdb.pty.windows.Handle;
import ghidra.util.Msg;
public class LocalWindowsNativeProcessPtySession implements PtySession {
//private final int pid;
//private final int tid;
private final Handle processHandle;
//private final Handle threadHandle;
public LocalWindowsNativeProcessPtySession(int pid, int tid, Handle processHandle,
Handle threadHandle) {
//this.pid = pid;
//this.tid = tid;
this.processHandle = processHandle;
//this.threadHandle = threadHandle;
Msg.info(this, "local Windows Pty session. PID = " + pid);
}
@Override
public int waitExited() throws InterruptedException {
while (true) {
switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), -1)) {
case Kernel32.WAIT_OBJECT_0:
case Kernel32.WAIT_ABANDONED:
IntByReference lpExitCode = new IntByReference();
Kernel32.INSTANCE.GetExitCodeProcess(processHandle.getNative(), lpExitCode);
if (lpExitCode.getValue() != WinBase.STILL_ACTIVE) {
return lpExitCode.getValue();
}
case Kernel32.WAIT_TIMEOUT:
throw new AssertionError();
case Kernel32.WAIT_FAILED:
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
}
}
@Override
public void destroyForcibly() {
if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) {
int error = Kernel32.INSTANCE.GetLastError();
switch (error) {
case Kernel32.ERROR_ACCESS_DENIED:
/**
* This indicates the process has already terminated. It's unclear to me whether
* or not that is the only possible cause of this error.
*/
return;
}
throw new LastErrorException(error);
}
}
}

View file

@ -0,0 +1,473 @@
/* ###
* 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.pty.windows;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Stream;
// TODO: I shouldn't have to do any of this.
public class AnsiBufferedInputStream extends InputStream {
private static final Charset WINDOWS_1252 = Charset.forName("windows-1252");
private enum Mode {
CHARS,
ESC,
CSI,
CSI_p,
CSI_Q,
OSC,
WINDOW_TITLE,
WINDOW_TITLE_ESC;
}
private final InputStream in;
private int countIn = 0;
private ByteBuffer lineBaked = ByteBuffer.allocate(Short.MAX_VALUE);
private ByteBuffer lineBuf = ByteBuffer.allocate(Short.MAX_VALUE);
private ByteBuffer escBuf = ByteBuffer.allocate(1024);
private ByteBuffer titleBuf = ByteBuffer.allocate(255);
private Mode mode = Mode.CHARS;
public AnsiBufferedInputStream(InputStream in) {
if (in instanceof HandleInputStream) {
// Spare myself the 1-by-1 native calls
in = new BufferedInputStream(in);
}
this.in = in;
lineBuf.limit(0);
lineBaked.limit(0);
}
@Override
public int read() throws IOException {
if (lineBaked.hasRemaining()) {
return lineBaked.get();
}
if (readUntilBaked() < 0) {
return -1;
}
if (lineBaked.hasRemaining()) {
return lineBaked.get();
}
return -1; // EOF
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (!lineBaked.hasRemaining()) {
if (readUntilBaked() < 0) {
return -1;
}
}
int read = Math.min(lineBaked.remaining(), len);
lineBaked.get(b, off, read);
return read;
}
@Override
public void close() throws IOException {
in.close();
super.close();
}
protected int readUntilBaked() throws IOException {
while (!lineBaked.hasRemaining()) {
if (processNext() < 0) {
break;
}
}
if (!lineBaked.hasRemaining()) {
return -1;
}
return lineBaked.remaining();
}
protected void printDebugChar(byte c) {
if (0x20 <= c && c <= 0x7f) {
System.err.print(new String(new byte[] { c }));
}
else {
System.err.print(String.format("<%02x>", c & 0xff));
}
}
protected int processNext() throws IOException {
int ci = in.read();
if (ci == -1) {
return -1;
}
byte c = (byte) ci;
// printDebugChar(c);
switch (mode) {
case CHARS:
processChars(c);
break;
case ESC:
processEsc(c);
break;
case CSI:
processCsi(c);
break;
case CSI_p:
processCsiParamOrCommand(c);
break;
case CSI_Q:
processCsiQ(c);
break;
case OSC:
processOsc(c);
break;
case WINDOW_TITLE:
processWindowTitle(c);
break;
case WINDOW_TITLE_ESC:
processWindowTitleEsc(c);
break;
default:
throw new AssertionError();
}
countIn++;
return c;
}
/**
* There's not really a good way to know if any trailing space was intentional. For GDB/MI, that
* doesn't really matter.
*/
protected int guessEnd() {
for (int i = lineBuf.limit() - 1; i >= 0; i--) {
byte c = lineBuf.get(i);
if (c != 0x20 && c != 0) {
return i + 1;
}
}
return 0;
}
protected void bakeLine() {
lineBuf.position(0);
lineBuf.limit(guessEnd() + 1);
lineBuf.put(lineBuf.limit() - 1, (byte) '\n');
ByteBuffer temp = lineBaked;
lineBaked = lineBuf;
lineBuf = temp;
lineBuf.clear();
Arrays.fill(lineBuf.array(), (byte) 0);
lineBuf.limit(0);
}
protected void appendChar(byte c) {
int limit = lineBuf.limit();
if (lineBuf.position() == limit) {
lineBuf.limit(limit + 1);
}
lineBuf.put(c);
}
protected void processChars(byte c) {
switch (c) {
case 0x08:
if (lineBuf.get(lineBuf.position() - 1) == ' ') {
lineBuf.position(lineBuf.position() - 1);
}
break;
case '\n':
//appendChar(c);
bakeLine();
break;
case 0x1b:
mode = Mode.ESC;
break;
default:
appendChar(c);
break;
}
}
protected void processEsc(byte c) {
switch (c) {
case '[':
mode = Mode.CSI;
break;
case ']':
mode = Mode.OSC;
break;
default:
throw new AssertionError("Saw 'ESC " + c + "' at " + countIn);
}
}
protected void processCsi(byte c) {
switch (c) {
default:
processCsiParamOrCommand(c);
break;
case '?':
mode = Mode.CSI_Q;
break;
}
}
protected void processCsiParamOrCommand(byte c) {
switch (c) {
default:
escBuf.put(c);
break;
case 'A':
execCursorUp();
mode = Mode.CHARS;
break;
case 'B':
execCursorDown();
mode = Mode.CHARS;
break;
case 'C':
execCursorForward();
mode = Mode.CHARS;
break;
case 'D':
execCursorBackward();
mode = Mode.CHARS;
break;
case 'H':
execCursorPosition();
mode = Mode.CHARS;
break;
case 'J':
execEraseInDisplay();
mode = Mode.CHARS;
break;
case 'K':
execEraseInLine();
mode = Mode.CHARS;
break;
case 'X':
execEraseCharacter();
mode = Mode.CHARS;
break;
case 'm':
execSetGraphicsRendition();
mode = Mode.CHARS;
break;
}
}
protected void processCsiQ(byte c) {
String buf;
switch (c) {
default:
escBuf.put(c);
break;
case 'h':
buf = readAndClearEscBuf();
if ("12".equals(buf)) {
execTextCursorEnableBlinking();
escBuf.clear();
mode = Mode.CHARS;
}
else if ("25".equals(buf)) {
execTextCursorEnableModeShow();
escBuf.clear();
mode = Mode.CHARS;
}
else {
throw new AssertionError();
}
break;
case 'l':
buf = readAndClearEscBuf();
if ("12".equals(buf)) {
execTextCursorDisableBlinking();
escBuf.clear();
mode = Mode.CHARS;
}
else if ("25".equals(buf)) {
execTextCursorDisableModeShow();
escBuf.clear();
mode = Mode.CHARS;
}
break;
}
}
protected void processOsc(byte c) {
switch (c) {
default:
escBuf.put(c);
break;
case ';':
if (Set.of("0", "2").contains(readAndClearEscBuf())) {
mode = Mode.WINDOW_TITLE;
escBuf.clear();
break;
}
throw new AssertionError();
}
}
protected void processWindowTitle(byte c) {
switch (c) {
default:
titleBuf.put(c);
break;
case 0x07: // bell, even though MSDN says longer form preferred
execSetWindowTitle();
mode = Mode.CHARS;
break;
case 0x1b:
mode = Mode.WINDOW_TITLE_ESC;
break;
}
}
protected void processWindowTitleEsc(byte c) {
switch (c) {
case '\\':
execSetWindowTitle();
mode = Mode.CHARS;
break;
default:
throw new AssertionError("Saw <ST> ... ESC " + c + " at " + countIn);
}
}
protected String readAndClear(ByteBuffer buf) {
buf.flip();
String result = new String(buf.array(), buf.position(), buf.remaining(), WINDOWS_1252);
buf.clear();
return result;
}
protected String readAndClearEscBuf() {
return readAndClear(escBuf);
}
protected int parseNumericBuffer() {
String numeric = readAndClearEscBuf();
int result = Integer.parseInt(numeric);
return result;
}
protected int[] parseNumericListBuffer() {
String numericList = readAndClearEscBuf();
if (numericList.isEmpty()) {
return new int[] {};
}
return Stream.of(numericList.split(";"))
.mapToInt(Integer::parseInt)
.toArray();
}
protected void execCursorUp() {
throw new UnsupportedOperationException("Cursor Up");
}
protected void execCursorDown() {
throw new UnsupportedOperationException("Cursor Down");
}
protected void setPosition(int newPosition) {
if (lineBuf.limit() < newPosition) {
lineBuf.limit(newPosition);
}
lineBuf.position(newPosition);
}
protected void execCursorForward() {
int delta = parseNumericBuffer();
setPosition(lineBuf.position() + delta);
}
protected void execCursorBackward() {
int delta = parseNumericBuffer();
lineBuf.position(lineBuf.position() - delta);
}
protected void execCursorPosition() {
int[] yx = parseNumericListBuffer();
if (yx.length == 0) {
lineBuf.position(0);
return;
}
if (yx.length != 2) {
throw new AssertionError();
}
if (yx[0] != 1) {
throw new AssertionError();
}
lineBuf.position(yx[1] - 1);
}
protected void execTextCursorEnableBlinking() {
// Don't care
}
protected void execTextCursorDisableBlinking() {
// Don't care
}
protected void execTextCursorEnableModeShow() {
// Don't care
}
protected void execTextCursorDisableModeShow() {
// Don't care
}
protected void execEraseInDisplay() {
// Because I have only one line, right?
execEraseInLine();
}
protected void execEraseInLine() {
switch (parseNumericBuffer()) {
case 0:
Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.capacity(), (byte) 0);
break;
case 1:
Arrays.fill(lineBuf.array(), 0, lineBuf.position() + 1, (byte) 0);
break;
case 2:
Arrays.fill(lineBuf.array(), (byte) 0);
break;
}
}
protected void execEraseCharacter() {
int count = parseNumericBuffer();
Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.position() + count, (byte) ' ');
}
protected void execSetGraphicsRendition() {
// TODO: Maybe echo these or provide callbacks
// Otherwise, don't care
escBuf.clear();
}
protected void execSetWindowTitle() {
// Msg.info(this, "Title: " + readAndClear(titleBuf));
// TODO: Maybe a callback. Otherwise, don't care
titleBuf.clear();
}
}

View file

@ -0,0 +1,111 @@
/* ###
* 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.pty.windows;
import java.io.IOException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinDef.DWORD;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
import com.sun.jna.platform.win32.COM.COMUtils;
import agent.gdb.pty.*;
import agent.gdb.pty.windows.jna.ConsoleApiNative;
import agent.gdb.pty.windows.jna.ConsoleApiNative.COORD;
public class ConPty implements Pty {
static final DWORD DW_ZERO = new DWORD(0);
static final DWORD DW_ONE = new DWORD(1);
static final DWORD PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = new DWORD(0x20016);
static final DWORD EXTENDED_STARTUPINFO_PRESENT =
new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT);
private static final COORD SIZE = new COORD();
static {
SIZE.X = Short.MAX_VALUE;
SIZE.Y = 1;
}
private final Pipe pipeToChild;
private final Pipe pipeFromChild;
private final PseudoConsoleHandle pseudoConsoleHandle;
private boolean closed = false;
private final ConPtyParent parent;
private final ConPtyChild child;
public static ConPty openpty() {
// Create communication channels
Pipe pipeToChild = Pipe.createPipe();
Pipe pipeFromChild = Pipe.createPipe();
// Close the child-connected ends after creating the pseudoconsole
// Keep the parent-connected ends, because we're the parent
HANDLEByReference lphPC = new HANDLEByReference();
COMUtils.checkRC(ConsoleApiNative.INSTANCE.CreatePseudoConsole(
SIZE,
pipeToChild.getReadHandle().getNative(),
pipeFromChild.getWriteHandle().getNative(),
DW_ZERO,
lphPC));
return new ConPty(pipeToChild, pipeFromChild, new PseudoConsoleHandle(lphPC.getValue()));
}
public ConPty(Pipe pipeToChild, Pipe pipeFromChild, PseudoConsoleHandle pseudoConsoleHandle) {
this.pipeToChild = pipeToChild;
this.pipeFromChild = pipeFromChild;
this.pseudoConsoleHandle = pseudoConsoleHandle;
// TODO: See if this can all be combined with named pipes.
// Would be nice if that's sufficient to support new-ui
this.parent = new ConPtyParent(pipeToChild.getWriteHandle(), pipeFromChild.getReadHandle());
this.child = new ConPtyChild(pipeFromChild.getWriteHandle(), pipeToChild.getReadHandle(),
pseudoConsoleHandle);
}
@Override
public PtyParent getParent() {
return parent;
}
@Override
public PtyChild getChild() {
return child;
}
@Override
public synchronized void close() throws IOException {
if (closed) {
return;
}
try {
pseudoConsoleHandle.close();
pipeToChild.close();
pipeFromChild.close();
}
catch (IOException e) {
throw e;
}
catch (Exception e) {
throw new IOException(e);
}
closed = true;
}
}

View file

@ -0,0 +1,111 @@
/* ###
* 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.pty.windows;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import com.sun.jna.*;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinBase.PROCESS_INFORMATION;
import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import agent.gdb.pty.PtyChild;
import agent.gdb.pty.local.LocalWindowsNativeProcessPtySession;
import agent.gdb.pty.windows.jna.ConsoleApiNative;
import agent.gdb.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX;
import ghidra.dbg.util.ShellUtils;
public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
private final Handle pseudoConsoleHandle;
public ConPtyChild(Handle writeHandle, Handle readHandle, Handle pseudoConsoleHandle) {
super(writeHandle, readHandle);
this.pseudoConsoleHandle = pseudoConsoleHandle;
}
protected STARTUPINFOEX prepareStartupInfo() {
STARTUPINFOEX si = new STARTUPINFOEX();
si.StartupInfo.cb = new DWORD(si.size());
si.StartupInfo.hStdOutput = new HANDLE();
si.StartupInfo.hStdError = new HANDLE();
si.StartupInfo.hStdInput = new HANDLE();
si.StartupInfo.dwFlags = WinBase.STARTF_USESTDHANDLES;
// Discover the size required for the thread attrs list and allocate
UINTByReference bytesRequired = new UINTByReference();
// NB. This will "fail." See Remarks on MSDN.
ConsoleApiNative.INSTANCE.InitializeProcThreadAttributeList(
null, ConPty.DW_ONE, ConPty.DW_ZERO, bytesRequired);
// NB. Memory frees itself in .finalize()
si.lpAttributeList = new Memory(bytesRequired.getValue().intValue());
// Initialize it
if (!ConsoleApiNative.INSTANCE.InitializeProcThreadAttributeList(
si.lpAttributeList, ConPty.DW_ONE, ConPty.DW_ZERO, bytesRequired)
.booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
// Set the pseudoconsole information into the list
if (!ConsoleApiNative.INSTANCE.UpdateProcThreadAttribute(
si.lpAttributeList, ConPty.DW_ZERO,
ConPty.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
new PVOID(pseudoConsoleHandle.getNative().getPointer()),
new DWORD(Native.POINTER_SIZE),
null, null).booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return si;
}
@Override
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env)
throws IOException {
/**
* TODO: How to incorporate environment into CreateProcess?
*/
STARTUPINFOEX si = prepareStartupInfo();
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
if (!ConsoleApiNative.INSTANCE.CreateProcessW(
null /*lpApplicationName*/,
new WString(ShellUtils.generateLine(Arrays.asList(args))),
null /*lpProcessAttributes*/,
null /*lpThreadAttributes*/,
false /*bInheritHandles*/,
ConPty.EXTENDED_STARTUPINFO_PRESENT /*dwCreationFlags*/,
null /*lpEnvironment*/,
null /*lpCurrentDirectory*/,
si /*lpStartupInfo*/,
pi /*lpProcessInformation*/).booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return new LocalWindowsNativeProcessPtySession(pi.dwProcessId.intValue(),
pi.dwThreadId.intValue(),
new Handle(pi.hProcess), new Handle(pi.hThread));
}
@Override
public String nullSession() throws IOException {
throw new UnsupportedOperationException("ConPTY does not have a name");
}
}

View file

@ -0,0 +1,41 @@
/* ###
* 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.pty.windows;
import java.io.InputStream;
import java.io.OutputStream;
import agent.gdb.pty.PtyEndpoint;
public class ConPtyEndpoint implements PtyEndpoint {
protected InputStream inputStream;
protected OutputStream outputStream;
public ConPtyEndpoint(Handle writeHandle, Handle readHandle) {
this.inputStream = new HandleInputStream(readHandle);
this.outputStream = new HandleOutputStream(writeHandle);
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public InputStream getInputStream() {
return inputStream;
}
}

View file

@ -0,0 +1,33 @@
/* ###
* 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.pty.windows;
import java.io.IOException;
import agent.gdb.pty.Pty;
import agent.gdb.pty.PtyFactory;
public class ConPtyFactory implements PtyFactory {
@Override
public Pty openpty() throws IOException {
return ConPty.openpty();
}
@Override
public String getDescription() {
return "local (Windows)";
}
}

View file

@ -0,0 +1,24 @@
/* ###
* 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.pty.windows;
import agent.gdb.pty.PtyParent;
public class ConPtyParent extends ConPtyEndpoint implements PtyParent {
public ConPtyParent(Handle writeHandle, Handle readHandle) {
super(writeHandle, readHandle);
}
}

View file

@ -0,0 +1,66 @@
/* ###
* 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.pty.windows;
import java.lang.ref.Cleaner;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinNT.HANDLE;
public class Handle implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
protected static class State implements Runnable {
protected final HANDLE handle;
protected State(HANDLE handle) {
this.handle = handle;
}
@Override
public void run() {
if (!Kernel32.INSTANCE.CloseHandle(handle)) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
}
}
private final State state;
private final Cleaner.Cleanable cleanable;
public Handle(HANDLE handle) {
this.state = newState(handle);
this.cleanable = CLEANER.register(this, state);
}
protected State newState(HANDLE handle) {
return new State(handle);
}
@Override
public void close() throws Exception {
cleanable.clean();
}
public HANDLE getNative() {
HANDLE handle = state.handle;
if (handle == null) {
throw new IllegalStateException("This handle is no longer valid");
}
return handle;
}
}

View file

@ -0,0 +1,94 @@
/* ###
* 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.pty.windows;
import java.io.IOException;
import java.io.InputStream;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.ptr.IntByReference;
public class HandleInputStream extends InputStream {
private final Handle handle;
private boolean closed = false;
HandleInputStream(Handle handle) {
this.handle = handle;
}
@Override
public synchronized int read() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
byte[] buf = new byte[1];
if (0 == read(buf)) {
return -1;
}
return buf[0] & 0x0FF;
}
protected void waitPipeConnected() {
if (Kernel32.INSTANCE.ConnectNamedPipe(handle.getNative(), null)) {
return; // We waited, and now we're connected
}
int error = Kernel32.INSTANCE.GetLastError();
if (error == Kernel32.ERROR_PIPE_CONNECTED) {
return; // We got the connection before we waited. OK
}
throw new LastErrorException(error);
}
@Override
public synchronized int read(byte[] b) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
IntByReference dwRead = new IntByReference();
while (!Kernel32.INSTANCE.ReadFile(handle.getNative(), b, b.length, dwRead, null)) {
int error = Kernel32.INSTANCE.GetLastError();
switch (error) {
case Kernel32.ERROR_BROKEN_PIPE:
return -1;
case Kernel32.ERROR_PIPE_LISTENING:
/**
* Well, we know we're dealing with a listening pipe, now. Wait for a client,
* then try reading again.
*/
waitPipeConnected();
continue;
}
throw new IOException("Could not read",
new LastErrorException(error));
}
return dwRead.getValue();
}
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
byte[] temp = new byte[len];
int read = read(temp);
System.arraycopy(temp, 0, b, off, read);
return read;
}
@Override
public synchronized void close() throws IOException {
closed = true;
}
}

View file

@ -0,0 +1,92 @@
/* ###
* 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.pty.windows;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.ptr.IntByReference;
public class HandleOutputStream extends OutputStream {
private final Handle handle;
private boolean closed = false;
public HandleOutputStream(Handle handle) {
this.handle = handle;
}
@Override
public synchronized void write(int b) throws IOException {
write(new byte[] { (byte) b });
}
@Override
public synchronized void write(byte[] b) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
int total = 0;
do {
IntByReference dwWritten = new IntByReference();
if (!Kernel32.INSTANCE.WriteFile(handle.getNative(), b, b.length, dwWritten, null)) {
throw new IOException("Could not write",
new LastErrorException(Kernel32.INSTANCE.GetLastError()));
}
total += dwWritten.getValue();
}
while (total < b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
// Abstraction of Windows API given by JNA doesn't have offset
// In C, could add offset to lpBuffer, but not here :(
write(Arrays.copyOfRange(b, off, off + len));
}
@Override
public synchronized void close() throws IOException {
closed = true;
}
/**
* Check whether this handle has buffered output
*
* <p>
* Windows can get touchy when trying to flush handles that are not actually buffered. If the
* wrapped handle is not buffered, then this method must return false, otherwise, any attempt to
* flush this stream will result in {@code ERROR_INVALID_HANDLE}.
*
* @return
*/
protected boolean isBuffered() {
return true;
}
@Override
public void flush() throws IOException {
if (!isBuffered()) {
return;
}
if (!(Kernel32.INSTANCE.FlushFileBuffers(handle.getNative()))) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
}
}

View file

@ -0,0 +1,54 @@
/* ###
* 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.pty.windows;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase.SECURITY_ATTRIBUTES;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
public class Pipe {
public static Pipe createPipe() {
HANDLEByReference pRead = new HANDLEByReference();
HANDLEByReference pWrite = new HANDLEByReference();
if (!Kernel32.INSTANCE.CreatePipe(pRead, pWrite, new SECURITY_ATTRIBUTES(), 0)) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return new Pipe(new Handle(pRead.getValue()), new Handle(pWrite.getValue()));
}
private final Handle readHandle;
private final Handle writeHandle;
private Pipe(Handle read, Handle write) {
this.readHandle = read;
this.writeHandle = write;
}
public Handle getReadHandle() {
return readHandle;
}
public Handle getWriteHandle() {
return writeHandle;
}
public void close() throws Exception {
writeHandle.close();
readHandle.close();
}
}

View file

@ -0,0 +1,43 @@
/* ###
* 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.pty.windows;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import agent.gdb.pty.windows.jna.ConsoleApiNative;
public class PseudoConsoleHandle extends Handle {
protected static class PseudoConsoleState extends State {
public PseudoConsoleState(HANDLE handle) {
super(handle);
}
@Override
public void run() {
ConsoleApiNative.INSTANCE.ClosePseudoConsole(handle);
}
}
public PseudoConsoleHandle(HANDLE handle) {
super(handle);
}
@Override
protected State newState(HANDLE handle) {
return new PseudoConsoleState(handle);
}
}

View file

@ -0,0 +1,160 @@
/* ###
* 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.pty.windows.jna;
import java.util.List;
import com.sun.jna.*;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT.*;
import com.sun.jna.win32.StdCallLibrary;
public interface ConsoleApiNative extends StdCallLibrary {
ConsoleApiNative INSTANCE = Native.load("Kernel32.dll", ConsoleApiNative.class);
BOOL FAIL = new BOOL(false);
BOOL CreatePipe(HANDLEByReference hReadPipe, HANDLEByReference hWritePipe,
SECURITY_ATTRIBUTES.ByReference lpPipeAttributes, DWORD nSize);
HRESULT CreatePseudoConsole(COORD.ByValue size, HANDLE hInput, HANDLE hOutput,
DWORD dwFlags,
HANDLEByReference phPC);
void ClosePseudoConsole(HANDLE hPC);
BOOL InitializeProcThreadAttributeList(Pointer lpAttributeList,
DWORD dwAttributeCount, DWORD dwFlags, UINTByReference lpSize);
BOOL UpdateProcThreadAttribute(
Pointer lpAttributeList,
DWORD dwFlags,
DWORD Attribute,
PVOID lpValue,
DWORD cbSize,
PVOID lpPreviousValue,
ULONGLONGByReference lpReturnSize);
BOOL CreateProcessW(
WString lpApplicationName,
WString lpCommandLine,
WinBase.SECURITY_ATTRIBUTES lpProcessAttributes,
WinBase.SECURITY_ATTRIBUTES lpThreadAttributes,
boolean bInheritHandles,
DWORD dwCreationFlags,
Pointer lpEnvironment,
WString lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
WinBase.PROCESS_INFORMATION lpProcessInformation);
/*
BOOL GetConsoleMode(
HANDLE hConsoleMode,
DWORDByReference dwMode);
BOOL CreateProcessWithTokenW(
HANDLE hToken,
DWORD dwLogonFlags,
WString lpApplicationName,
WString lpCommandLine,
DWORD dwCreationFlags,
Pointer lpEnvironment,
WString lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
WinBase.PROCESS_INFORMATION lpProcessInformation);
BOOL LogonUserW(
WString lpUsername,
WString lpDomain,
WString lpPassword,
DWORD dwLogonType,
DWORD dwLogonProvider,
HANDLEByReference phToken);
*/
public static class COORD extends Structure implements Structure.ByValue {
public static class ByReference extends COORD
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder("X", "Y");
public short X;
public short Y;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
public static class SECURITY_ATTRIBUTES extends Structure {
public static class ByReference extends SECURITY_ATTRIBUTES
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder(
"nLength", "lpSecurityDescriptor", "bInheritedHandle");
public DWORD nLength;
public ULONGLONG lpSecurityDescriptor;
public BOOL bInheritedHandle;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
public static class PROC_THREAD_ATTRIBUTE_LIST extends Structure {
public static class ByReference extends PROC_THREAD_ATTRIBUTE_LIST
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder(
"dwFlags", "Size", "Count", "Reserved", "Unknown");
public DWORD dwFlags;
public ULONG Size;
public ULONG Count;
public ULONG Reserved;
public ULONGLONG Unknown;
//public PROC_THREAD_ATTRIBUTE_ENTRY Entries[0];
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
public static class STARTUPINFOEX extends Structure {
public static class ByReference extends STARTUPINFOEX
implements Structure.ByReference {
}
public static final List<String> FIELDS = createFieldsOrder(
"StartupInfo", "lpAttributeList");
public WinBase.STARTUPINFO StartupInfo;
public Pointer lpAttributeList;
@Override
protected List<String> getFieldOrder() {
return FIELDS;
}
}
}

View file

@ -264,7 +264,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
waitOn(libcLoaded);
Thread.sleep(100); // TODO: Why?
Msg.debug(this, "Interrupting");
waitOn(mgr.interrupt());
mgr.sendInterruptNow();
waitOn(mgr.waitForState(GdbState.STOPPED));
assertResponsive(mgr);
}
@ -285,7 +285,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
waitOn(libcLoaded);
Thread.sleep(100); // TODO: Why?
Msg.debug(this, "Interrupting");
waitOn(mgr.interrupt());
mgr.sendInterruptNow();
Msg.debug(this, "Verifying at syscall");
String out = waitOn(mgr.consoleCapture("x/1i $pc-2"));
// TODO: This is x86-specific
@ -296,7 +296,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
CompletableFuture<Void> stopped = mgr.waitForState(GdbState.STOPPED);
Thread.sleep(100); // NB: Not exactly reliable, but verify we're waiting
assertFalse(stopped.isDone());
waitOn(mgr.interrupt());
mgr.sendInterruptNow();
waitOn(stopped);
assertResponsive(mgr);
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.impl;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import org.junit.Ignore;
import agent.gdb.manager.GdbManager;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.windows.ConPtyFactory;
@Ignore("Need compatible version on CI")
public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {
@Override
protected CompletableFuture<Void> startManager(GdbManager manager) {
try {
manager.start("C:\\msys64\\mingw64\\bin\\gdb.exe", "-i", "mi2");
return manager.runRC();
}
catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
protected PtyFactory getPtyFactory() {
// TODO: Choose by host OS
return new ConPtyFactory();
}
}

View file

@ -0,0 +1,73 @@
/* ###
* 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.pty;
import static org.junit.Assert.assertEquals;
import java.io.*;
public class AbstractPtyTest {
public Thread pump(InputStream is, OutputStream os) {
Thread t = new Thread(() -> {
byte[] buf = new byte[1];
while (true) {
int len;
try {
len = is.read(buf);
if (len == -1) {
return;
}
os.write(buf, 0, len);
}
catch (IOException e) {
throw new AssertionError(e);
}
}
});
t.setDaemon(true);
t.start();
return t;
}
public BufferedReader loggingReader(InputStream is) {
return new BufferedReader(new InputStreamReader(is)) {
@Override
public String readLine() throws IOException {
String line = super.readLine();
System.out.println("log: " + line);
return line;
}
};
}
public Thread runExitCheck(int expected, PtySession session) {
Thread exitCheck = new Thread(() -> {
while (true) {
try {
assertEquals("Early exit with wrong code", expected,
session.waitExited());
return;
}
catch (InterruptedException e) {
System.err.println("Exit check interrupted");
}
}
});
exitCheck.setDaemon(true);
exitCheck.start();
return exitCheck;
}
}

View file

@ -17,16 +17,25 @@ package agent.gdb.pty.linux;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import java.io.*;
import java.util.*;
import org.junit.Before;
import org.junit.Test;
import agent.gdb.pty.AbstractPtyTest;
import agent.gdb.pty.PtySession;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem;
public class LinuxPtyTest extends AbstractPtyTest {
@Before
public void checkLinux() {
assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM);
}
public class LinuxPtyTest {
@Test
public void testOpenClosePty() throws IOException {
LinuxPty pty = LinuxPty.openpty();
@ -82,54 +91,6 @@ public class LinuxPtyTest {
}
}
public Thread pump(InputStream is, OutputStream os) {
Thread t = new Thread(() -> {
byte[] buf = new byte[1024];
while (true) {
int len;
try {
len = is.read(buf);
os.write(buf, 0, len);
}
catch (IOException e) {
throw new AssertionError(e);
}
}
});
t.setDaemon(true);
t.start();
return t;
}
public BufferedReader loggingReader(InputStream is) {
return new BufferedReader(new InputStreamReader(is)) {
@Override
public String readLine() throws IOException {
String line = super.readLine();
System.out.println("log: " + line);
return line;
}
};
}
public Thread runExitCheck(int expected, PtySession session) {
Thread exitCheck = new Thread(() -> {
while (true) {
try {
assertEquals("Early exit with wrong code", expected,
session.waitExited());
return;
}
catch (InterruptedException e) {
System.err.println("Exit check interrupted");
}
}
});
exitCheck.setDaemon(true);
exitCheck.start();
return exitCheck;
}
@Test
public void testSessionBashEchoTest() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();

View file

@ -0,0 +1,194 @@
/* ###
* 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.pty.windows;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import java.io.*;
import java.lang.ProcessBuilder.Redirect;
import org.junit.Before;
import org.junit.Test;
import com.sun.jna.LastErrorException;
import agent.gdb.pty.*;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem;
public class ConPtyTest extends AbstractPtyTest {
@Before
public void checkWindows() {
assumeTrue(OperatingSystem.WINDOWS == OperatingSystem.CURRENT_OPERATING_SYSTEM);
}
@Test
public void testSessionCmd() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
PtySession cmd = pty.getChild().session(new String[] { DummyProc.which("cmd") }, null);
pty.getParent().getOutputStream().write("exit\r\n".getBytes());
assertEquals(0, cmd.waitExited());
}
}
@Test
public void testSessionNonExistent() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
pty.getChild().session(new String[] { "thisHadBetterNoExist" }, null);
fail();
}
catch (LastErrorException e) {
assertEquals(2, e.getErrorCode());
}
}
@Test
public void testSessionCmdEchoTest() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession cmd = pty.getChild().session(new String[] { DummyProc.which("cmd") }, null);
runExitCheck(3, cmd);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertEquals(3, cmd.waitExited());
}
}
@Test
public void testSessionGdbLineLength() throws IOException, InterruptedException {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession gdb =
pty.getChild().session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe" }, null);
writer.println(
"echo This line is cleary much, much, much, much, much, much, much, much, much " +
" longer than 80 characters");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
}
}
@Test
public void testGdbInterruptPlain() throws Exception {
ProcessBuilder builder = new ProcessBuilder("C:\\msys64\\mingw64\\bin\\gdb.exe");
builder.redirectOutput(Redirect.PIPE);
builder.redirectInput(Redirect.PIPE);
builder.redirectErrorStream(true);
Process gdb = builder.start();
PrintWriter writer = new PrintWriter(gdb.getOutputStream());
pump(gdb.getInputStream(), System.err);
System.out.println("Testing");
writer.println("echo test");
writer.println("set new-console on");
System.out.println("Launching notepad");
writer.println("file C:\\\\Windows\\\\notepad.exe");
writer.println("run");
writer.flush();
System.out.println("Waiting");
Thread.sleep(3000);
System.out.println("Interrupting");
writer.write(3);
writer.println();
writer.flush();
System.out.println("Killing");
writer.println("kill");
writer.flush();
writer.println("y");
writer.flush();
}
@Test
public void testGdbInterruptConPty() throws Exception {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
//BufferedReader reader = loggingReader(parent.getInputStream());
PtySession gdb =
pty.getChild().session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe" }, null);
pump(parent.getInputStream(), System.err);
System.out.println("Testing");
writer.println("echo test");
writer.println("set new-console on");
System.out.println("Launching notepad");
writer.println("file C:\\\\Windows\\\\notepad.exe");
writer.println("run");
writer.flush();
System.out.println("Waiting");
Thread.sleep(3000);
System.out.println("Interrupting");
writer.write(3);
writer.println();
writer.flush();
System.out.println("Killing");
writer.println("kill");
writer.flush();
writer.println("y");
writer.flush();
Thread.sleep(100000);
}
}
@Test
public void testGdbMiConPty() throws Exception {
try (Pty pty = ConPty.openpty()) {
PtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
//BufferedReader reader = loggingReader(parent.getInputStream());
PtySession gdb = pty.getChild()
.session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe", "-i", "mi2" },
null);
InputStream inputStream = parent.getInputStream();
inputStream = new AnsiBufferedInputStream(inputStream);
pump(inputStream, System.out);
writer.println("-interpreter-exec console \"echo test\"");
writer.println("-interpreter-exec console \"quit\"");
writer.flush();
gdb.waitExited();
//System.out.println("Exited");
}
}
}

View file

@ -0,0 +1,68 @@
/* ###
* 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.pty.windows;
import java.io.*;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinNT.HANDLE;
public class NamedPipeTest {
protected Handle checkHandle(HANDLE handle) {
if (Kernel32.INVALID_HANDLE_VALUE.equals(handle)) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
}
return new Handle(handle);
}
// @Test
/**
* Experiment with MinGW GDB, named pipes, and {@code new-ui}.
*
* <p>
* Run this test, start GDB in a command shell, then issue
* "{@code new-ui mi2 \\\\.\\pipe\\GhidraGDB}". With GDB 11.1, GDB will print "New UI allocated"
* to the console, and I'll receive {@code =thread-group-added,id="i1"} on the pipe. However,
* GDB never seems to process my {@code -add-inferior} command. Furthermore, GDB freezes and no
* longer accepts input on the console, either.
*/
public void testExpNamedPipes() throws Exception {
Handle hPipe = checkHandle(Kernel32.INSTANCE.CreateNamedPipe(
"\\\\.\\pipe\\GhidraGDB" /*lpName*/,
Kernel32.PIPE_ACCESS_DUPLEX /*dwOpenMode*/,
Kernel32.PIPE_TYPE_BYTE | Kernel32.PIPE_WAIT /*dwPipeMode*/,
Kernel32.PIPE_UNLIMITED_INSTANCES /*nMaxInstances*/,
1024 /*nOutBufferSize*/,
1024 /*nInBufferSize*/,
0 /*nDefaultTimeOut*/,
null /*lpSecurityAttributes*/));
try (PrintWriter writer = new PrintWriter(new HandleOutputStream(hPipe));
BufferedReader reader =
new BufferedReader(new InputStreamReader(new HandleInputStream(hPipe)))) {
writer.println("-add-inferior");
writer.flush();
String line;
while (null != (line = reader.readLine())) {
System.out.println(line);
}
}
}
}

View file

@ -73,6 +73,27 @@ public class GdbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
}
}
protected class InVmGdbConPtyDebuggerProgramLaunchOffer
extends AbstractGdbDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME =
"agent.gdb.GdbInJvmConPtyDebuggerModelFactory";
public InVmGdbConPtyDebuggerProgramLaunchOffer(Program program, PluginTool tool,
DebuggerModelFactory factory) {
super(program, tool, factory);
}
@Override
public String getConfigName() {
return "IN-VM GDB (Windows)";
}
@Override
public String getMenuTitle() {
return "in GDB locally IN-VM (Windows)";
}
}
protected class GadpGdbDebuggerProgramLaunchOffer
extends AbstractGdbDebuggerProgramLaunchOffer {
private static final String FACTORY_CLS_NAME =
@ -143,6 +164,9 @@ public class GdbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
else if (clsName.equals(SshGdbDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new SshGdbDebuggerProgramLaunchOffer(program, tool, factory));
}
else if (clsName.equals(InVmGdbConPtyDebuggerProgramLaunchOffer.FACTORY_CLS_NAME)) {
offers.add(new InVmGdbConPtyDebuggerProgramLaunchOffer(program, tool, factory));
}
}
return offers;
}

View file

@ -30,7 +30,7 @@ public class GdbX86DebuggerMappingOpinion implements DebuggerMappingOpinion {
protected static final LanguageID LANG_ID_X86 = new LanguageID("x86:LE:32:default");
protected static final LanguageID LANG_ID_X86_64 = new LanguageID("x86:LE:64:default");
protected static final CompilerSpecID COMP_ID_GCC = new CompilerSpecID("gcc");
protected static final CompilerSpecID COMP_ID_VS = new CompilerSpecID("Visual Studio");
protected static final CompilerSpecID COMP_ID_VS = new CompilerSpecID("windows");
protected static class GdbI386X86_64RegisterMapper extends DefaultDebuggerRegisterMapper {
public GdbI386X86_64RegisterMapper(CompilerSpec cSpec,
@ -120,7 +120,7 @@ public class GdbX86DebuggerMappingOpinion implements DebuggerMappingOpinion {
return Set.of(new GdbI386LinuxOffer(process));
}
}
else if (os.contains("Cygwin")) {
else if (os.contains("Cygwin") || os.contains("Windows")) {
if (is64Bit) {
return Set.of(new GdbI386X86_64WindowsOffer(process));
}