diff options
Diffstat (limited to 'gdb/python/lib')
-rw-r--r-- | gdb/python/lib/gdb/dap/__init__.py | 69 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/breakpoint.py | 143 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/bt.py | 93 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/disassemble.py | 51 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/evaluate.py | 42 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/events.py | 166 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/frames.py | 57 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/io.py | 67 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/launch.py | 39 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/next.py | 51 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/pause.py | 23 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/scopes.py | 65 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/server.py | 205 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/startup.py | 189 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/state.py | 25 | ||||
-rw-r--r-- | gdb/python/lib/gdb/dap/threads.py | 42 |
16 files changed, 1327 insertions, 0 deletions
diff --git a/gdb/python/lib/gdb/dap/__init__.py b/gdb/python/lib/gdb/dap/__init__.py new file mode 100644 index 0000000..0df9386 --- /dev/null +++ b/gdb/python/lib/gdb/dap/__init__.py @@ -0,0 +1,69 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import gdb + +# This must come before other DAP imports. +from . import startup + +# Load modules that define commands. +from . import breakpoint +from . import bt +from . import disassemble +from . import evaluate +from . import launch +from . import next +from . import pause +from . import scopes +from . import threads + +from .server import Server + + +def run(): + """Main entry point for the DAP server. + This is called by the GDB DAP interpreter.""" + startup.exec_and_log("set python print-stack full") + startup.exec_and_log("set pagination off") + + # We want to control gdb stdin and stdout entirely, so we dup + # them to new file descriptors. + saved_out = os.dup(1) + saved_in = os.dup(0) + # Make sure these are not inheritable. This is already the case + # for Unix, but not for Windows. + os.set_inheritable(saved_out, False) + os.set_inheritable(saved_in, False) + + # The new gdb (and inferior) stdin will just be /dev/null. For + # gdb, the "dap" interpreter also rewires the UI so that gdb + # doesn't try to read this (and thus see EOF and exit). + new_in = os.open(os.devnull, os.O_RDONLY) + os.dup2(new_in, 0, True) + os.close(new_in) + + # Make the new stdout be a pipe. This way the DAP code can easily + # read from the inferior and send OutputEvent to the client. + (rfd, wfd) = os.pipe() + os.set_inheritable(rfd, False) + os.dup2(wfd, 1, True) + # Also send stderr this way. + os.dup2(wfd, 2, True) + os.close(wfd) + + # Note the inferior output is opened in text mode. + server = Server(open(saved_in, "rb"), open(saved_out, "wb"), open(rfd, "r")) + startup.start_dap(server.main_loop) diff --git a/gdb/python/lib/gdb/dap/breakpoint.py b/gdb/python/lib/gdb/dap/breakpoint.py new file mode 100644 index 0000000..502beb0 --- /dev/null +++ b/gdb/python/lib/gdb/dap/breakpoint.py @@ -0,0 +1,143 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import gdb +import os + +from .server import request, capability +from .startup import send_gdb_with_response, in_gdb_thread + + +# Map from the breakpoint "kind" (like "function") to a second map, of +# breakpoints of that type. The second map uses the breakpoint spec +# as a key, and the gdb.Breakpoint itself as a value. This is used to +# implement the clearing behavior specified by the protocol, while +# allowing for reuse when a breakpoint can be kept. +breakpoint_map = {} + + +@in_gdb_thread +def breakpoint_descriptor(bp): + "Return the Breakpoint object descriptor given a gdb Breakpoint." + if bp.locations: + # Just choose the first location, because DAP doesn't allow + # multiple locations. See + # https://github.com/microsoft/debug-adapter-protocol/issues/13 + loc = bp.locations[0] + (basename, line) = loc.source + return { + "id": bp.number, + "verified": True, + "source": { + "name": os.path.basename(basename), + "path": loc.fullname, + # We probably don't need this but it doesn't hurt to + # be explicit. + "sourceReference": 0, + }, + "line": line, + "instructionReference": hex(loc.address), + } + else: + return { + "id": bp.number, + "verified": False, + } + + +# Helper function to set some breakpoints according to a list of +# specifications. +@in_gdb_thread +def _set_breakpoints(kind, specs): + global breakpoint_map + # Try to reuse existing breakpoints if possible. + if kind in breakpoint_map: + saved_map = breakpoint_map[kind] + else: + saved_map = {} + breakpoint_map[kind] = {} + result = [] + for spec in specs: + keyspec = frozenset(spec.items()) + if keyspec in saved_map: + bp = saved_map.pop(keyspec) + else: + # FIXME handle exceptions here + bp = gdb.Breakpoint(**spec) + breakpoint_map[kind][keyspec] = bp + result.append(breakpoint_descriptor(bp)) + # Delete any breakpoints that were not reused. + for entry in saved_map.values(): + entry.delete() + return result + + +@request("setBreakpoints") +def set_breakpoint(source, *, breakpoints=[], **args): + if "path" not in source: + result = [] + else: + specs = [] + for obj in breakpoints: + specs.append( + { + "source": source["path"], + "line": obj["line"], + } + ) + # Be sure to include the path in the key, so that we only + # clear out breakpoints coming from this same source. + key = "source:" + source["path"] + result = send_gdb_with_response(lambda: _set_breakpoints(key, specs)) + return { + "breakpoints": result, + } + + +@request("setFunctionBreakpoints") +@capability("supportsFunctionBreakpoints") +def set_fn_breakpoint(breakpoints, **args): + specs = [] + for bp in breakpoints: + specs.append( + { + "function": bp["name"], + } + ) + result = send_gdb_with_response(lambda: _set_breakpoints("function", specs)) + return { + "breakpoints": result, + } + + +@request("setInstructionBreakpoints") +@capability("supportsInstructionBreakpoints") +def set_insn_breakpoints(*, breakpoints, offset=None, **args): + specs = [] + for bp in breakpoints: + # There's no way to set an explicit address breakpoint + # from Python, so we rely on "spec" instead. + val = "*" + bp["instructionReference"] + if offset is not None: + val = val + " + " + str(offset) + specs.append( + { + "spec": val, + } + ) + result = send_gdb_with_response(lambda: _set_breakpoints("instruction", specs)) + return { + "breakpoints": result, + } diff --git a/gdb/python/lib/gdb/dap/bt.py b/gdb/python/lib/gdb/dap/bt.py new file mode 100644 index 0000000..990ab13 --- /dev/null +++ b/gdb/python/lib/gdb/dap/bt.py @@ -0,0 +1,93 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import gdb +import os + +from .frames import frame_id +from .server import request, capability +from .startup import send_gdb_with_response, in_gdb_thread +from .state import set_thread + + +# Helper function to safely get the name of a frame as a string. +@in_gdb_thread +def _frame_name(frame): + name = frame.name() + if name is None: + name = "???" + return name + + +# Helper function to get a frame's SAL without an error. +@in_gdb_thread +def _safe_sal(frame): + try: + return frame.find_sal() + except gdb.error: + return None + + +# Helper function to compute a stack trace. +@in_gdb_thread +def _backtrace(thread_id, levels, startFrame): + set_thread(thread_id) + frames = [] + current_number = 0 + # FIXME could invoke frame filters here. + try: + current_frame = gdb.newest_frame() + except gdb.error: + current_frame = None + # Note that we always iterate over all frames, which is lame, but + # seemingly necessary to support the totalFrames response. + # FIXME maybe the mildly mysterious note about "monotonically + # increasing totalFrames values" would let us fix this. + while current_frame is not None: + # This condition handles the startFrame==0 case as well. + if current_number >= startFrame and (levels == 0 or len(frames) < levels): + newframe = { + "id": frame_id(current_frame), + "name": _frame_name(current_frame), + # This must always be supplied, but we will set it + # correctly later if that is possible. + "line": 0, + # GDB doesn't support columns. + "column": 0, + "instructionPointerReference": hex(current_frame.pc()), + } + sal = _safe_sal(current_frame) + if sal is not None: + newframe["source"] = { + "name": os.path.basename(sal.symtab.filename), + "path": sal.symtab.filename, + # We probably don't need this but it doesn't hurt + # to be explicit. + "sourceReference": 0, + } + newframe["line"] = sal.line + frames.append(newframe) + current_number = current_number + 1 + current_frame = current_frame.older() + return { + "stackFrames": frames, + "totalFrames": current_number, + } + + +@request("stackTrace") +@capability("supportsDelayedStackTraceLoading") +def stacktrace(*, levels=0, startFrame=0, threadId, **extra): + return send_gdb_with_response(lambda: _backtrace(threadId, levels, startFrame)) diff --git a/gdb/python/lib/gdb/dap/disassemble.py b/gdb/python/lib/gdb/dap/disassemble.py new file mode 100644 index 0000000..3d3b3a5 --- /dev/null +++ b/gdb/python/lib/gdb/dap/disassemble.py @@ -0,0 +1,51 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import gdb + +from .server import request, capability +from .startup import send_gdb_with_response, in_gdb_thread + + +@in_gdb_thread +def _disassemble(pc, skip_insns, count): + try: + arch = gdb.selected_frame().architecture() + except gdb.error: + # Maybe there was no frame. + arch = gdb.selected_inferior().architecture() + result = [] + total_count = skip_insns + count + for elt in arch.disassemble(pc, count=total_count)[skip_insns:]: + result.append( + { + "address": hex(elt["addr"]), + "instruction": elt["asm"], + } + ) + return { + "instructions": result, + } + + +@request("disassemble") +@capability("supportsDisassembleRequest") +def disassemble( + *, memoryReference, offset=0, instructionOffset=0, instructionCount, **extra +): + pc = int(memoryReference, 0) + offset + return send_gdb_with_response( + lambda: _disassemble(pc, instructionOffset, instructionCount) + ) diff --git a/gdb/python/lib/gdb/dap/evaluate.py b/gdb/python/lib/gdb/dap/evaluate.py new file mode 100644 index 0000000..c05e62d --- /dev/null +++ b/gdb/python/lib/gdb/dap/evaluate.py @@ -0,0 +1,42 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import gdb + +from .frames import frame_for_id +from .server import request +from .startup import send_gdb_with_response, in_gdb_thread + + +# Helper function to evaluate an expression in a certain frame. +@in_gdb_thread +def _evaluate(expr, frame_id): + if frame_id is not None: + frame = frame_for_id(frame_id) + frame.select() + return str(gdb.parse_and_eval(expr)) + + +# FIXME 'format' & hex +# FIXME return a structured response using pretty-printers / varobj +# FIXME supportsVariableType handling +@request("evaluate") +def eval_request(expression, *, frameId=None, **args): + result = send_gdb_with_response(lambda: _evaluate(expression, frameId)) + return { + "result": result, + # FIXME + "variablesReference": -1, + } diff --git a/gdb/python/lib/gdb/dap/events.py b/gdb/python/lib/gdb/dap/events.py new file mode 100644 index 0000000..45e2a1e --- /dev/null +++ b/gdb/python/lib/gdb/dap/events.py @@ -0,0 +1,166 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import enum +import gdb + +from .server import send_event +from .startup import in_gdb_thread, Invoker, log +from .breakpoint import breakpoint_descriptor + + +@in_gdb_thread +def _on_exit(event): + code = 0 + if hasattr(event, "exit_code"): + code = event.exit_code + send_event( + "exited", + { + "exitCode": code, + }, + ) + + +@in_gdb_thread +def _bp_modified(event): + send_event( + "breakpoint", + { + "reason": "changed", + "breakpoint": breakpoint_descriptor(event), + }, + ) + + +@in_gdb_thread +def _bp_created(event): + send_event( + "breakpoint", + { + "reason": "new", + "breakpoint": breakpoint_descriptor(event), + }, + ) + + +@in_gdb_thread +def _bp_deleted(event): + send_event( + "breakpoint", + { + "reason": "removed", + "breakpoint": breakpoint_descriptor(event), + }, + ) + + +@in_gdb_thread +def _new_thread(event): + send_event( + "thread", + { + "reason": "started", + "threadId": event.inferior_thread.global_num, + }, + ) + + +_suppress_cont = False + + +@in_gdb_thread +def _cont(event): + global _suppress_cont + if _suppress_cont: + log("_suppress_cont case") + _suppress_cont = False + else: + send_event( + "continued", + { + "threadId": gdb.selected_thread().global_num, + "allThreadsContinued": True, + }, + ) + + +class StopKinds(enum.Enum): + # The values here are chosen to follow the DAP spec. + STEP = "step" + BREAKPOINT = "breakpoint" + PAUSE = "pause" + EXCEPTION = "exception" + + +_expected_stop = None + + +@in_gdb_thread +def expect_stop(reason): + """Indicate that a stop is expected, for the reason given.""" + global _expected_stop + _expected_stop = reason + + +# A wrapper for Invoker that also sets the expected stop. +class ExecutionInvoker(Invoker): + """A subclass of Invoker that sets the expected stop. + Note that this assumes that the command will restart the inferior, + so it will also cause ContinuedEvents to be suppressed.""" + + def __init__(self, cmd, expected): + super().__init__(cmd) + self.expected = expected + + @in_gdb_thread + def __call__(self): + expect_stop(self.expected) + global _suppress_cont + _suppress_cont = True + # FIXME if the call fails should we clear _suppress_cont? + super().__call__() + + +@in_gdb_thread +def _on_stop(event): + log("entering _on_stop: " + repr(event)) + global _expected_stop + obj = { + "threadId": gdb.selected_thread().global_num, + # FIXME we don't support non-stop for now. + "allThreadsStopped": True, + } + if isinstance(event, gdb.BreakpointEvent): + # Ignore the expected stop, we hit a breakpoint instead. + # FIXME differentiate between 'breakpoint', 'function breakpoint', + # 'data breakpoint' and 'instruction breakpoint' here. + _expected_stop = StopKinds.BREAKPOINT + obj["hitBreakpointIds"] = [x.number for x in event.breakpoints] + elif _expected_stop is None: + # FIXME what is even correct here + _expected_stop = StopKinds.EXCEPTION + obj["reason"] = _expected_stop.value + _expected_stop = None + send_event("stopped", obj) + + +gdb.events.stop.connect(_on_stop) +gdb.events.exited.connect(_on_exit) +gdb.events.breakpoint_created.connect(_bp_created) +gdb.events.breakpoint_modified.connect(_bp_modified) +gdb.events.breakpoint_deleted.connect(_bp_deleted) +gdb.events.new_thread.connect(_new_thread) +gdb.events.cont.connect(_cont) diff --git a/gdb/python/lib/gdb/dap/frames.py b/gdb/python/lib/gdb/dap/frames.py new file mode 100644 index 0000000..a1c2689 --- /dev/null +++ b/gdb/python/lib/gdb/dap/frames.py @@ -0,0 +1,57 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import gdb + +from .startup import in_gdb_thread + + +# Map from frame (thread,level) pair to frame ID numbers. Note we +# can't use the frame itself here as it is not hashable. +_frame_ids = {} + +# Map from frame ID number back to frames. +_id_to_frame = {} + + +# Clear all the frame IDs. +@in_gdb_thread +def _clear_frame_ids(evt): + global _frame_ids, _id_to_frame + _frame_ids = {} + _id_to_frame = {} + + +# Clear the frame ID map whenever the inferior runs. +gdb.events.cont.connect(_clear_frame_ids) + + +@in_gdb_thread +def frame_id(frame): + """Return the frame identifier for FRAME.""" + global _frame_ids, _id_to_frame + pair = (gdb.selected_thread().global_num, frame.level) + if pair not in _frame_ids: + id = len(_frame_ids) + _frame_ids[pair] = id + _id_to_frame[id] = frame + return _frame_ids[pair] + + +@in_gdb_thread +def frame_for_id(id): + """Given a frame identifier ID, return the corresponding frame.""" + global _id_to_frame + return _id_to_frame[id] diff --git a/gdb/python/lib/gdb/dap/io.py b/gdb/python/lib/gdb/dap/io.py new file mode 100644 index 0000000..656ac08 --- /dev/null +++ b/gdb/python/lib/gdb/dap/io.py @@ -0,0 +1,67 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json + +from .startup import start_thread, send_gdb + + +def read_json(stream): + """Read a JSON-RPC message from STREAM. + The decoded object is returned.""" + # First read and parse the header. + content_length = None + while True: + line = stream.readline() + line = line.strip() + if line == b"": + break + if line.startswith(b"Content-Length:"): + line = line[15:].strip() + content_length = int(line) + data = bytes() + while len(data) < content_length: + new_data = stream.read(content_length - len(data)) + data += new_data + result = json.loads(data) + return result + + +def start_json_writer(stream, queue): + """Start the JSON writer thread. + It will read objects from QUEUE and write them to STREAM, + following the JSON-RPC protocol.""" + + def _json_writer(): + seq = 1 + while True: + obj = queue.get() + if obj is None: + # This is an exit request. The stream is already + # flushed, so all that's left to do is request an + # exit. + send_gdb("quit") + break + obj["seq"] = seq + seq = seq + 1 + encoded = json.dumps(obj) + body_bytes = encoded.encode("utf-8") + header = f"Content-Length: {len(body_bytes)}\r\n\r\n" + header_bytes = header.encode("ASCII") + stream.write(header_bytes) + stream.write(body_bytes) + stream.flush() + + start_thread("JSON writer", _json_writer) diff --git a/gdb/python/lib/gdb/dap/launch.py b/gdb/python/lib/gdb/dap/launch.py new file mode 100644 index 0000000..7ac8177 --- /dev/null +++ b/gdb/python/lib/gdb/dap/launch.py @@ -0,0 +1,39 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from .events import ExecutionInvoker +from .server import request, capability +from .startup import send_gdb + + +_program = None + + +@request("launch") +def launch(*, program=None, **args): + if program is not None: + global _program + _program = program + send_gdb(f"file {_program}") + + +@capability("supportsConfigurationDoneRequest") +@request("configurationDone") +def config_done(**args): + global _program + if _program is not None: + # Suppress the continue event, but don't set any particular + # expected stop. + send_gdb(ExecutionInvoker("run", None)) diff --git a/gdb/python/lib/gdb/dap/next.py b/gdb/python/lib/gdb/dap/next.py new file mode 100644 index 0000000..726b659 --- /dev/null +++ b/gdb/python/lib/gdb/dap/next.py @@ -0,0 +1,51 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from .events import StopKinds, ExecutionInvoker +from .server import capability, request +from .startup import send_gdb +from .state import set_thread + + +# Helper function to set the current thread. +def _handle_thread_step(threadId): + # Ensure we're going to step the correct thread. + send_gdb(lambda: set_thread(threadId)) + + +@request("next") +def next(*, threadId, granularity="statement", **args): + _handle_thread_step(threadId) + cmd = "next" + if granularity == "instruction": + cmd += "i" + send_gdb(ExecutionInvoker(cmd, StopKinds.STEP)) + + +@capability("supportsSteppingGranularity") +@request("stepIn") +def stepIn(*, threadId, granularity="statement", **args): + _handle_thread_step(threadId) + cmd = "step" + if granularity == "instruction": + cmd += "i" + send_gdb(ExecutionInvoker(cmd, StopKinds.STEP)) + + +@request("continue") +def continue_request(**args): + send_gdb(ExecutionInvoker("continue", None)) + # FIXME Just ignore threadId for the time being, and assume all-stop. + return {"allThreadsContinued": True} diff --git a/gdb/python/lib/gdb/dap/pause.py b/gdb/python/lib/gdb/dap/pause.py new file mode 100644 index 0000000..74fdf48 --- /dev/null +++ b/gdb/python/lib/gdb/dap/pause.py @@ -0,0 +1,23 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from .events import StopKinds, ExecutionInvoker +from .server import request +from .startup import send_gdb + + +@request("pause") +def pause(**args): + send_gdb(ExecutionInvoker("interrupt -a", StopKinds.PAUSE)) diff --git a/gdb/python/lib/gdb/dap/scopes.py b/gdb/python/lib/gdb/dap/scopes.py new file mode 100644 index 0000000..0c887db --- /dev/null +++ b/gdb/python/lib/gdb/dap/scopes.py @@ -0,0 +1,65 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import gdb + +from .frames import frame_for_id +from .startup import send_gdb_with_response, in_gdb_thread +from .server import request + + +# Helper function to return a frame's block without error. +@in_gdb_thread +def _safe_block(frame): + try: + return frame.block() + except gdb.error: + return None + + +# Helper function to return a list of variables of block, up to the +# enclosing function. +@in_gdb_thread +def _block_vars(block): + result = [] + while True: + result += list(block) + if block.function is not None: + break + block = block.superblock + return result + + +# Helper function to create a DAP scopes for a given frame ID. +@in_gdb_thread +def _get_scope(id): + frame = frame_for_id(id) + block = _safe_block(frame) + scopes = [] + if block is not None: + new_scope = { + # FIXME + "name": "Locals", + "expensive": False, + "namedVariables": len(_block_vars(block)), + } + scopes.append(new_scope) + return scopes + + +@request("scopes") +def scopes(*, frameId, **extra): + scopes = send_gdb_with_response(lambda: _get_scope(frameId)) + return {"scopes": scopes} diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py new file mode 100644 index 0000000..d6fc0bd --- /dev/null +++ b/gdb/python/lib/gdb/dap/server.py @@ -0,0 +1,205 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json +import queue + +from .io import start_json_writer, read_json +from .startup import ( + in_dap_thread, + start_thread, + log, + log_stack, + send_gdb_with_response, +) + + +# Map capability names to values. +_capabilities = {} + +# Map command names to callables. +_commands = {} + +# The global server. +_server = None + + +class Server: + """The DAP server class.""" + + def __init__(self, in_stream, out_stream, child_stream): + self.in_stream = in_stream + self.out_stream = out_stream + self.child_stream = child_stream + self.delayed_events = [] + # This queue accepts JSON objects that are then sent to the + # DAP client. Writing is done in a separate thread to avoid + # blocking the read loop. + self.write_queue = queue.SimpleQueue() + self.done = False + global _server + _server = self + + # Treat PARAMS as a JSON-RPC request and perform its action. + # PARAMS is just a dictionary from the JSON. + @in_dap_thread + def _handle_command(self, params): + # We don't handle 'cancel' for now. + result = { + "request_seq": params["seq"], + "type": "response", + "command": params["command"], + } + try: + if "arguments" in params: + args = params["arguments"] + else: + args = {} + global _commands + body = _commands[params["command"]](**args) + if body is not None: + result["body"] = body + result["success"] = True + except BaseException as e: + log_stack() + result["success"] = False + result["message"] = str(e) + return result + + # Read inferior output and sends OutputEvents to the client. It + # is run in its own thread. + def _read_inferior_output(self): + while True: + line = self.child_stream.readline() + self.send_event( + "output", + { + "category": "stdout", + "output": line, + }, + ) + + # Send OBJ to the client, logging first if needed. + def _send_json(self, obj): + log("WROTE: <<<" + json.dumps(obj) + ">>>") + self.write_queue.put(obj) + + # This must be run in the DAP thread, but we can't use + # @in_dap_thread here because the global isn't set until after + # this starts running. FIXME. + def main_loop(self): + """The main loop of the DAP server.""" + # Before looping, start the thread that writes JSON to the + # client, and the thread that reads output from the inferior. + start_thread("output reader", self._read_inferior_output) + start_json_writer(self.out_stream, self.write_queue) + while not self.done: + cmd = read_json(self.in_stream) + log("READ: <<<" + json.dumps(cmd) + ">>>") + result = self._handle_command(cmd) + self._send_json(result) + events = self.delayed_events + self.delayed_events = [] + for (event, body) in events: + self.send_event(event, body) + # Got the terminate request. This is handled by the + # JSON-writing thread, so that we can ensure that all + # responses are flushed to the client before exiting. + self.write_queue.put(None) + + @in_dap_thread + def send_event_later(self, event, body=None): + """Send a DAP event back to the client, but only after the + current request has completed.""" + self.delayed_events.append((event, body)) + + # Note that this does not need to be run in any particular thread, + # because it just creates an object and writes it to a thread-safe + # queue. + def send_event(self, event, body=None): + """Send an event to the DAP client. + EVENT is the name of the event, a string. + BODY is the body of the event, an arbitrary object.""" + obj = { + "type": "event", + "event": event, + } + if body is not None: + obj["body"] = body + self._send_json(obj) + + def shutdown(self): + """Request that the server shut down.""" + # Just set a flag. This operation is complicated because we + # want to write the result of the request before exiting. See + # main_loop. + self.done = True + + +def send_event(event, body): + """Send an event to the DAP client. + EVENT is the name of the event, a string. + BODY is the body of the event, an arbitrary object.""" + global _server + _server.send_event(event, body) + + +def request(name): + """A decorator that indicates that the wrapper function implements + the DAP request NAME.""" + + def wrap(func): + global _commands + _commands[name] = func + # All requests must run in the DAP thread. + return in_dap_thread(func) + + return wrap + + +def capability(name): + """A decorator that indicates that the wrapper function implements + the DAP capability NAME.""" + + def wrap(func): + global _capabilities + _capabilities[name] = True + return func + + return wrap + + +@request("initialize") +def initialize(**args): + global _server, _capabilities + _server.config = args + _server.send_event_later("initialized") + return _capabilities.copy() + + +@request("terminate") +@capability("supportsTerminateRequest") +def terminate(**args): + # We can ignore the result here, because we only really need to + # synchronize. + send_gdb_with_response("kill") + + +@request("disconnect") +@capability("supportTerminateDebuggee") +def disconnect(*, terminateDebuggee=False, **args): + if terminateDebuggee: + terminate() + _server.shutdown() diff --git a/gdb/python/lib/gdb/dap/startup.py b/gdb/python/lib/gdb/dap/startup.py new file mode 100644 index 0000000..acfdcb4 --- /dev/null +++ b/gdb/python/lib/gdb/dap/startup.py @@ -0,0 +1,189 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Do not import other gdbdap modules here -- this module must come +# first. +import functools +import gdb +import queue +import signal +import threading +import traceback +from contextlib import contextmanager + + +# The GDB thread, aka the main thread. +_gdb_thread = threading.current_thread() + + +# The DAP thread. +_dap_thread = None + + +@contextmanager +def blocked_signals(): + """A helper function that blocks and unblocks signals.""" + if not hasattr(signal, "pthread_sigmask"): + yield + return + + to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH} + signal.pthread_sigmask(signal.SIG_BLOCK, to_block) + try: + yield None + finally: + signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block) + + +def start_thread(name, target, args=()): + """Start a new thread, invoking TARGET with *ARGS there. + This is a helper function that ensures that any GDB signals are + correctly blocked.""" + # GDB requires that these be delivered to the gdb thread. We + # do this here to avoid any possible race with the creation of + # the new thread. The thread mask is inherited by new + # threads. + with blocked_signals(): + result = threading.Thread(target=target, args=args, daemon=True) + result.start() + return result + + +def start_dap(target): + """Start the DAP thread and invoke TARGET there.""" + global _dap_thread + exec_and_log("set breakpoint pending on") + _dap_thread = start_thread("DAP", target) + + +def in_gdb_thread(func): + """A decorator that asserts that FUNC must be run in the GDB thread.""" + + @functools.wraps(func) + def ensure_gdb_thread(*args, **kwargs): + assert threading.current_thread() is _gdb_thread + return func(*args, **kwargs) + + return ensure_gdb_thread + + +def in_dap_thread(func): + """A decorator that asserts that FUNC must be run in the DAP thread.""" + + @functools.wraps(func) + def ensure_dap_thread(*args, **kwargs): + assert threading.current_thread() is _dap_thread + return func(*args, **kwargs) + + return ensure_dap_thread + + +class LoggingParam(gdb.Parameter): + """Whether DAP logging is enabled.""" + + set_doc = "Set the DAP logging status." + show_doc = "Show the DAP logging status." + + log_file = None + + def __init__(self): + super().__init__( + "debug dap-log-file", gdb.COMMAND_MAINTENANCE, gdb.PARAM_OPTIONAL_FILENAME + ) + self.value = None + + def get_set_string(self): + # Close any existing log file, no matter what. + if self.log_file is not None: + self.log_file.close() + self.log_file = None + if self.value is not None: + self.log_file = open(self.value, "w") + return "" + + +dap_log = LoggingParam() + + +def log(something): + """Log SOMETHING to the log file, if logging is enabled.""" + if dap_log.log_file is not None: + print(something, file=dap_log.log_file) + dap_log.log_file.flush() + + +def log_stack(): + """Log a stack trace to the log file, if logging is enabled.""" + if dap_log.log_file is not None: + traceback.print_exc(file=dap_log.log_file) + + +@in_gdb_thread +def exec_and_log(cmd): + """Execute the gdb command CMD. + If logging is enabled, log the command and its output.""" + log("+++ " + cmd) + try: + output = gdb.execute(cmd, from_tty=True, to_string=True) + if output != "": + log(">>> " + output) + except gdb.error: + log_stack() + + +class Invoker(object): + """A simple class that can invoke a gdb command.""" + + def __init__(self, cmd): + self.cmd = cmd + + # This is invoked in the gdb thread to run the command. + @in_gdb_thread + def __call__(self): + exec_and_log(self.cmd) + + +def send_gdb(cmd): + """Send CMD to the gdb thread. + CMD can be either a function or a string. + If it is a string, it is passed to gdb.execute.""" + if isinstance(cmd, str): + cmd = Invoker(cmd) + gdb.post_event(cmd) + + +def send_gdb_with_response(fn): + """Send FN to the gdb thread and return its result. + If FN is a string, it is passed to gdb.execute and None is + returned as the result. + If FN throws an exception, this function will throw the + same exception in the calling thread. + """ + if isinstance(fn, str): + fn = Invoker(fn) + result_q = queue.SimpleQueue() + + def message(): + try: + val = fn() + result_q.put(val) + except Exception as e: + result_q.put(e) + + send_gdb(message) + val = result_q.get() + if isinstance(val, Exception): + raise val + return val diff --git a/gdb/python/lib/gdb/dap/state.py b/gdb/python/lib/gdb/dap/state.py new file mode 100644 index 0000000..fb2e543 --- /dev/null +++ b/gdb/python/lib/gdb/dap/state.py @@ -0,0 +1,25 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from .startup import in_gdb_thread, exec_and_log, log + + +@in_gdb_thread +def set_thread(thread_id): + """Set the current thread to THREAD_ID.""" + if thread_id == 0: + log("+++ Thread == 0 +++") + else: + exec_and_log(f"thread {thread_id}") diff --git a/gdb/python/lib/gdb/dap/threads.py b/gdb/python/lib/gdb/dap/threads.py new file mode 100644 index 0000000..b6a0ca0 --- /dev/null +++ b/gdb/python/lib/gdb/dap/threads.py @@ -0,0 +1,42 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import gdb + +from .server import request +from .startup import send_gdb_with_response, in_gdb_thread + + +# A helper function to construct the list of threads. +@in_gdb_thread +def _get_threads(): + result = [] + for thr in gdb.selected_inferior().threads(): + one_result = { + "id": thr.global_num, + } + name = thr.name + if name is not None: + one_result["name"] = name + result.append(one_result) + return result + + +@request("threads") +def threads(**args): + result = send_gdb_with_response(_get_threads) + return { + "threads": result, + } |