Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
Expand Down Expand Up @@ -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`)
>
Expand Down
105 changes: 105 additions & 0 deletions debugger_test.sh
Original file line number Diff line number Diff line change
@@ -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
99 changes: 99 additions & 0 deletions lldbinit.py
Original file line number Diff line number Diff line change
@@ -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))
Loading