| import ctypes |
| import errno |
| import io |
| import threading |
| import socket |
| import traceback |
| from lldbsuite.support import seven |
| |
| |
| def checksum(message): |
| """ |
| Calculate the GDB server protocol checksum of the message. |
| |
| The GDB server protocol uses a simple modulo 256 sum. |
| """ |
| check = 0 |
| for c in message: |
| check += ord(c) |
| return check % 256 |
| |
| |
| def frame_packet(message): |
| """ |
| Create a framed packet that's ready to send over the GDB connection |
| channel. |
| |
| Framing includes surrounding the message between $ and #, and appending |
| a two character hex checksum. |
| """ |
| return "$%s#%02x" % (message, checksum(message)) |
| |
| |
| def escape_binary(message): |
| """ |
| Escape the binary message using the process described in the GDB server |
| protocol documentation. |
| |
| Most bytes are sent through as-is, but $, #, and { are escaped by writing |
| a { followed by the original byte mod 0x20. |
| """ |
| out = "" |
| for c in message: |
| d = ord(c) |
| if d in (0x23, 0x24, 0x7D): |
| out += chr(0x7D) |
| out += chr(d ^ 0x20) |
| else: |
| out += c |
| return out |
| |
| |
| def hex_encode_bytes(message): |
| """ |
| Encode the binary message by converting each byte into a two-character |
| hex string. |
| """ |
| out = "" |
| for c in message: |
| out += "%02x" % ord(c) |
| return out |
| |
| |
| def hex_decode_bytes(hex_bytes): |
| """ |
| Decode the hex string into a binary message by converting each two-character |
| hex string into a single output byte. |
| """ |
| out = "" |
| hex_len = len(hex_bytes) |
| i = 0 |
| while i < hex_len - 1: |
| out += chr(int(hex_bytes[i : i + 2], 16)) |
| i += 2 |
| return out |
| |
| |
| class MockGDBServerResponder: |
| """ |
| A base class for handling client packets and issuing server responses for |
| GDB tests. |
| |
| This handles many typical situations, while still allowing subclasses to |
| completely customize their responses. |
| |
| Most subclasses will be interested in overriding the other() method, which |
| handles any packet not recognized in the common packet handling code. |
| """ |
| |
| registerCount = 40 |
| packetLog = None |
| |
| class RESPONSE_DISCONNECT: |
| pass |
| |
| def __init__(self): |
| self.packetLog = [] |
| |
| def respond(self, packet): |
| """ |
| Return the unframed packet data that the server should issue in response |
| to the given packet received from the client. |
| """ |
| self.packetLog.append(packet) |
| if packet is MockGDBServer.PACKET_INTERRUPT: |
| return self.interrupt() |
| if packet == "c": |
| return self.cont() |
| if packet.startswith("vCont;c"): |
| return self.vCont(packet) |
| if packet[0] == "A": |
| return self.A(packet) |
| if packet[0] == "D": |
| return self.D(packet) |
| if packet[0] == "g": |
| return self.readRegisters() |
| if packet[0] == "G": |
| # Gxxxxxxxxxxx |
| # Gxxxxxxxxxxx;thread:1234; |
| return self.writeRegisters(packet[1:].split(";")[0]) |
| if packet[0] == "p": |
| regnum = packet[1:].split(";")[0] |
| return self.readRegister(int(regnum, 16)) |
| if packet[0] == "P": |
| register, value = packet[1:].split("=") |
| return self.writeRegister(int(register, 16), value) |
| if packet[0] == "m": |
| addr, length = [int(x, 16) for x in packet[1:].split(",")] |
| return self.readMemory(addr, length) |
| if packet[0] == "M": |
| location, encoded_data = packet[1:].split(":") |
| addr, length = [int(x, 16) for x in location.split(",")] |
| return self.writeMemory(addr, encoded_data) |
| if packet[0:7] == "qSymbol": |
| return self.qSymbol(packet[8:]) |
| if packet[0:10] == "qSupported": |
| return self.qSupported(packet[11:].split(";")) |
| if packet == "qfThreadInfo": |
| return self.qfThreadInfo() |
| if packet == "qsThreadInfo": |
| return self.qsThreadInfo() |
| if packet == "qC": |
| return self.qC() |
| if packet == "QEnableErrorStrings": |
| return self.QEnableErrorStrings() |
| if packet == "?": |
| return self.haltReason() |
| if packet == "s": |
| return self.haltReason() |
| if packet[0] == "H": |
| tid = packet[2:] |
| if "." in tid: |
| assert tid.startswith("p") |
| # TODO: do we want to do anything with PID? |
| tid = tid.split(".", 1)[1] |
| return self.selectThread(packet[1], int(tid, 16)) |
| if packet[0:6] == "qXfer:": |
| obj, read, annex, location = packet[6:].split(":") |
| offset, length = [int(x, 16) for x in location.split(",")] |
| data, has_more = self.qXferRead(obj, annex, offset, length) |
| if data is not None: |
| return self._qXferResponse(data, has_more) |
| return "" |
| if packet.startswith("vAttach;"): |
| pid = packet.partition(";")[2] |
| return self.vAttach(int(pid, 16)) |
| if packet[0] == "Z": |
| return self.setBreakpoint(packet) |
| if packet.startswith("qThreadStopInfo"): |
| threadnum = int(packet[15:], 16) |
| return self.threadStopInfo(threadnum) |
| if packet == "QThreadSuffixSupported": |
| return self.QThreadSuffixSupported() |
| if packet == "QListThreadsInStopReply": |
| return self.QListThreadsInStopReply() |
| if packet.startswith("qMemoryRegionInfo:"): |
| return self.qMemoryRegionInfo(int(packet.split(":")[1], 16)) |
| if packet == "qQueryGDBServer": |
| return self.qQueryGDBServer() |
| if packet == "qHostInfo": |
| return self.qHostInfo() |
| if packet == "qGetWorkingDir": |
| return self.qGetWorkingDir() |
| if packet == "qOffsets": |
| return self.qOffsets() |
| if packet == "qProcessInfo": |
| return self.qProcessInfo() |
| if packet == "qsProcessInfo": |
| return self.qsProcessInfo() |
| if packet.startswith("qfProcessInfo"): |
| return self.qfProcessInfo(packet) |
| if packet.startswith("jGetLoadedDynamicLibrariesInfos"): |
| return self.jGetLoadedDynamicLibrariesInfos(packet) |
| if packet.startswith("qPathComplete:"): |
| return self.qPathComplete() |
| if packet.startswith("vFile:"): |
| return self.vFile(packet) |
| if packet.startswith("vRun;"): |
| return self.vRun(packet) |
| if packet.startswith("qLaunchGDBServer;"): |
| _, host = packet.partition(";")[2].split(":") |
| return self.qLaunchGDBServer(host) |
| if packet.startswith("qLaunchSuccess"): |
| return self.qLaunchSuccess() |
| if packet.startswith("QEnvironment:"): |
| return self.QEnvironment(packet) |
| if packet.startswith("QEnvironmentHexEncoded:"): |
| return self.QEnvironmentHexEncoded(packet) |
| if packet.startswith("qRegisterInfo"): |
| regnum = int(packet[len("qRegisterInfo") :], 16) |
| return self.qRegisterInfo(regnum) |
| if packet == "k": |
| return self.k() |
| |
| return self.other(packet) |
| |
| def qsProcessInfo(self): |
| return "E04" |
| |
| def qfProcessInfo(self, packet): |
| return "E04" |
| |
| def jGetLoadedDynamicLibrariesInfos(self, packet): |
| return "" |
| |
| def qGetWorkingDir(self): |
| return "2f" |
| |
| def qOffsets(self): |
| return "" |
| |
| def qProcessInfo(self): |
| return "" |
| |
| def qHostInfo(self): |
| return "ptrsize:8;endian:little;" |
| |
| def qQueryGDBServer(self): |
| return "E04" |
| |
| def interrupt(self): |
| raise self.UnexpectedPacketException() |
| |
| def cont(self): |
| raise self.UnexpectedPacketException() |
| |
| def vCont(self, packet): |
| raise self.UnexpectedPacketException() |
| |
| def A(self, packet): |
| return "" |
| |
| def D(self, packet): |
| return "OK" |
| |
| def readRegisters(self): |
| return "00000000" * self.registerCount |
| |
| def readRegister(self, register): |
| return "00000000" |
| |
| def writeRegisters(self, registers_hex): |
| return "OK" |
| |
| def writeRegister(self, register, value_hex): |
| return "OK" |
| |
| def readMemory(self, addr, length): |
| return "00" * length |
| |
| def writeMemory(self, addr, data_hex): |
| return "OK" |
| |
| def qSymbol(self, symbol_args): |
| return "OK" |
| |
| def qSupported(self, client_supported): |
| return "qXfer:features:read+;PacketSize=3fff;QStartNoAckMode+" |
| |
| def qfThreadInfo(self): |
| return "l" |
| |
| def qsThreadInfo(self): |
| return "l" |
| |
| def qC(self): |
| return "QC0" |
| |
| def QEnableErrorStrings(self): |
| return "OK" |
| |
| def haltReason(self): |
| # SIGINT is 2, return type is 2 digit hex string |
| return "S02" |
| |
| def qXferRead(self, obj, annex, offset, length): |
| return None, False |
| |
| def _qXferResponse(self, data, has_more): |
| return "%s%s" % ("m" if has_more else "l", escape_binary(data)) |
| |
| def vAttach(self, pid): |
| raise self.UnexpectedPacketException() |
| |
| def selectThread(self, op, thread_id): |
| return "OK" |
| |
| def setBreakpoint(self, packet): |
| raise self.UnexpectedPacketException() |
| |
| def threadStopInfo(self, threadnum): |
| return "" |
| |
| def other(self, packet): |
| # empty string means unsupported |
| return "" |
| |
| def QThreadSuffixSupported(self): |
| return "" |
| |
| def QListThreadsInStopReply(self): |
| return "" |
| |
| def qMemoryRegionInfo(self, addr): |
| return "" |
| |
| def qPathComplete(self): |
| return "" |
| |
| def vFile(self, packet): |
| return "" |
| |
| def vRun(self, packet): |
| return "" |
| |
| def qLaunchGDBServer(self, host): |
| raise self.UnexpectedPacketException() |
| |
| def qLaunchSuccess(self): |
| return "" |
| |
| def QEnvironment(self, packet): |
| return "OK" |
| |
| def QEnvironmentHexEncoded(self, packet): |
| return "OK" |
| |
| def qRegisterInfo(self, num): |
| return "" |
| |
| def k(self): |
| return ["W01", self.RESPONSE_DISCONNECT] |
| |
| """ |
| Raised when we receive a packet for which there is no default action. |
| Override the responder class to implement behavior suitable for the test at |
| hand. |
| """ |
| |
| class UnexpectedPacketException(Exception): |
| pass |
| |
| |
| class ServerChannel: |
| """ |
| A wrapper class for TCP or pty-based server. |
| """ |
| |
| def get_connect_address(self): |
| """Get address for the client to connect to.""" |
| |
| def get_connect_url(self): |
| """Get URL suitable for process connect command.""" |
| |
| def close_server(self): |
| """Close all resources used by the server.""" |
| |
| def accept(self): |
| """Accept a single client connection to the server.""" |
| |
| def close_connection(self): |
| """Close all resources used by the accepted connection.""" |
| |
| def recv(self): |
| """Receive a data packet from the connected client.""" |
| |
| def sendall(self, data): |
| """Send the data to the connected client.""" |
| |
| |
| class ServerSocket(ServerChannel): |
| def __init__(self, family, type, proto, addr): |
| self._server_socket = socket.socket(family, type, proto) |
| self._connection = None |
| |
| self._server_socket.bind(addr) |
| self._server_socket.listen(1) |
| |
| def close_server(self): |
| self._server_socket.close() |
| |
| def accept(self): |
| assert self._connection is None |
| # accept() is stubborn and won't fail even when the socket is |
| # shutdown, so we'll use a timeout |
| self._server_socket.settimeout(30.0) |
| client, client_addr = self._server_socket.accept() |
| # The connected client inherits its timeout from self._socket, |
| # but we'll use a blocking socket for the client |
| client.settimeout(None) |
| self._connection = client |
| |
| def close_connection(self): |
| assert self._connection is not None |
| self._connection.close() |
| self._connection = None |
| |
| def recv(self): |
| assert self._connection is not None |
| return self._connection.recv(4096) |
| |
| def sendall(self, data): |
| assert self._connection is not None |
| return self._connection.sendall(data) |
| |
| |
| class TCPServerSocket(ServerSocket): |
| def __init__(self): |
| family, type, proto, _, addr = socket.getaddrinfo( |
| "localhost", 0, proto=socket.IPPROTO_TCP |
| )[0] |
| super().__init__(family, type, proto, addr) |
| |
| def get_connect_address(self): |
| return "[{}]:{}".format(*self._server_socket.getsockname()) |
| |
| def get_connect_url(self): |
| return "connect://" + self.get_connect_address() |
| |
| |
| class UnixServerSocket(ServerSocket): |
| def __init__(self, addr): |
| super().__init__(socket.AF_UNIX, socket.SOCK_STREAM, 0, addr) |
| |
| def get_connect_address(self): |
| return self._server_socket.getsockname() |
| |
| def get_connect_url(self): |
| return "unix-connect://" + self.get_connect_address() |
| |
| |
| class PtyServerSocket(ServerChannel): |
| def __init__(self): |
| import pty |
| import tty |
| |
| primary, secondary = pty.openpty() |
| tty.setraw(primary) |
| self._primary = io.FileIO(primary, "r+b") |
| self._secondary = io.FileIO(secondary, "r+b") |
| |
| def get_connect_address(self): |
| libc = ctypes.CDLL(None) |
| libc.ptsname.argtypes = (ctypes.c_int,) |
| libc.ptsname.restype = ctypes.c_char_p |
| return libc.ptsname(self._primary.fileno()).decode() |
| |
| def get_connect_url(self): |
| return "serial://" + self.get_connect_address() |
| |
| def close_server(self): |
| self._secondary.close() |
| self._primary.close() |
| |
| def recv(self): |
| try: |
| return self._primary.read(4096) |
| except OSError as e: |
| # closing the pty results in EIO on Linux, convert it to EOF |
| if e.errno == errno.EIO: |
| return b"" |
| raise |
| |
| def sendall(self, data): |
| return self._primary.write(data) |
| |
| |
| class MockGDBServer: |
| """ |
| A simple TCP-based GDB server that can test client behavior by receiving |
| commands and issuing custom-tailored responses. |
| |
| Responses are generated via the .responder property, which should be an |
| instance of a class based on MockGDBServerResponder. |
| """ |
| |
| responder = None |
| _socket = None |
| _thread = None |
| _receivedData = None |
| _receivedDataOffset = None |
| _shouldSendAck = True |
| |
| def __init__(self, socket): |
| self._socket = socket |
| self.responder = MockGDBServerResponder() |
| |
| def start(self): |
| # Start a thread that waits for a client connection. |
| self._thread = threading.Thread(target=self.run) |
| self._thread.start() |
| |
| def stop(self): |
| self._thread.join() |
| self._thread = None |
| |
| def get_connect_address(self): |
| return self._socket.get_connect_address() |
| |
| def get_connect_url(self): |
| return self._socket.get_connect_url() |
| |
| def run(self): |
| # For testing purposes, we only need to worry about one client |
| # connecting just one time. |
| try: |
| self._socket.accept() |
| except: |
| traceback.print_exc() |
| return |
| self._shouldSendAck = True |
| self._receivedData = "" |
| self._receivedDataOffset = 0 |
| data = None |
| try: |
| while True: |
| data = seven.bitcast_to_string(self._socket.recv()) |
| if data is None or len(data) == 0: |
| break |
| self._receive(data) |
| except self.TerminateConnectionException: |
| pass |
| except Exception as e: |
| print( |
| "An exception happened when receiving the response from the gdb server. Closing the client..." |
| ) |
| traceback.print_exc() |
| finally: |
| self._socket.close_connection() |
| self._socket.close_server() |
| |
| def _receive(self, data): |
| """ |
| Collects data, parses and responds to as many packets as exist. |
| Any leftover data is kept for parsing the next time around. |
| """ |
| self._receivedData += data |
| packet = self._parsePacket() |
| while packet is not None: |
| self._handlePacket(packet) |
| packet = self._parsePacket() |
| |
| def _parsePacket(self): |
| """ |
| Reads bytes from self._receivedData, returning: |
| - a packet's contents if a valid packet is found |
| - the PACKET_ACK unique object if we got an ack |
| - None if we only have a partial packet |
| |
| Raises an InvalidPacketException if unexpected data is received |
| or if checksums fail. |
| |
| Once a complete packet is found at the front of self._receivedData, |
| its data is removed form self._receivedData. |
| """ |
| data = self._receivedData |
| i = self._receivedDataOffset |
| data_len = len(data) |
| if data_len == 0: |
| return None |
| if i == 0: |
| # If we're looking at the start of the received data, that means |
| # we're looking for the start of a new packet, denoted by a $. |
| # It's also possible we'll see an ACK here, denoted by a + |
| if data[0] == "+": |
| self._receivedData = data[1:] |
| return self.PACKET_ACK |
| if ord(data[0]) == 3: |
| self._receivedData = data[1:] |
| return self.PACKET_INTERRUPT |
| if data[0] == "$": |
| i += 1 |
| else: |
| raise self.InvalidPacketException( |
| "Unexpected leading byte: %s" % data[0] |
| ) |
| |
| # If we're looking beyond the start of the received data, then we're |
| # looking for the end of the packet content, denoted by a #. |
| # Note that we pick up searching from where we left off last time |
| while i < data_len and data[i] != "#": |
| i += 1 |
| |
| # If there isn't enough data left for a checksum, just remember where |
| # we left off so we can pick up there the next time around |
| if i > data_len - 3: |
| self._receivedDataOffset = i |
| return None |
| |
| # If we have enough data remaining for the checksum, extract it and |
| # compare to the packet contents |
| packet = data[1:i] |
| i += 1 |
| try: |
| check = int(data[i : i + 2], 16) |
| except ValueError: |
| raise self.InvalidPacketException("Checksum is not valid hex") |
| i += 2 |
| if check != checksum(packet): |
| raise self.InvalidPacketException( |
| "Checksum %02x does not match content %02x" % (check, checksum(packet)) |
| ) |
| # remove parsed bytes from _receivedData and reset offset so parsing |
| # can start on the next packet the next time around |
| self._receivedData = data[i:] |
| self._receivedDataOffset = 0 |
| return packet |
| |
| def _sendPacket(self, packet): |
| self._socket.sendall(seven.bitcast_to_bytes(frame_packet(packet))) |
| |
| def _handlePacket(self, packet): |
| if packet is self.PACKET_ACK: |
| # Ignore ACKs from the client. For the future, we can consider |
| # adding validation code to make sure the client only sends ACKs |
| # when it's supposed to. |
| return |
| response = "" |
| # We'll handle the ack stuff here since it's not something any of the |
| # tests will be concerned about, and it'll get turned off quickly anyway. |
| if self._shouldSendAck: |
| self._socket.sendall(seven.bitcast_to_bytes("+")) |
| if packet == "QStartNoAckMode": |
| self._shouldSendAck = False |
| response = "OK" |
| elif self.responder is not None: |
| # Delegate everything else to our responder |
| response = self.responder.respond(packet) |
| if not isinstance(response, list): |
| response = [response] |
| for part in response: |
| if part is MockGDBServerResponder.RESPONSE_DISCONNECT: |
| raise self.TerminateConnectionException() |
| self._sendPacket(part) |
| |
| PACKET_ACK = object() |
| PACKET_INTERRUPT = object() |
| |
| class TerminateConnectionException(Exception): |
| pass |
| |
| class InvalidPacketException(Exception): |
| pass |