aboutsummaryrefslogtreecommitdiff
path: root/clang/docs/DebuggingCoroutines.rst
diff options
context:
space:
mode:
Diffstat (limited to 'clang/docs/DebuggingCoroutines.rst')
-rw-r--r--clang/docs/DebuggingCoroutines.rst431
1 files changed, 271 insertions, 160 deletions
diff --git a/clang/docs/DebuggingCoroutines.rst b/clang/docs/DebuggingCoroutines.rst
index 9eaf8d4..c62e2ea 100644
--- a/clang/docs/DebuggingCoroutines.rst
+++ b/clang/docs/DebuggingCoroutines.rst
@@ -179,8 +179,8 @@ generator and its internal state.
To do so, we can simply look into the ``gen.hdl`` variable. LLDB comes with a
pretty printer for ``std::coroutine_handle`` which will show us the internal
-state of the coroutine. For GDB, you will have to use the ``show-coro-frame``
-command provided by the :ref:`gdb-script`.
+state of the coroutine. For GDB, the pretty printer is provided by a script,
+see :ref:`gdb-script` for setup instructions.
.. image:: ./coro-generator-suspended.png
@@ -206,23 +206,16 @@ Tracking the exact suspension point
Among the compiler-generated members, the ``__coro_index`` is particularly
important. This member identifies the suspension point at which the coroutine
-is currently suspended.
+is currently suspended. However, it is non-trivial to map this number back to
+a source code location.
-However, it is non-trivial to map this number back to a source code location.
-The compiler emits debug info labels for the suspension points. This allows us
-to map the suspension point index back to a source code location. In gdb, we
-can use the ``info line`` command to get the source code location of the
-suspension point.
+For GDB, the provided :ref:`gdb-script` already takes care of this and provides
+the exact line number of the suspension point as part of the coroutine handle's
+summary string. Unfortunately, LLDB's pretty-printer does not support this, yet.
+Furthermore, those labels are only emitted starting with clang 21.0.
-::
-
- (gdb) info line -function coro_task -label __coro_resume_2
- Line 45 of "llvm-example.cpp" starts at address 0x1b1b <_ZL9coro_taski.resume+555> and ends at 0x1b46 <_ZL9coro_taski.resume+598>.
- Line 45 of "llvm-example.cpp" starts at address 0x201b <_ZL9coro_taski.destroy+555> and ends at 0x2046 <_ZL9coro_taski.destroy+598>.
- Line 45 of "llvm-example.cpp" starts at address 0x253b <_ZL9coro_taski.cleanup+555> and ends at 0x2566 <_ZL9coro_taski.cleanup+598>.
-
-LLDB does not support looking up labels. Furthermore, those labels are only emitted
-starting with clang 21.0.
+When debugging with LLDB or when using older clang versions, we will have to use
+a different approach.
For simple cases, you might still be able to guess the suspension point correctly.
Alternatively, you might also want to modify your coroutine library to store
@@ -655,33 +648,17 @@ There are two possible approaches to do so:
We can lookup their types and thereby get the types of promise
and coroutine frame.
-In gdb, one can use the following approach to devirtualize a coroutine type,
-assuming we have a ``std::coroutine_handle`` is at address 0x418eb0:
-
-::
+In general, the second approach is preferred, as it is more portable.
- (gdb) # Get the address of coroutine frame
- (gdb) print/x *0x418eb0
- $1 = 0x4019e0
- (gdb) # Get the linkage name for the coroutine
- (gdb) x 0x4019e0
- 0x4019e0 <_ZL9coro_taski>: 0xe5894855
- (gdb) # Turn off the demangler temporarily to avoid the debugger misunderstanding the name.
- (gdb) set demangle-style none
- (gdb) # The coroutine frame type is 'linkage_name.coro_frame_ty'
- (gdb) print ('_ZL9coro_taski.coro_frame_ty')*(0x418eb0)
- $2 = {__resume_fn = 0x4019e0 <coro_task(int)>, __destroy_fn = 0x402000 <coro_task(int)>, __promise = {...}, ...}
-
-In practice, one would use the ``show-coro-frame`` command provided by the
-:ref:`gdb-script`.
+To do so, we look up the types in the destroy function and not the resume function
+because the resume function pointer will be set to a ``nullptr`` as soon as a
+coroutine reaches its final suspension point. If we used the resume function,
+devirtualization would hence fail for all coroutines that have reached their final
+suspension point.
LLDB comes with devirtualization support out of the box, as part of the
-pretty-printer for ``std::coroutine_handle``. Internally, this pretty-printer
-uses the second approach. We look up the types in the destroy function and not
-the resume function because the resume function pointer will be set to a
-``nullptr`` as soon as a coroutine reaches its final suspension point. If we used
-the resume function, devirtualization would hence fail for all coroutines that
-have reached their final suspension point.
+pretty-printer for ``std::coroutine_handle``. For GDB, a similar pretty-printer
+is provided by the :ref:`gdb-script`.
Interpreting the coroutine frame in optimized builds
----------------------------------------------------
@@ -756,6 +733,26 @@ should not be thought of as directly representing the variables in the C++
source.
+Mapping suspension point indices to source code locations
+---------------------------------------------------------
+
+To aid in mapping a ``__coro_index`` back to a source code location, clang 21.0
+and newer emit special, compiler-generated labels for the suspension points.
+
+In gdb, we can use the ``info line`` command to get the source code location of
+the suspension point.
+
+::
+
+ (gdb) info line -function coro_task -label __coro_resume_2
+ Line 45 of "llvm-example.cpp" starts at address 0x1b1b <_ZL9coro_taski.resume+555> and ends at 0x1b46 <_ZL9coro_taski.resume+598>.
+ Line 45 of "llvm-example.cpp" starts at address 0x201b <_ZL9coro_taski.destroy+555> and ends at 0x2046 <_ZL9coro_taski.destroy+598>.
+ Line 45 of "llvm-example.cpp" starts at address 0x253b <_ZL9coro_taski.cleanup+555> and ends at 0x2566 <_ZL9coro_taski.cleanup+598>.
+
+LLDB does not support looking up labels, yet. For this reason, LLDB's pretty-printer
+does not show the exact line number of the suspension point.
+
+
Resources
=========
@@ -1017,156 +1014,270 @@ Note that this script requires LLDB 21.0 or newer.
GDB Debugger Script
-------------------
-For GDB, the following script provides a couple of useful commands:
+The following script provides:
-* ``async-bt`` to print the stack trace of a coroutine
-* ``show-coro-frame`` to print the coroutine frame, similar to
- LLDB's builtin pretty-printer for coroutine frames
+* a pretty-printer for coroutine handles
+* a frame filter to add coroutine frames to the built-in ``bt`` command
+* the ``get_coro_frame`` and ``get_coro_promise`` functions to be used in
+ expressions, e.g. ``p get_coro_promise(fib.coro_hdl)->current_state``
+
+It can be loaded into GDB using ``source gdb_coro_debugging.py``.
+To load this by default, add this command to your ``~/.gdbinit`` file.
.. code-block:: python
- # debugging-helper.py
+ # gdb_coro_debugging.py
import gdb
from gdb.FrameDecorator import FrameDecorator
- class SymValueWrapper():
- def __init__(self, symbol, value):
- self.sym = symbol
- self.val = value
+ import typing
+ import re
- def __str__(self):
- return str(self.sym) + " = " + str(self.val)
+ def _load_pointer_at(addr: int):
+ return gdb.Value(addr).reinterpret_cast(gdb.lookup_type('void').pointer().pointer()).dereference()
- def get_long_pointer_size():
- return gdb.lookup_type('long').pointer().sizeof
+ """
+ Devirtualized coroutine frame.
- def cast_addr2long_pointer(addr):
- return gdb.Value(addr).cast(gdb.lookup_type('long').pointer())
+ Devirtualizes the promise and frame pointer types by inspecting
+ the destroy function.
- def dereference(addr):
- return long(cast_addr2long_pointer(addr).dereference())
+ Implements `to_string` and `children` to be used by `gdb.printing.PrettyPrinter`.
+ Base class for `CoroutineHandlePrinter`.
+ """
+ class DevirtualizedCoroFrame:
+ def __init__(self, frame_ptr_raw: int, val: gdb.Value | None = None):
+ self.val = val
+ self.frame_ptr_raw = frame_ptr_raw
- class CoroutineFrame(object):
- def __init__(self, task_addr):
- self.frame_addr = task_addr
- self.resume_addr = task_addr
- self.destroy_addr = task_addr + get_long_pointer_size()
- self.promise_addr = task_addr + get_long_pointer_size() * 2
- # In the example, the continuation is the first field member of the promise_type.
- # So they have the same addresses.
- # If we want to generalize the scripts to other coroutine types, we need to be sure
- # the continuation field is the first member of promise_type.
- self.continuation_addr = self.promise_addr
+ # Get the resume and destroy pointers.
+ if frame_ptr_raw == 0:
+ self.resume_ptr = None
+ self.destroy_ptr = None
+ self.promise_ptr = None
+ self.frame_ptr = gdb.Value(frame_ptr_raw).reinterpret_cast(gdb.lookup_type("void").pointer())
+ return
- def next_task_addr(self):
- return dereference(self.continuation_addr)
+ # Get the resume and destroy pointers.
+ self.resume_ptr = _load_pointer_at(frame_ptr_raw)
+ self.destroy_ptr = _load_pointer_at(frame_ptr_raw + 8)
+
+ # Devirtualize the promise and frame pointer types.
+ frame_type = gdb.lookup_type("void")
+ promise_type = gdb.lookup_type("void")
+ self.destroy_func = gdb.block_for_pc(int(self.destroy_ptr))
+ if self.destroy_func is not None:
+ frame_var = gdb.lookup_symbol("__coro_frame", self.destroy_func, gdb.SYMBOL_VAR_DOMAIN)[0]
+ if frame_var is not None:
+ frame_type = frame_var.type
+ promise_var = gdb.lookup_symbol("__promise", self.destroy_func, gdb.SYMBOL_VAR_DOMAIN)[0]
+ if promise_var is not None:
+ promise_type = promise_var.type.strip_typedefs()
+
+ # If the type has a template argument, prefer it over the devirtualized type.
+ if self.val is not None:
+ promise_type_template_arg = self.val.type.template_argument(0)
+ if promise_type_template_arg is not None and promise_type_template_arg.code != gdb.TYPE_CODE_VOID:
+ promise_type = promise_type_template_arg
+
+ self.promise_ptr = gdb.Value(frame_ptr_raw + 16).reinterpret_cast(promise_type.pointer())
+ self.frame_ptr = gdb.Value(frame_ptr_raw).reinterpret_cast(frame_type.pointer())
+
+ # Try to get the suspension point index and look up the exact line entry.
+ self.suspension_point_index = int(self.frame_ptr.dereference()["__coro_index"]) if frame_type.code == gdb.TYPE_CODE_STRUCT else None
+ self.resume_func = gdb.block_for_pc(int(self.resume_ptr))
+ self.resume_label = None
+ if self.resume_func is not None and self.suspension_point_index is not None:
+ label_name = f"__coro_resume_{self.suspension_point_index}"
+ self.resume_label = gdb.lookup_symbol(label_name, self.resume_func, gdb.SYMBOL_LABEL_DOMAIN)[0]
+
+ def get_function_name(self):
+ if self.destroy_func is None:
+ return None
+ name = self.destroy_func.function.name
+ # Strip the "clone" suffix if it exists.
+ if "() [clone " in name:
+ name = name[:name.index("() [clone ")]
+ return name
+
+ def to_string(self):
+ result = "coro(" + str(self.frame_ptr_raw) + ")"
+ if self.destroy_func is not None:
+ result += ": " + self.get_function_name()
+ if self.resume_label is not None:
+ result += ", line " + str(self.resume_label.line)
+ if self.suspension_point_index is not None:
+ result += ", suspension point " + str(self.suspension_point_index)
+ return result
+
+ def children(self):
+ if self.resume_ptr is None:
+ return [
+ ("coro_frame", self.frame_ptr),
+ ]
+ else:
+ return [
+ ("resume", self.resume_ptr),
+ ("destroy", self.destroy_ptr),
+ ("promise", self.promise_ptr),
+ ("coro_frame", self.frame_ptr)
+ ]
- class CoroutineFrameDecorator(FrameDecorator):
- def __init__(self, coro_frame):
- super(CoroutineFrameDecorator, self).__init__(None)
- self.coro_frame = coro_frame
- self.resume_func = dereference(self.coro_frame.resume_addr)
- self.resume_func_block = gdb.block_for_pc(self.resume_func)
- if self.resume_func_block is None:
- raise Exception('Not stackless coroutine.')
- self.line_info = gdb.find_pc_line(self.resume_func)
- def address(self):
- return self.resume_func
+ # Works for both libc++ and libstdc++.
+ libcxx_corohdl_regex = re.compile('^std::__[A-Za-z0-9]+::coroutine_handle<.+>$|^std::coroutine_handle<.+>(( )?&)?$')
- def filename(self):
- return self.line_info.symtab.filename
+ def _extract_coro_frame_ptr_from_handle(val: gdb.Value):
+ if libcxx_corohdl_regex.match(val.type.strip_typedefs().name) is None:
+ raise ValueError("Expected a std::coroutine_handle, got %s" % val.type.strip_typedefs().name)
- def frame_args(self):
- return [SymValueWrapper("frame_addr", cast_addr2long_pointer(self.coro_frame.frame_addr)),
- SymValueWrapper("promise_addr", cast_addr2long_pointer(self.coro_frame.promise_addr)),
- SymValueWrapper("continuation_addr", cast_addr2long_pointer(self.coro_frame.continuation_addr))
- ]
+ # We expect the coroutine handle to have a single field, which is the frame pointer.
+ # This heuristic works for both libc++ and libstdc++.
+ fields = val.type.fields()
+ if len(fields) != 1:
+ raise ValueError("Expected 1 field, got %d" % len(fields))
+ return int(val[fields[0]])
- def function(self):
- return self.resume_func_block.function.print_name
- def line(self):
- return self.line_info.line
-
- class StripDecorator(FrameDecorator):
- def __init__(self, frame):
- super(StripDecorator, self).__init__(frame)
- self.frame = frame
- f = frame.function()
- self.function_name = f
-
- def __str__(self, shift = 2):
- addr = "" if self.address() is None else '%#x' % self.address() + " in "
- location = "" if self.filename() is None else " at " + self.filename() + ":" + str(self.line())
- return addr + self.function() + " " + str([str(args) for args in self.frame_args()]) + location
-
- class CoroutineFilter:
- def create_coroutine_frames(self, task_addr):
- frames = []
- while task_addr != 0:
- coro_frame = CoroutineFrame(task_addr)
- frames.append(CoroutineFrameDecorator(coro_frame))
- task_addr = coro_frame.next_task_addr()
- return frames
-
- class AsyncStack(gdb.Command):
+ """
+ Pretty printer for `std::coroutine_handle<T>`
+
+ Works for both libc++ and libstdc++.
+
+ It prints the coroutine handle as a struct with the following fields:
+ - resume: the resume function pointer
+ - destroy: the destroy function pointer
+ - promise: the promise pointer
+ - coro_frame: the coroutine frame pointer
+
+ Most of the functionality is implemented in `DevirtualizedCoroFrame`.
+ """
+ class CoroutineHandlePrinter(DevirtualizedCoroFrame):
+ def __init__(self, val : gdb.Value):
+ frame_ptr_raw = _extract_coro_frame_ptr_from_handle(val)
+ super(CoroutineHandlePrinter, self).__init__(frame_ptr_raw, val)
+
+
+ def build_pretty_printer():
+ pp = gdb.printing.RegexpCollectionPrettyPrinter("coroutine")
+ pp.add_printer('std::coroutine_handle', libcxx_corohdl_regex, CoroutineHandlePrinter)
+ return pp
+
+ gdb.printing.register_pretty_printer(
+ gdb.current_objfile(),
+ build_pretty_printer())
+
+
+ """
+ Get the coroutine frame pointer from a coroutine handle.
+
+ Usage:
+ ```
+ p *get_coro_frame(coroutine_hdl)
+ ```
+ """
+ class GetCoroFrame(gdb.Function):
def __init__(self):
- super(AsyncStack, self).__init__("async-bt", gdb.COMMAND_USER)
+ super(GetCoroFrame, self).__init__("get_coro_frame")
- def invoke(self, arg, from_tty):
- coroutine_filter = CoroutineFilter()
- argv = gdb.string_to_argv(arg)
- if len(argv) == 0:
- try:
- task = gdb.parse_and_eval('__coro_frame')
- task = int(str(task.address), 16)
- except Exception:
- print ("Can't find __coro_frame in current context.\n" +
- "Please use `async-bt` in stackless coroutine context.")
- return
- elif len(argv) != 1:
- print("usage: async-bt <pointer to task>")
- return
- else:
- task = int(argv[0], 16)
+ def invoke(self, coroutine_hdl_raw):
+ return CoroutineHandlePrinter(coroutine_hdl_raw).frame_ptr
+
+ GetCoroFrame()
- frames = coroutine_filter.create_coroutine_frames(task)
- i = 0
- for f in frames:
- print '#'+ str(i), str(StripDecorator(f))
- i += 1
- return
- AsyncStack()
+ """
+ Get the coroutine frame pointer from a coroutine handle.
- class ShowCoroFrame(gdb.Command):
+ Usage:
+ ```
+ p *get_coro_promise(coroutine_hdl)
+ ```
+ """
+ class GetCoroFrame(gdb.Function):
def __init__(self):
- super(ShowCoroFrame, self).__init__("show-coro-frame", gdb.COMMAND_USER)
+ super(GetCoroFrame, self).__init__("get_coro_promise")
- def invoke(self, arg, from_tty):
- argv = gdb.string_to_argv(arg)
- if len(argv) != 1:
- print("usage: show-coro-frame <address of coroutine frame>")
- return
+ def invoke(self, coroutine_hdl_raw):
+ return CoroutineHandlePrinter(coroutine_hdl_raw).promise_ptr
- addr = int(argv[0], 16)
- block = gdb.block_for_pc(long(cast_addr2long_pointer(addr).dereference()))
- if block is None:
- print "block " + str(addr) + " is None."
- return
+ GetCoroFrame()
+
+
+ """
+ Decorator for coroutine frames.
+
+ Used by `CoroutineFrameFilter` to add the coroutine frames to the built-in `bt` command.
+ """
+ class CoroutineFrameDecorator(FrameDecorator):
+ def __init__(self, coro_frame: DevirtualizedCoroFrame, inferior_frame: gdb.Frame):
+ super(CoroutineFrameDecorator, self).__init__(inferior_frame)
+ self.coro_frame = coro_frame
+
+ def function(self):
+ func_name = self.coro_frame.get_function_name()
+ if func_name is not None:
+ return "[async] " + func_name
+ return "[async] coroutine (coro_frame=" + str(self.coro_frame.frame_ptr_raw) + ")"
+
+ def address(self):
+ return None
+
+ def filename(self):
+ if self.coro_frame.destroy_func is not None:
+ return self.coro_frame.destroy_func.function.symtab.filename
+ return None
+
+ def line(self):
+ if self.coro_frame.resume_label is not None:
+ return self.coro_frame.resume_label.line
+ return None
+
+ def frame_args(self):
+ return []
+
+ def frame_locals(self):
+ return []
+
+
+ def _get_continuation(promise: gdb.Value) -> DevirtualizedCoroFrame | None:
+ try:
+ # TODO: adjust this according for your coroutine framework
+ return DevirtualizedCoroFrame(_extract_coro_frame_ptr_from_handle(promise["continuation"]))
+ except Exception as e:
+ return None
- # Disable demangling since gdb will treat names starting with `_Z`(The marker for Itanium ABI) specially.
- gdb.execute("set demangle-style none")
- coro_frame_type = gdb.lookup_type(block.function.linkage_name + ".coro_frame_ty")
- coro_frame_ptr_type = coro_frame_type.pointer()
- coro_frame = gdb.Value(addr).cast(coro_frame_ptr_type).dereference()
+ def _create_coroutine_frames(coro_frame: DevirtualizedCoroFrame, inferior_frame: gdb.Frame):
+ while coro_frame is not None:
+ yield CoroutineFrameDecorator(coro_frame, inferior_frame)
+ coro_frame = _get_continuation(coro_frame.promise_ptr)
- gdb.execute("set demangle-style auto")
- gdb.write(coro_frame.format_string(pretty_structs = True))
- ShowCoroFrame()
+ """
+ Frame filter to add coroutine frames to the built-in `bt` command.
+ """
+ class CppCoroutineFrameFilter():
+ def __init__(self):
+ self.name = "CppCoroutineFrameFilter"
+ self.priority = 50
+ self.enabled = True
+ # Register this frame filter with the global frame_filters dictionary.
+ gdb.frame_filters[self.name] = self
+
+ def filter(self, frame_iter: typing.Iterable[gdb.FrameDecorator]):
+ for frame in frame_iter:
+ yield frame
+ inferior_frame = frame.inferior_frame()
+ try:
+ promise_ptr = inferior_frame.read_var("__promise")
+ except Exception:
+ continue
+ parent_coro = _get_continuation(promise_ptr)
+ if parent_coro is not None:
+ yield from _create_coroutine_frames(parent_coro, inferior_frame)
+
+ CppCoroutineFrameFilter()
Further Reading
---------------