Index: packages/Python/lldbsuite/test/functionalities/gdb_remote_client/TestGDBRemoteClient.py =================================================================== --- /dev/null +++ packages/Python/lldbsuite/test/functionalities/gdb_remote_client/TestGDBRemoteClient.py @@ -0,0 +1,12 @@ +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test.decorators import * +from gdbclientutils import * + + +class TestGDBRemoteClient(GDBRemoteTestBase): + + def test_connect(self): + """Test connecting to a remote gdb server""" + target = self.createTarget("a.yaml") + process = self.connect(target) Index: packages/Python/lldbsuite/test/functionalities/gdb_remote_client/a.yaml =================================================================== --- /dev/null +++ packages/Python/lldbsuite/test/functionalities/gdb_remote_client/a.yaml @@ -0,0 +1,34 @@ +!ELF +FileHeader: + Class: ELFCLASS32 + Data: ELFDATA2LSB + Type: ET_EXEC + Machine: EM_ARM +Sections: + - Name: .text + Type: SHT_PROGBITS + Flags: [ SHF_ALLOC, SHF_EXECINSTR ] + Address: 0x1000 + AddressAlign: 0x4 + Content: "c3c3c3c3" + - Name: .data + Type: SHT_PROGBITS + Flags: [ SHF_ALLOC ] + Address: 0x2000 + AddressAlign: 0x4 + Content: "3232" +ProgramHeaders: + - Type: PT_LOAD + Flags: [ PF_X, PF_R ] + VAddr: 0x1000 + PAddr: 0x1000 + Align: 0x4 + Sections: + - Section: .text + - Type: PT_LOAD + Flags: [ PF_R, PF_W ] + VAddr: 0x2000 + PAddr: 0x1004 + Align: 0x4 + Sections: + - Section: .data Index: packages/Python/lldbsuite/test/functionalities/gdb_remote_client/gdbclientutils.py =================================================================== --- /dev/null +++ packages/Python/lldbsuite/test/functionalities/gdb_remote_client/gdbclientutils.py @@ -0,0 +1,432 @@ +import os +import os.path +import subprocess +import threading +import socket +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbtest_config + + +def yaml2obj_executable(): + """ + Get the path to the yaml2obj executable, which can be used to create test + object files from easy to write yaml instructions. + + Throws an Exception if the executable cannot be found. + """ + # Tries to find yaml2obj at the same folder as the lldb + path = os.path.join(os.path.dirname(lldbtest_config.lldbExec), "yaml2obj") + if os.path.exists(path): + return path + raise Exception("yaml2obj executable not found") + + +def yaml2elf(yaml_path, elf_path): + """ + Create an ELF file at the given path from a yaml file at the given path. + + Throws a subprocess.CalledProcessError if the ELF could not be created. + """ + yaml2obj = yaml2obj_executable() + command = [yaml2obj, "-o=%s" % elf_path, yaml_path] + system([command]) + + +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) + while i < hex_len - 1: + out += chr(int(hex_bytes[i:i + 2]), 16) + i += 2 + return out + + +class MockGDBServerResponder: + """ + A base class for handing 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 + + 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 == "g": + return self.readRegisters() + if packet[0] == "G": + return self.writeRegisters(packet[1:]) + if packet[0] == "p": + return self.readRegister(int(packet[1:], 16)) + if packet[0] == "P": + register, value = packet[1:].split("=") + return self.readRegister(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 == "qC": + return self.qC() + if packet == "?": + return self.haltReason() + if packet[0] == "H": + return self.selectThread(packet[1], int(packet[2:], 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 "" + return self.other(packet) + + def readRegisters(self): + return "xxxxxxxx" * self.registerCount + + def readRegister(self, register): + return "xxxxxxxx" + + 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 "PacketSize=3fff;QStartNoAckMode+" + + def qfThreadInfo(self): + return "l" + + def qC(self): + return "QC0" + + 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 selectThread(self, op, thread_id): + return "OK" + + def other(self, packet): + # empty string means unsupported + return "" + + +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 + port = 0 + _socket = None + _client = None + _thread = None + _incomingPacket = None + _incomingChecksum = None + _shouldSendAck = True + _isExpectingAck = False + + def __init__(self, port = 0): + self.responder = MockGDBServerResponder() + self.port = port + self._socket = socket.socket() + + def start(self): + # Block until the socket is up, so self.port is available immediately. + # Then start a thread that waits for a client connection. + addr = ("127.0.0.1", self.port) + self._socket.bind(addr) + self.port = self._socket.getsockname()[1] + self._socket.listen(0) + self._thread = threading.Thread(target=self._run) + self._thread.start() + + def stop(self): + if self._client is not None: + self._client.shutdown(socket.SHUT_RDWR) + self._client.close() + # Would call self._socket.shutdown, but it blocks forever for some + # unknown reason. close() works just fine. + self._socket.close() + self._thread.join() + self._thread = None + + def _run(self): + # For testing purposes, we only need to worry about one client + # connecting just one time. + try: + # accept() is stubborn and won't fail even when the socket is + # shutdown, so we'll use a timeout + self._socket.settimeout(2.0) + client, client_addr = self._socket.accept() + self._client = client + # The connected client inherits its timeout from self._socket, + # but we'll use a blocking socket for the client + self._client.settimeout(None) + except: + return + self._shouldSendAck = True + self._isExpectingAck = False + data = None + while True: + try: + data = self._client.recv(4096) + if data is None or len(data) == 0: + break + except Exception as e: + self._client.close() + break + self._receive(data) + + def _receive(self, data): + i = 0 + data_len = len(data) + while i < data_len: + # If we haven't set _incomingPacket to anything yet, it means we're + # expecting the start of a new packet. + if self._incomingPacket is None: + if data[i] == '+': + if self._isExpectingAck: + # We're expecting an ack from the client, so ignore it. + self._isExpectingAck = False + else: + # Not expecting an ack, so just ack back. + self._client.sendall('+') + i += 1 + elif data[i] == '$': + self._incomingPacket = "" + i += 1 + else: + # Unexpected byte, closing connection to indicate error. + self._client.close() + return + # If we haven't set _incomingChecksum to anything yet, it means + # we're collecting bytes, waiting for a # to indicate the end of + # packet data. + elif self._incomingChecksum is None: + while i < data_len: + if data[i] == '#': + self._incomingChecksum = "" + i += 1 + break + self._incomingPacket += data[i] + i += 1 + # If we have set _incomingChecksum, then we're collecting the + # two bytes of the checksum string. + else: + while i < data_len and len(self._incomingChecksum) < 2: + self._incomingChecksum += data[i] + i += 1 + if len(self._incomingChecksum) == 2: + check = None + try: + check = int(self._incomingChecksum, 16) + except ValueError: + # Non-hex checksum, closing connection. + self._client.close() + return + if check != checksum(self._incomingPacket): + # Mismatching checksums, closing connection. + # Since we're using TCP transport, the checksum can + # only be wrong if the client did something wrong. + self._client.close() + return + packet = self._incomingPacket + self._incomingPacket = None + self._incomingChecksum = None + self._handlePacket(packet) + + def _handlePacket(self, packet): + 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 quicly anyway. + if self._shouldSendAck: + self._client.sendall('+') + self._isExpectingAck = True + 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) + # Handle packet framing since we don't want to bother tests with it. + framed = frame_packet(response) + self._client.sendall(framed) + + +class GDBRemoteTestBase(TestBase): + """ + Base class for GDB client tests. + + This class will setup and start a mock GDB server for the test to use. + It also provides assertPacketLogContains, which simplifies the checking + of packets sent by the client. + """ + + NO_DEBUG_INFO_TESTCASE = True + mydir = TestBase.compute_mydir(__file__) + server = None + temp_files = None + + def setUp(self): + TestBase.setUp(self) + self.temp_files = [] + self.server = MockGDBServer() + self.server.start() + + def tearDown(self): + for temp_file in self.temp_files: + self.RemoveTempFile(temp_file) + self.server.stop() + self.temp_files = [] + TestBase.tearDown(self) + + def createTarget(self, yaml_path): + """ + Create an ELF target by auto-generating the ELF based on the given yaml + instructions. + + This will track the generated ELF so it can be automatically removed + during tearDown. + """ + yaml_base, ext = os.path.splitext(yaml_path) + elf_path = "%s.elf" % yaml_base + yaml2elf(yaml_path, elf_path) + self.temp_files.append(elf_path) + return self.dbg.CreateTarget(elf_path) + + def connect(self, target): + """ + Create a process by connecting to the mock GDB server. + + Includes assertions that the process was successfully created. + """ + listener = self.dbg.GetListener() + error = lldb.SBError() + url = "connect://localhost:%d" % self.server.port + process = target.ConnectRemote(listener, url, "gdb-remote", error) + self.assertTrue(error.Success(), error.description) + self.assertTrue(process, PROCESS_IS_VALID) + + def assertPacketLogContains(self, packets): + """ + Assert that the mock server's packet log contains the given packets. + + The packet log includes all packets sent by the client and received + by the server. This fuction makes it easy to verify that the client + sent the expected packets to the server. + + The check does not require that the packets be consecutive, but does + require that they are ordered in the log as they ordered in the arg. + """ + i = 0 + j = 0 + log = self.server.responder.packetLog + while i < len(packets) and j < len(log): + if log[j] == packets[i]: + i += 1 + j += 1 + if i < len(packets): + self.fail("Did not receive: %s\n\t%s" % (packets[i], + '\n\t'.join(log[-10:])))