summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2024-12-07 23:50:28 -0700
committerLuke T. Shumaker <lukeshu@lukeshu.com>2024-12-08 08:27:23 -0700
commitb9ebe41358244caa9334e72ca4e3c8c7a14c86e7 (patch)
tree1ce291df34d78acd6f1619a779abbd4f06134e26
parent056082dc81641875626d5f9c0ff23a3b9d66deff (diff)
Fix libcr gdb integration?
-rw-r--r--gdb-helpers/libcr.py355
-rw-r--r--libcr/coroutine.c60
2 files changed, 313 insertions, 102 deletions
diff --git a/gdb-helpers/libcr.py b/gdb-helpers/libcr.py
index 385b61a..c07b679 100644
--- a/gdb-helpers/libcr.py
+++ b/gdb-helpers/libcr.py
@@ -4,41 +4,55 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import contextlib
+import time
import typing
import gdb
+import gdb.unwinder
# GDB helpers ##################################################################
-def gdb_argv_to_string(argv: list[str]) -> str:
- """Reverse of gdb.string_to_argv()"""
- # TODO: This is wrong
- return " ".join(argv)
+class _gdb_Locus(typing.Protocol):
+ @property
+ def frame_unwinders(self) -> list["gdb._Unwinder"]: ...
+
+
+def gdb_unregister_unwinder(
+ locus: gdb.Objfile | gdb.Progspace | None, unwinder: "gdb._Unwinder"
+) -> None:
+ _locus: _gdb_Locus = typing.cast(_gdb_Locus, gdb) if locus is None else locus
+ _locus.frame_unwinders.remove(unwinder)
+ gdb.invalidate_cached_frames()
class gdb_JmpBuf:
"""Our own in-Python GDB-specific implementation of `jmp_buf`"""
- frame: gdb.Frame
+ level: int
registers: dict[str, str]
def gdb_setjmp() -> gdb_JmpBuf:
"""Our own in-Python GDB-specific implementation of `setjmp()`"""
buf = gdb_JmpBuf()
- buf.frame = gdb.selected_frame()
+ buf.level = gdb.selected_frame().level()
+ gdb.execute("select-frame level 0")
buf.registers = {}
for line in gdb.execute("info registers", to_string=True).split("\n"):
words = line.split(maxsplit=2)
if len(words) < 2:
continue
buf.registers[words[0]] = words[1]
+ gdb.execute(f"select-frame level {buf.level}")
return buf
def gdb_longjmp(buf: gdb_JmpBuf) -> None:
"""Our own in-Python GDB-specific implementation of `longjmp()`"""
+
+ gdb.execute("select-frame level 0")
+
if (
("sp" in buf.registers)
and ("msp" in buf.registers)
@@ -52,9 +66,12 @@ def gdb_longjmp(buf: gdb_JmpBuf) -> None:
gdb.execute(f"set $sp = {buf.registers['sp']}", to_string=True)
gdb.execute(f"set $msp = {buf.registers['msp']}")
gdb.execute(f"set $psp = {buf.registers['psp']}")
+
for reg, val in buf.registers.items():
gdb.execute(f"set ${reg} = {val}")
- buf.frame.select()
+ gdb.invalidate_cached_frames()
+
+ gdb.execute(f"select-frame level {buf.level}")
# Core libcr functionality #####################################################
@@ -62,42 +79,167 @@ def gdb_longjmp(buf: gdb_JmpBuf) -> None:
class CrGlobals:
coroutines: list["CrCoroutine"]
- breakpoints: list[gdb.Breakpoint]
+ _breakpoint: "CrBreakpoint"
+ _known_threads: set[gdb.InferiorThread]
def __init__(self) -> None:
num = int(
gdb.parse_and_eval("sizeof(coroutine_table)/sizeof(coroutine_table[0])")
)
+
self.coroutines = [CrCoroutine(self, i + 1) for i in range(num)]
- self.breakpoints = []
+
+ self._breakpoint = CrBreakpoint()
+ self._breakpoint.enabled = False
+
+ self._known_threads = set()
+
+ gdb.events.cont.connect(self._on_cont)
def delete(self) -> None:
self.coroutines = []
- for b in self.breakpoints:
- b.delete()
- self.breakpoints = []
+ self._breakpoint.delete()
+ gdb.events.cont.disconnect(self._on_cont)
+
+ def readjmp(self, env_ptr_expr: str) -> gdb_JmpBuf:
+ self._breakpoint.enabled = True
+ gdb.execute(f"call (void)cr_gdb_readjmp({env_ptr_expr})")
+ self._breakpoint.enabled = False
+ gdb.execute("queue-signal SIGWINCH")
+ return self._breakpoint.env
+
+ def _on_cont(self, event: gdb.Event) -> None:
+ cur_threads = set(gdb.selected_inferior().threads())
+ if cur_threads - self._known_threads:
+ # Ignore thread creation events.
+ self._known_threads = cur_threads
+ return
+ if self.coroutine_running:
+ if not self.coroutine_running.is_selected():
+ if True: # https://sourceware.org/bugzilla/show_bug.cgi?id=32428
+ print("Must return to running coroutine before continuing.")
+ print("Hit ^C twice then run:")
+ print(f" cr select {self.coroutine_running.id}")
+ while True:
+ time.sleep(1)
+ assert self.coroutine_running._cont_env
+ gdb_longjmp(self.coroutine_running._cont_env)
+ for cr in self.coroutines:
+ cr._cont_env = None
def is_valid_cid(self, cid: int) -> bool:
return 0 < cid and cid <= len(self.coroutines)
@property
- def coroutine_running(self) -> int:
- return int(gdb.parse_and_eval("coroutine_running"))
+ def coroutine_running(self) -> "CrCoroutine | None":
+ cid = int(gdb.parse_and_eval("coroutine_running"))
+ if not self.is_valid_cid(cid):
+ return None
+ return self.coroutines[cid - 1]
+
+ @property
+ def coroutine_selected(self) -> "CrCoroutine | None":
+ for cr in self.coroutines:
+ if cr.is_selected():
+ return cr
+ return None
@property
def CR_NONE(self) -> gdb.Value:
return gdb.parse_and_eval("CR_NONE")
+ @property
+ def CR_RUNNING(self) -> gdb.Value:
+ return gdb.parse_and_eval("CR_RUNNING")
+
+
+class CrBreakpointUnwinder(gdb.unwinder.Unwinder):
+ """Used to temporarily disable unwinding so that
+ gdb/breakpoint.c:check_longjmp_breakpoint_for_call_dummy() doesn't
+ prematurely garbage collect the `call`-dummy-frame.
+
+ """
+
+ def __init__(self) -> None:
+ super().__init__("cr_breakpoint_unwinder")
+
+ # The .pyi is wrong; it says `Frame` instead of `PendingFrame`.
+ def __call__(self, pending_frame: gdb.PendingFrame) -> gdb.UnwindInfo | None:
+ # Stop unwinding with stop_reason=UNWIND_NO_SAVED_PC by
+ # returning an UnwindInfo that doesn't have
+ # `.add_saved_register("pc", ...)`.
+ return pending_frame.create_unwind_info(
+ gdb.unwinder.FrameId(
+ sp=pending_frame.read_register("sp"),
+ pc=pending_frame.read_register("pc"),
+ )
+ )
+
+
+class CrBreakpoint(gdb.Breakpoint):
+ env: gdb_JmpBuf
+ _unwinder_locus: gdb.Objfile
+ _unwinder: CrBreakpointUnwinder
+
+ def __init__(self) -> None:
+ self.env = gdb_JmpBuf()
+
+ self._unwinder = CrBreakpointUnwinder()
+ readjmp_sym = gdb.lookup_global_symbol("cr_gdb_readjmp")
+ assert readjmp_sym
+ self._unwinder_locus = readjmp_sym.symtab.objfile
+ gdb.unwinder.register_unwinder(self._unwinder_locus, self._unwinder, True)
+ self._unwinder.enabled = False
+
+ super().__init__(
+ function="cr_gdb_breakpoint", type=gdb.BP_BREAKPOINT, internal=True
+ )
+
+ @property
+ def enabled(self) -> bool:
+ return super().enabled
+
+ @enabled.setter
+ def enabled(self, value: bool) -> None:
+ self._unwinder.enabled = value
+ gdb.Breakpoint.enabled.__set__(self, value) # type: ignore
+
+ def stop(self) -> bool:
+ assert self._unwinder.enabled
+ self._unwinder.enabled = False
+ self.env = gdb_setjmp()
+ self._unwinder.enabled = True
+ return False # don't stop
+
+ def delete(self) -> None:
+ gdb_unregister_unwinder(self._unwinder_locus, self._unwinder)
+ super().delete()
+
+
+def cr_select_top_frame() -> None:
+ gdb.execute("select-frame level 0")
+ base_frame = gdb.selected_frame()
+ while True:
+ fn = gdb.selected_frame().name()
+ if fn and (fn.startswith("cr_") or fn.startswith("_cr_")):
+ older = gdb.selected_frame().older()
+ if not older:
+ base_frame.select()
+ break
+ older.select()
+ else:
+ break
+
class CrCoroutine:
cr_globals: CrGlobals
cid: int
- env: gdb_JmpBuf | None
+ _cont_env: gdb_JmpBuf | None
def __init__(self, cr_globals: CrGlobals, cid: int) -> None:
self.cr_globals = cr_globals
self.cid = cid
- self.env = None
+ self._cont_env = None
@property
def id(self) -> int:
@@ -114,50 +256,39 @@ class CrCoroutine:
bs[i] = int(gdb.parse_and_eval(f"coroutine_table[{self.cid-1}].name[{i}]"))
return bytes(bs).decode("UTF-8").split("\x00", maxsplit=1)[0]
+ def is_selected(self) -> bool:
+ sp = int(gdb.parse_and_eval("$sp"))
+ lo = int(gdb.parse_and_eval(f"coroutine_table[{self.id-1}].stack"))
+ hi = lo + int(gdb.parse_and_eval(f"coroutine_table[{self.id-1}].stack_size"))
+ return lo <= sp and sp < hi
+
+ def select(self, level: int = -1) -> None:
+ if self.cr_globals.coroutine_selected:
+ self.cr_globals.coroutine_selected._cont_env = gdb_setjmp()
+
+ if self._cont_env:
+ gdb_longjmp(self._cont_env)
+ else:
+ env: gdb_JmpBuf
+ if self == self.cr_globals.coroutine_running:
+ assert False # self._cont_env should have been set
+ elif self.state == self.cr_globals.CR_RUNNING:
+ env = self.cr_globals.readjmp("&coroutine_add_env")
+ else:
+ env = self.cr_globals.readjmp(f"&coroutine_table[{self.id-1}].env")
+ gdb_longjmp(env)
+ cr_select_top_frame()
+
@contextlib.contextmanager
- def active(self) -> typing.Iterator[None]:
+ def with_selected(self) -> typing.Iterator[None]:
saved_env = gdb_setjmp()
- cr_env = self.env
- if self.cid == self.cr_globals.coroutine_running:
- cr_env = saved_env
- if not cr_env:
- raise gdb.GdbError(
- f"GDB does not have a saved execution environment for coroutine {self.id}. "
- + "This can happen if the coroutine has not run for as long as GDB has been attached."
- )
-
- gdb_longjmp(cr_env)
+ self.select()
try:
yield
finally:
gdb_longjmp(saved_env)
-class CrSetJmpBreakpoint(gdb.Breakpoint):
- cr_globals: CrGlobals
-
- def __init__(self, cr_globals: CrGlobals) -> None:
- self.cr_globals = cr_globals
- cr_globals.breakpoints += [self]
- super().__init__(
- function="_cr_plat_setjmp_pre", type=gdb.BP_BREAKPOINT, internal=True
- )
-
- def stop(self) -> bool:
- if bool(gdb.parse_and_eval("env == &coroutine_add_env")):
- cid = self.cr_globals.coroutine_running
- elif bool(gdb.parse_and_eval("env == &coroutine_main_env")):
- cid = 0
- else:
- idx = gdb.parse_and_eval(
- "( ((char*)env)-((char *)&coroutine_table)) / sizeof(coroutine_table[0])"
- )
- cid = int(idx) + 1
- if self.cr_globals.is_valid_cid(cid):
- self.cr_globals.coroutines[cid - 1].env = gdb_setjmp()
- return False
-
-
# User-facing commands #########################################################
@@ -176,7 +307,12 @@ class CrCommand(gdb.Command):
class CrListCommand(gdb.Command):
"""List libcr coroutines.
- Usage: cr list"""
+ Usage: cr list
+
+ In the output:
+ - the 'R' marker indicates the currently-running coroutine
+ - the 'G' marker indicates the coroutine that GDB is viewing; this may be changed with `cr select`
+ """
cr_globals: CrGlobals
@@ -189,59 +325,80 @@ class CrListCommand(gdb.Command):
if len(argv) != 0:
raise gdb.GdbError(f"Usage: cr list")
- w_marker = 1
- w_id = max(len("Id"), len(str(len(self.cr_globals.coroutines))))
- w_name = max(
- len("Name"), int(gdb.parse_and_eval("sizeof(coroutine_table[0].name)"))
- )
- w_state = len("CR_INITIALIZING")
- print(
- f" {'Id'.ljust(w_id)} {'Name'.ljust(w_name)} {'State'.ljust(w_state)} Frame"
- )
+ rows: list[tuple[str, str, str, str, str]] = [
+ ("", "Id", "Name", "State", "Frame")
+ ]
for cr in self.cr_globals.coroutines:
if cr.state == self.cr_globals.CR_NONE:
continue
- v_marker = "*" if cr.id == self.cr_globals.coroutine_running else ""
- v_id = str(cr.id)
- v_name = cr.name
- v_state = str(cr.state)
- v_frame = self._pretty_frame(cr, from_tty)
- print(
- f"{v_marker.ljust(w_marker)} {v_id.ljust(w_id)} {v_name.ljust(w_name)} {v_state.ljust(w_state)} {v_frame}"
- )
+ rows += [
+ (
+ "".join(
+ [
+ "R" if cr == self.cr_globals.coroutine_running else " ",
+ "G" if cr.is_selected() else " ",
+ ]
+ ),
+ str(cr.id),
+ repr(cr.name),
+ str(cr.state),
+ self._pretty_frame(cr, from_tty),
+ )
+ ]
+
+ widths: list[int] = [
+ max(len(row[col]) for row in rows) for col in range(len(rows[0]))
+ ]
+
+ def line(row: tuple[str, str, str, str, str]) -> str:
+
+ def cell(col: int) -> str:
+ return row[col].ljust(widths[col])
+
+ return f"{cell(0)} {cell(1)} {cell(2)} {cell(3)} {row[4]}"
+
+ maxline = 0
+ if screenwidth := gdb.parameter("width"):
+ assert isinstance(screenwidth, int)
+ maxline = max(screenwidth, len(line(rows[0])))
+
+ for row in rows:
+ l = line(row)
+ if maxline and len(l) > maxline:
+ l = l[:maxline]
+ print(l)
def _pretty_frame(self, cr: CrCoroutine, from_tty: bool) -> str:
try:
- with cr.active():
+ with cr.with_selected():
+ saved_level = gdb.selected_frame().level()
+ cr_select_top_frame()
full = gdb.execute("frame", from_tty=from_tty, to_string=True)
+ gdb.execute(f"select-frame level {saved_level}")
except Exception as e:
- full = "#1 err: " + str(e)
+ full = "#0 err: " + str(e)
line = full.split("\n", maxsplit=1)[0]
return line.split(maxsplit=1)[1]
-class CrApplyCommand(gdb.Command):
- """Apply a GDB command to libcr coroutines.
- Usage: cr apply COROUTINE... -- COMMAND
- COROUTINE is a space-separated list of coroutine IDs or names."""
+class CrSelectCommand(gdb.Command):
+ """Select the coroutine that GDB is viewing
+ Usage: cr select COROUTINE
+ COROUTINE is either a coroutine ID or coroutine name."""
cr_globals: CrGlobals
def __init__(self, cr_globals: CrGlobals) -> None:
self.cr_globals = cr_globals
- gdb.Command.__init__(self, "cr apply", gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE)
+ gdb.Command.__init__(self, "cr select", gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE)
def invoke(self, arg: str, from_tty: bool) -> None:
argv = gdb.string_to_argv(arg)
- if "--" not in argv:
- raise gdb.GdbError("Usage: cr apply COROUTINE... -- COMMAND")
- sep = argv.index("--")
- crs = argv[:sep]
- cmd = argv[sep + 1 :]
- for spec in crs:
- cr = self._find(spec)
- with cr.active():
- gdb.execute(gdb_argv_to_string(cmd), from_tty=from_tty)
+ if len(argv) != 1:
+ raise gdb.GdbError("Usage: cr select COROUTINE")
+ cr = self._find(argv[0])
+ cr.select()
+ gdb.execute("frame")
def _find(self, name: str) -> CrCoroutine:
if name.isnumeric():
@@ -266,25 +423,35 @@ class CrApplyCommand(gdb.Command):
# Wire it all in ###############################################################
+cr_globals: CrGlobals | None = None
-def cr_initialize() -> None:
- cr_globals = CrGlobals()
-
- CrSetJmpBreakpoint(cr_globals)
+def cr_initialize() -> None:
+ global cr_globals
+ if cr_globals:
+ old = cr_globals
+ new = CrGlobals()
+ for i in range(min(len(old.coroutines), len(new.coroutines))):
+ new.coroutines[i]._cont_env = old.coroutines[i]._cont_env
+ old.delete()
+ cr_globals = new
+ else:
+ cr_globals = CrGlobals()
CrCommand(cr_globals)
CrListCommand(cr_globals)
- CrApplyCommand(cr_globals)
+ CrSelectCommand(cr_globals)
def cr_on_new_objfile(event: gdb.Event) -> None:
if any(
- objfile.lookup_static_symbol("_cr_plat_setjmp_pre")
- for objfile in gdb.objfiles()
+ objfile.lookup_global_symbol("cr_gdb_readjmp") for objfile in gdb.objfiles()
):
print("Initializing libcr integration...")
cr_initialize()
gdb.events.new_objfile.disconnect(cr_on_new_objfile)
-gdb.events.new_objfile.connect(cr_on_new_objfile)
+if cr_globals:
+ cr_initialize()
+else:
+ gdb.events.new_objfile.connect(cr_on_new_objfile)
diff --git a/libcr/coroutine.c b/libcr/coroutine.c
index c446276..a947ae9 100644
--- a/libcr/coroutine.c
+++ b/libcr/coroutine.c
@@ -135,6 +135,8 @@
#define ARRAY_LEN(arr) (sizeof(arr)/sizeof((arr)[0]))
#define NEXT_POWER_OF_2(x) ((1ULL)<<((sizeof(unsigned long long)*8)-__builtin_clzll(x)))
+#define UNUSED(name)
+
#define ALWAYS_INLINE [[gnu::always_inline]] inline
#define NEVER_INLINE [[gnu::noinline]]
@@ -158,7 +160,10 @@
/* For a signal to be *in* the mask means that the signal is
* *blocked*. */
- #define _CR_SIG_SENTINEL SIGHUP
+ #define _CR_SIG_SENTINEL SIGURG
+ #if CONFIG_COROUTINE_GDB
+ #define _CR_SIG_GDB SIGWINCH
+ #endif
bool cr_is_in_intrhandler(void) {
sigset_t cur_mask;
@@ -205,6 +210,19 @@
sigemptyset(&zero);
sigprocmask(SIG_SETMASK, &zero, NULL);
}
+ #if CONFIG_COROUTINE_GDB
+ static void _cr_gdb_intrhandler(int UNUSED(sig)) {}
+ #endif
+ static void cr_plat_init(void) {
+ #if CONFIG_COROUTINE_GDB
+ int r;
+ struct sigaction action = {
+ .sa_handler = _cr_gdb_intrhandler,
+ };
+ r = sigaction(_CR_SIG_GDB, &action, NULL);
+ assert(r == 0);
+ #endif
+ }
#elif __ARM_ARCH_6M__ && __ARM_EABI__
bool cr_is_in_intrhandler(void) {
uint32_t isr_number;
@@ -242,6 +260,7 @@
assert(!_cr_plat_are_interrupts_enabled());
asm volatile ("cpsie i");
}
+ static void cr_plat_init(void) {}
#else
#error unsupported platform (not __unix__, not __ARM_ARCH_6M__ && __ARM_EABI__)
#endif
@@ -327,14 +346,7 @@
uintptr_t sp;
#endif
} cr_plat_jmp_buf;
- #if CONIG_COROUTINE_GDB
- NEVER_INLINE
- #endif
static void _cr_plat_setjmp_pre(cr_plat_jmp_buf *env [[gnu::unused]]) {
- #if CONIG_COROUTINE_GDB
- /* Prevent the call from being optimized away. */
- asm ("");
- #endif
#if CONFIG_COROUTINE_MEASURE_STACK
env->sp = cr_plat_get_sp();
#endif
@@ -415,8 +427,12 @@ static const uint8_t stack_pattern[] = {
/* global variables ***********************************************************/
+static bool coroutine_initialized = false;
static cr_plat_jmp_buf coroutine_add_env;
static cr_plat_jmp_buf coroutine_main_env;
+#if CONFIG_COROUTINE_GDB
+static cr_plat_jmp_buf coroutine_gdb_env;
+#endif
/*
* Invariants (and non-invariants):
@@ -471,7 +487,26 @@ static inline cid_t coroutine_ringbuf_pop(void) {
return coroutine_ringbuf.buf[coroutine_ringbuf.tail++ % ARRAY_LEN(coroutine_ringbuf.buf)];
}
+#if CONFIG_COROUTINE_GDB
+NEVER_INLINE void cr_gdb_breakpoint(void) {
+ /* Prevent the call from being optimized away. */
+ asm ("");
+}
+NEVER_INLINE void cr_gdb_readjmp(cr_plat_jmp_buf *env) {
+ if (!cr_plat_setjmp(&coroutine_gdb_env))
+ cr_plat_longjmp(env, 2);
+}
+#define cr_setjmp(env) ({ \
+ int val = cr_plat_setjmp(env); \
+ if (val == 2) { \
+ cr_gdb_breakpoint(); \
+ cr_plat_longjmp(&coroutine_gdb_env, 1); \
+ } \
+ val; \
+ })
+#else
#define cr_setjmp(env) cr_plat_setjmp(env)
+#endif
#define cr_longjmp(env) cr_plat_longjmp(env, 1)
static inline void assert_cid(cid_t cid) {
@@ -511,6 +546,11 @@ cid_t coroutine_add_with_stack_size(size_t stack_size,
debugf("coroutine_add_with_stack_size(%zu, \"%s\", %p, %p)...",
stack_size, name, fn, args);
+ if (!coroutine_initialized) {
+ cr_plat_init();
+ coroutine_initialized = true;
+ }
+
cid_t child;
{
size_t base = last_created;
@@ -587,6 +627,10 @@ cid_t coroutine_add(const char *name, cr_fn_t fn, void *args) {
void coroutine_main(void) {
debugf("coroutine_main()");
+ if (!coroutine_initialized) {
+ cr_plat_init();
+ coroutine_initialized = true;
+ }
bool saved = cr_save_and_disable_interrupts();
assert(saved);
assert(!cr_is_in_intrhandler());