From 0680a5720bb67201898981aab8c0332b3093b566 Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Tue, 3 Nov 2026 23:56:41 -0700 Subject: [PATCH 1/2] Add lldb versions of the ptbt and ptbtall gdb macros lldbinit.py is a Python command script with the same output as gdbinit. Load it with "command script import lldbinit.py" (or from ~/.lldbinit). Unlike the gdb macros, the argument can be a struct or a pointer, so "ptbtall pt" works as well as "ptbtall &pt". If the name isn't visible in the current frame, outer frames are tried, so it also works when stopped inside a library function. Each frame line shows the address of the pt_func_t. Checked against gdbinit on pttpcc (1040 threads, same backtraces). Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +-- lldbinit.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 lldbinit.py diff --git a/README.md b/README.md index 437b467..7e3441f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Allowing blocking within a `switch` requires [gcc label variables](http://gcc.gn Protothreads are faster here because they do less. There's no kernel transition, no scheduler, no per-thread stack, no mutex locking. POSIX threads provide preemption and real parallelism, which protothreads do not. See [Memory overhead and performance](#memory-overhead-and-performance-benchmarks) for a detailed comparison and [Protothreads on a multi-core system](#protothreads-on-a-multi-core-system) for using both together. Also included: * optional semaphores, reader-writer locks, timers, and message queues, each in its own header and built entirely on the core * about 1000 lines of test code - * gdb (debugger) macros to print the stack traces of a given protothread or all protothreads + * gdb and lldb (debugger) macros (`gdbinit`, `lldbinit.py`) to print the stack traces of a given protothread or all protothreads * a CMake find module, `FindPROTOTHREAD.cmake`. Copy it into another CMake project, and `find_package(PROTOTHREAD)` will locate the installed headers ### Using it ### @@ -771,7 +771,7 @@ All configuration is by preprocessor macro. Because the library is header-only, > `PT_DEBUG` (default `1`) > -> Enables internal assertions and the bookkeeping the gdb macros use to print protothread stack traces. Set to `0` for production builds. **This changes the layout of `pt_thread_t` and `pt_func_t`**, so it must be the same for your whole program. +> Enables internal assertions and the bookkeeping the gdb and lldb macros use to print protothread stack traces. Set to `0` for production builds. **This changes the layout of `pt_thread_t` and `pt_func_t`**, so it must be the same for your whole program. > `PT_NWAIT` (default `1024`) > diff --git a/lldbinit.py b/lldbinit.py new file mode 100644 index 0000000..01702f1 --- /dev/null +++ b/lldbinit.py @@ -0,0 +1,99 @@ +#************************************************************** +#* lldbinit.py +#* https://github.com/LarryRuane/protothread +#* Copyright (c) 2008-present Larry Ruane +#* Distributed under the MIT software license, see the accompanying +#* file LICENSE or https://opensource.org/licenses/MIT. +#* SPDX-License-Identifier: MIT +#* +#* Protothread stack trace commands for lldb (port of gdbinit) +#* Load with: command script import /path/to/lldbinit.py +#* (or add that line to your ~/.lldbinit file) +#* +#* PT_DEBUG must be defined as 1 in protothread.h +#* +#************************************************************** + +import lldb + + +def _eval(exe_ctx, expr, result): + # try the current frame, then outer frames (so "pt" works inside callees) + frame = exe_ctx.GetFrame() + thread = exe_ctx.GetThread() + first = None + for i in range(frame.GetFrameID(), thread.GetNumFrames()): + v = thread.GetFrameAtIndex(i).EvaluateExpression(expr) + if v.IsValid() and v.GetError().Success(): + return v + first = first or v + result.SetError("can't evaluate '%s': %s" % (expr, first.GetError())) + return None + + +def _str(v): + addr = v.GetValueAsUnsigned() + if not addr: + return "?" + err = lldb.SBError() + s = v.GetProcess().ReadCStringFromMemory(addr, 4096, err) + return s if err.Success() else "?" + + +def _ptbt(t, out): + for name in ("func", "env", "channel"): + out.write("%-6s%s\n" % (name[:4] + ":", t.GetChildMemberWithName(name))) + pte = t.GetChildMemberWithName("pt_func") + j = 0 + while pte.GetValueAsUnsigned() and \ + pte.GetChildMemberWithName("label").GetValueAsUnsigned(): + out.write("#%d %s (0x%x) at %s:%d\n" % ( + j, _str(pte.GetChildMemberWithName("function")), + pte.GetValueAsUnsigned(), + _str(pte.GetChildMemberWithName("file")), + pte.GetChildMemberWithName("line").GetValueAsSigned())) + pte = pte.GetChildMemberWithName("next") + j += 1 + + +def _ring(head, state, out): + # lists are circular, head points to newest, head->next is oldest + if not head.GetValueAsUnsigned(): + return + pt = head + while True: + pt = pt.GetChildMemberWithName("next") + out.write("\nstate: %s p *(struct pt_thread_s *)0x%x\n" + % (state, pt.GetValueAsUnsigned())) + _ptbt(pt, out) + if pt.GetValueAsUnsigned() == head.GetValueAsUnsigned(): + break + + +def ptbt(debugger, command, exe_ctx, result, internal_dict): + """pt_thread_t -- print stack backtrace of given protothread""" + t = _eval(exe_ctx, command, result) + if t is not None: + _ptbt(t, result) + + +def ptbtall(debugger, command, exe_ctx, result, internal_dict): + """protothread_t -- print stack backtraces of all protothreads""" + s = _eval(exe_ctx, command, result) + if s is None: + return + running = s.GetChildMemberWithName("running") + if running.GetValueAsUnsigned(): + result.write("\nstate: running p *(struct pt_thread_s *)0x%x\n" + % running.GetValueAsUnsigned()) + _ptbt(running, result) + _ring(s.GetChildMemberWithName("ready"), "ready_to_run", result) + wait = s.GetChildMemberWithName("wait") + for i in range(wait.GetNumChildren()): + _ring(wait.GetChildAtIndex(i), "wait", result) + + +def __lldb_init_module(debugger, internal_dict): + for name in ("ptbt", "ptbtall"): + debugger.HandleCommand( + "command script add -o -f %s.%s %s" % (__name__, name, name)) From f31d9bf6a90ed0b76517747113e9e3cc45492f08 Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Wed, 4 Nov 2026 00:28:12 -0700 Subject: [PATCH 2/2] CI: test ptbt and ptbtall in gdb and lldb debugger_test.sh builds protothread_test with -g, stops it twice, and runs ptbtall in both debuggers. The first stop is in test_pc(), with the consumer running and the producer waiting. The second is where the last thread arrives at a barrier in test_barrier(), which has running, ready and waiting threads, some waiting inside a pt_call(). The two debuggers have to print the same threads and frames, and each frame's line has to be a pt_wait(), pt_yield() or pt_call(). The thread functions and states are checked too, but not line numbers, so the test doesn't break whenever protothread_test.c changes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 10 ++++ debugger_test.sh | 105 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100755 debugger_test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 071b74c..228bc8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,16 @@ jobs: done exit $fail + # The ptbt and ptbtall commands in gdbinit and lldbinit.py. + debuggers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Install gdb and lldb + run: sudo apt-get update && sudo apt-get install -y gdb lldb + - name: Run ptbtall in both debuggers + run: ./debugger_test.sh + standards: name: std (${{ matrix.cc }} ${{ matrix.std }}) runs-on: ubuntu-latest diff --git a/debugger_test.sh b/debugger_test.sh new file mode 100755 index 0000000..5f700cc --- /dev/null +++ b/debugger_test.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# https://github.com/LarryRuane/protothread +# Copyright (c) 2008-present Larry Ruane +# Distributed under the MIT software license, see the accompanying +# file LICENSE or https://opensource.org/licenses/MIT. +# SPDX-License-Identifier: MIT +# +# Check the ptbt/ptbtall debugger commands in gdbinit and lldbinit.py. +# Stops protothread_test in test_pc() and test_barrier(), runs ptbtall in +# gdb and lldb, and checks that both print the expected threads. +# Usage: ./debugger_test.sh (set CC, GDB or LLDB to override) + +set -euo pipefail +cd "$(dirname "$0")" +CC=${CC:-cc} GDB=${GDB:-gdb} +if [ -z "${LLDB:-}" ] ; then + # distributions often install only a versioned lldb-NN + LLDB=$(compgen -c lldb | grep -E '^lldb(-[0-9]+)?$' | sort -t- -k2n | + tail -1) +fi +for d in "$CC" "$GDB" "$LLDB" ; do + if ! command -v "$d" > /dev/null ; then + echo "${d:-lldb} not found (set CC, GDB or LLDB)" ; exit 1 + fi +done +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +$CC -g -O0 protothread_test.c -o "$tmp/pttest" + +# stop 1: consumer resumed (running), producer waiting on the mailbox +pc_bp=consumer_thr +pc_expr='((pc_thread_context_t*)env)->pt_thread.s' +# stop 2: last thread arriving at a barrier, so all three states appear +bar_line=$(grep -n 'pt_broadcast(pt_get_pt(c), &b->gen)' protothread_test.c | + cut -d: -f1) +bar_expr='c->pt_func.thread->s' + +"$GDB" -nx -q -batch -x gdbinit \ + -ex "break $pc_bp" -ex 'ignore 1 1' -ex run -ex "ptbtall $pc_expr" \ + -ex delete -ex "break protothread_test.c:$bar_line" -ex continue \ + -ex "ptbtall $bar_expr" -ex kill "$tmp/pttest" > "$tmp/gdb.out" 2>&1 || : + +bar_bp="breakpoint set -f protothread_test.c -l $bar_line" +"$LLDB" -x -b -o 'command script import lldbinit.py' \ + -o "breakpoint set -n $pc_bp -i 1" -o run -o "ptbtall $pc_expr" \ + -o 'breakpoint delete -f' -o "$bar_bp" -o continue \ + -o "ptbtall $bar_expr" -o kill "$tmp/pttest" > "$tmp/lldb.out" 2>&1 || : + +# keep the state and frame lines, without addresses +summary() { + { grep -E '^(state|#)' "$1" || : ; } | sed -E \ + -e 's/ p \*\(struct pt_thread_s \*\)0x[0-9a-f]+$//' \ + -e 's/ \(0x[0-9a-f]+\)//' +} +summary "$tmp/gdb.out" > "$tmp/gdb.sum" +summary "$tmp/lldb.out" > "$tmp/lldb.sum" + +fail=0 +for d in gdb lldb ; do + if grep -i -E 'error|not a valid command|undefined command' \ + "$tmp/$d.out" ; then + echo "$d reported an error" ; fail=1 + fi +done +if ! diff -u "$tmp/gdb.sum" "$tmp/lldb.sum" ; then + echo "gdb and lldb disagree" ; fail=1 +fi + +# each frame's line should be where that function blocks +while read -r _ func _ loc ; do + line=${loc##*:} + src=$(sed -n "${line}p" protothread_test.c) + if ! grep -q -E 'pt_(wait|yield|call)\(' <<< "$src" ; then + echo "$func at line $line doesn't block there: $src" ; fail=1 + fi +done < <(grep '^#' "$tmp/gdb.sum") + +# the threads and states, without line numbers +expected='state: running +#0 consumer_thr +state: wait +#0 producer_thr +state: running +state: ready_to_run +#0 barrier_thr +state: ready_to_run +#0 barrier_thr +state: wait +#0 barrier_thr +#1 barrier_arrive +state: wait +#0 exchange_thr +#1 barrier_arrive' +if ! diff -u <(echo "$expected") <(sed 's/ at .*//' "$tmp/gdb.sum") ; then + echo "unexpected threads or states" ; fail=1 +fi + +if [ $fail = 0 ] ; then + echo PASS +else + echo "--- gdb output" ; cat "$tmp/gdb.out" + echo "--- lldb output" ; cat "$tmp/lldb.out" +fi +exit $fail